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 // Perl's abrupt-exit builtins are `die` (raises an exception that
264 // unwinds to the nearest `eval`) and `exit` (terminates the
265 // process). Neither has a dedicated grammar node: every call form
266 // (`die;`, `die "m"`, `die("m")`, `... or die "m"`) nests a
267 // `call_expression_with_bareword` holding just the callee name, so
268 // matching that one kind counts each occurrence exactly once
269 // whatever wrapper carries the arguments (#1270). Compare the
270 // bareword text for the same reason Go matches `panic` and Lua
271 // matches `error`.
272 //
273 // `CORE::die` / `CORE::exit` are the same builtins spelled through
274 // the `CORE::` namespace — the way code that has overridden `die`
275 // reaches the real one — and keep that qualifier in the bareword
276 // text, so they are matched by name too.
277 //
278 // Deliberate exclusions:
279 // - Any other package-qualified callee keeps its qualifier in the
280 // bareword text (`Carp::croak`), so it can never equal the four
281 // spellings matched here.
282 // - `croak` / `confess` are Carp *library* functions, not builtins;
283 // an unqualified `croak "m"` is left uncounted rather than
284 // guessing that the module is loaded.
285 // - `$obj->die` parses as a `method_invocation` whose callee is a
286 // plain `identifier`, never a bareword, so a user method named
287 // `die` is not counted.
288 //
289 // Known artifact: a fat-comma hash key auto-quotes its bareword in
290 // Perl, but the grammar still emits `call_expression_with_bareword`
291 // for it, so `(die => 1)` counts as an exit. Gating on the `=>`
292 // that follows would need a forward sibling lookup from inside the
293 // metric walk, which #1096 took out of these bodies; the same node
294 // already counts as an ABC branch (`src/metrics/abc/perl.rs`), so
295 // the artifact is accepted rather than paid for here.
296 fn compute<'a>(node: &Node<'a>, code: &'a [u8], stats: &mut Stats) {
297 let is_abrupt_exit_builtin = node.kind_id() == Perl::CallExpressionWithBareword
298 && matches!(
299 node.utf8_text(code),
300 Some("die" | "exit" | "CORE::die" | "CORE::exit")
301 );
302 if node.kind_id() == Perl::ReturnExpression || is_abrupt_exit_builtin {
303 stats.exit += 1;
304 }
305 }
306}
307
308impl Exit for KotlinCode {
309 fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
310 if matches!(
311 node.kind_id().into(),
312 Kotlin::ReturnExpression | Kotlin::ThrowExpression
313 ) {
314 stats.exit += 1;
315 }
316 }
317}
318
319impl Exit for LuaCode {
320 // Lua has no `throw`/`raise` keyword: the abrupt-exit primitives are the
321 // built-in `error(...)` (raises a Lua error that unwinds to the nearest
322 // `pcall`) and `os.exit(...)` (terminates the process). Both parse as a
323 // `function_call` whose `name` field is the callee. `error(...)` is a
324 // bare `identifier`; `os.exit(...)` is a `dot_index_expression` with text
325 // `os.exit`. Count them as exits alongside `return`. Matching the exact
326 // callee text means a user call such as `foo()` or `myError()` is not
327 // counted, mirroring how Bash/Elixir match the bare builtin name.
328 fn compute<'a>(node: &Node<'a>, code: &'a [u8], stats: &mut Stats) {
329 if node.kind_id() == Lua::ReturnStatement {
330 stats.exit += 1;
331 } else if node.kind_id() == Lua::FunctionCall
332 && let Some(name) = node.child_by_field_name("name")
333 && matches!(name.utf8_text(code), Some("error" | "os.exit"))
334 {
335 stats.exit += 1;
336 }
337 }
338}
339
340impl Exit for BashCode {
341 fn compute<'a>(node: &Node<'a>, code: &'a [u8], stats: &mut Stats) {
342 // Bash has no `return_statement` node: `return` and `exit` are
343 // ordinary builtins parsed as `Bash::Command` whose `name` field
344 // points at a `Bash::CommandName`. Identify them by comparing the
345 // command-name text against the literal builtins.
346 if matches!(node.kind_id().into(), Bash::Command)
347 && let Some(name) = node.child_by_field_name("name")
348 && matches!(name.utf8_text(code), Some("return" | "exit"))
349 {
350 stats.exit += 1;
351 }
352 }
353}
354
355impl Exit for TclCode {
356 fn compute<'a>(node: &Node<'a>, code: &'a [u8], stats: &mut Stats) {
357 // Tcl has no return keyword node; `return` is a generic Command whose
358 // name field is a simple_word with text "return" — the same
359 // leading-word resolution the Cognitive / Cyclomatic `switch` and
360 // `for` detectors use, shared rather than restated here.
361 //
362 // `error` (raises an error that unwinds to the nearest `catch`),
363 // the Tcl 8.6 `throw`, and `exit` (terminates the interpreter —
364 // counted for the same reason Perl's, Ruby's and Bash's `exit`
365 // are) are the abrupt-exit builtins and parse to the same generic
366 // Command shape — the vendored grammar has no dedicated rule for
367 // any of them, so the leading word is the only seam (#1270,
368 // lesson 19). Because `tcl_command_name` reads the `name` field,
369 // `error` in argument position (`puts error`) is a `word_list`
370 // child and is not counted.
371 if matches!(
372 crate::lang_helpers::tcl::tcl_command_name(node, code),
373 Some("return" | "error" | "throw" | "exit")
374 ) {
375 stats.exit += 1;
376 }
377 }
378}
379
380impl Exit for IrulesCode {
381 fn compute<'a>(node: &Node<'a>, code: &'a [u8], stats: &mut Stats) {
382 // Like Tcl, iRules has no `return` keyword node — `return` is a
383 // generic Command (it is not among the grammar's `_builtin`
384 // commands). The bare name word can surface as either `simple_word`
385 // or `concat_word` depending on context, so match on the name text
386 // rather than a fixed kind. A multi-value `return $a $b` still has a
387 // single `name` field and is counted once.
388 //
389 // `error` is the Tcl abrupt-exit builtin and reaches iRules
390 // unchanged; re-derived against the iRules grammar rather than
391 // assumed from Tcl's, it parses to the same `command` +
392 // name-word shape (#1270). Tcl 8.6's `throw` is *not* matched
393 // here: TMOS iRules runs a Tcl 8.4-derived interpreter that has
394 // no `throw` builtin, so the word could only ever name a user
395 // proc. iRules flow commands (`event disable`, `TCP::close`,
396 // `reject`, `drop`) remain deliberately uncounted as exits in
397 // v1.
398 // Normalised for the same reason `tcl_command_name` is: `::return`
399 // is `return` through the global namespace. This walker resolves
400 // the name itself rather than through that helper, so the strip
401 // has to be repeated here (#1381 review).
402 if node.kind_id() == Irules::Command
403 && let Some(name) = node.child_by_field_name("name")
404 && matches!(
405 name.utf8_text(code)
406 .map(crate::lang_helpers::strip_global_qualifier),
407 Some("return" | "error")
408 )
409 {
410 stats.exit += 1;
411 }
412 }
413}
414
415impl Exit for PhpCode {
416 // tree-sitter-php 0.24.2's `exit_statement` rule covers `exit` only
417 // (with or without parentheses); `die(...)` is grammar-classified as
418 // a `function_call_expression` and therefore is NOT counted here.
419 // Detecting `die` would require inspecting call-expression callee
420 // text — brittle and likely to false-match user-defined `die`
421 // functions. Modern PHP idiom favors `throw new Exception()` over
422 // `die`, so leaving this asymmetric is acceptable.
423 fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
424 if matches!(
425 node.kind_id().into(),
426 Php::ReturnStatement | Php::YieldExpression | Php::ThrowExpression | Php::ExitStatement
427 ) {
428 stats.exit += 1;
429 }
430 }
431}
432
433// Real defaults — no functions to return from. Audited in #188.
434implement_metric_trait!(Exit, PreprocCode, CcommentCode);
435
436impl Exit for RubyCode {
437 // Ruby's `return` is the only dedicated grammar node for an
438 // intra-function exit; `yield` passes control to the block but does
439 // not exit the enclosing method. tree-sitter-ruby exposes the
440 // `return_statement` rule under two aliased visible kinds
441 // (`Return`, `Return2`); the `Return3` token is the bare `return`
442 // keyword inside those nodes and is not counted on its own.
443 //
444 // `raise` and `exit` are ordinary method calls with no grammar node
445 // of their own — which is the shape the Go / Lua / Elixir impls
446 // above already match by callee text, not a reason to leave them
447 // uncounted (#1270). Both parse as a `call` whose `method` field is
448 // a bare `identifier`; `Checker::is_call` carries the four visible
449 // `call` aliases, so the arm cannot miss one by position.
450 //
451 // `exit!` (`Kernel#exit!`, the immediate process exit) is the third
452 // spelling, and the explicit `Kernel.raise` / `Kernel.exit` receiver
453 // *is* the builtin — it is reached for precisely when a DSL shadows
454 // the bare name — so a `Kernel` constant receiver is admitted.
455 //
456 // Deliberate exclusions:
457 // - A call with any other `receiver` (`obj.raise`, `self.exit`) is a
458 // user method, never the Kernel builtin — the same bare-callee
459 // gate Go uses to keep `foo.panic()` out.
460 // - A *bare* `raise` / `exit` with no arguments parses as a plain
461 // `identifier`, indistinguishable from reading a local variable
462 // of that name, so the argument-less re-raise idiom inside
463 // `rescue` goes uncounted rather than blanket-matching every
464 // identifier. A bare `exit!` is different: the `!` cannot name a
465 // local, so the grammar emits a `call` and it is counted.
466 // - `fail` (a Kernel alias of `raise`), `abort`, and `throw`
467 // (catch/throw non-local flow) are not matched: each is a common
468 // user or test-DSL method name, and the counted set stays the two
469 // primitives the cross-language exit table names.
470 //
471 // The callee is inspected before the receiver: every call has a
472 // `method`, and the text test rejects nearly all of them, so the
473 // receiver lookup runs only for the handful that spell a builtin.
474 fn compute<'a>(node: &Node<'a>, code: &'a [u8], stats: &mut Stats) {
475 if matches!(node.kind_id().into(), Ruby::Return | Ruby::Return2) {
476 stats.exit += 1;
477 } else if Self::is_call(node)
478 && let Some(method) = node.child_by_field_name("method")
479 && method.kind_id() == Ruby::Identifier
480 && matches!(method.utf8_text(code), Some("raise" | "exit" | "exit!"))
481 && node.child_by_field_name("receiver").is_none_or(|receiver| {
482 receiver.kind_id() == Ruby::Constant && receiver.utf8_text(code) == Some("Kernel")
483 })
484 {
485 stats.exit += 1;
486 }
487 }
488}
489
490impl Exit for ElixirCode {
491 // Elixir has no `return` statement: the last expression in a function
492 // body is the return value. Early-exit happens through `throw`,
493 // `raise`, `reraise`, or `exit`, all of which surface as `Call`
494 // nodes whose target is an `Identifier` whose text spells the
495 // keyword. Mirrors the Bash/Tcl pattern of comparing target text.
496 fn compute<'a>(node: &Node<'a>, code: &'a [u8], stats: &mut Stats) {
497 if node.kind_id() == Elixir::Call
498 && let Some(target) = node.child_by_field_name("target")
499 && target.kind_id() == Elixir::Identifier
500 && matches!(
501 target.utf8_text(code),
502 Some("throw" | "raise" | "reraise" | "exit")
503 )
504 {
505 stats.exit += 1;
506 }
507 }
508}
509
510#[cfg(test)]
511#[allow(
512 clippy::float_cmp,
513 clippy::cast_precision_loss,
514 clippy::cast_possible_truncation,
515 clippy::cast_sign_loss,
516 clippy::similar_names,
517 clippy::doc_markdown,
518 clippy::needless_raw_string_hashes,
519 clippy::too_many_lines
520)]
521mod tests {
522 use crate::test_support::{
523 check_func_space_only_shim, check_metrics_only_shim, child_space, function_space,
524 };
525
526 use super::*;
527
528 // Nexits pulls Nom for its per-function average divisor, which is
529 // also what this module's one `metric.nom.functions_sum()`
530 // assertion reads.
531 check_metrics_only_shim!(check_metrics, Nexits);
532 check_func_space_only_shim!(check_func_space, Nexits);
533
534 /// A `Stats::default()` that never sees an
535 /// observation must not leak the `usize::MAX` sentinel for
536 /// `exit_min`. The getter collapses the sentinel to `0.0` so
537 /// JSON never emits `1.8446744e19`.
538 #[test]
539 fn exit_empty_file_min_is_zero() {
540 let stats = Stats::default();
541 assert_eq!(stats.nexits_min(), 0);
542 }
543
544 #[cfg(feature = "python")]
545 #[test]
546 fn python_no_exit() {
547 check_metrics::<PythonParser>("a = 42", "foo.py", |metric| {
548 // 0 functions
549 insta::assert_json_snapshot!(
550 metric.nexits,
551 @r#"
552 {
553 "sum": 0,
554 "average": 0.0,
555 "min": 0,
556 "max": 0
557 }
558 "#
559 );
560 });
561 }
562
563 #[cfg(feature = "rust")]
564 #[test]
565 fn rust_no_exit() {
566 check_metrics::<RustParser>("let a = 42;", "foo.rs", |metric| {
567 // 0 functions
568 insta::assert_json_snapshot!(
569 metric.nexits,
570 @r#"
571 {
572 "sum": 0,
573 "average": 0.0,
574 "min": 0,
575 "max": 0
576 }
577 "#
578 );
579 });
580 }
581
582 #[cfg(feature = "rust")]
583 #[test]
584 fn rust_question_mark() {
585 check_metrics::<RustParser>("let _ = a? + b? + c?;", "foo.rs", |metric| {
586 // 0 functions
587 insta::assert_json_snapshot!(
588 metric.nexits,
589 @r#"
590 {
591 "sum": 3,
592 "average": 3.0,
593 "min": 3,
594 "max": 3
595 }
596 "#
597 );
598 });
599 }
600
601 // Regression for #243: `Exit for RustCode` used to add 1 whenever
602 // a function_item with an explicit `-> T` was visited. Because the
603 // spaces traversal pushes a new State *before* Exit::compute runs
604 // for that function_item, every Rust function with an explicit
605 // return type was getting one extra exit on top of its real
606 // `return` / `?` exits. The fix drops the spurious clause; this
607 // test pins exit == 1 for a function with one explicit return.
608 #[cfg(feature = "rust")]
609 #[test]
610 fn rust_explicit_return_with_return_type() {
611 check_metrics::<RustParser>("fn foo() -> i32 { return 1; }", "foo.rs", |metric| {
612 // 1 explicit return / 1 space
613 insta::assert_json_snapshot!(
614 metric.nexits,
615 @r#"
616 {
617 "sum": 1,
618 "average": 1.0,
619 "min": 0,
620 "max": 1
621 }
622 "#
623 );
624 });
625 }
626
627 // Regression for #243: an implicit final-expression return must
628 // NOT count as an exit — matching every other language's
629 // convention (Java, C++, Go, etc. don't count implicit returns).
630 #[cfg(feature = "rust")]
631 #[test]
632 fn rust_implicit_return_not_counted() {
633 check_metrics::<RustParser>("fn foo() -> i32 { 0 }", "foo.rs", |metric| {
634 // 0 explicit exits / 1 space
635 insta::assert_json_snapshot!(
636 metric.nexits,
637 @r#"
638 {
639 "sum": 0,
640 "average": 0.0,
641 "min": 0,
642 "max": 0
643 }
644 "#
645 );
646 });
647 }
648
649 // Regression for #243: a function with both an explicit return on
650 // one branch and an implicit final expression should count only
651 // the explicit return.
652 #[cfg(feature = "rust")]
653 #[test]
654 fn rust_mixed_explicit_and_implicit_return() {
655 check_metrics::<RustParser>(
656 "fn foo(x: bool) -> i32 { if x { return 1; } 0 }",
657 "foo.rs",
658 |metric| {
659 // 1 explicit return; the implicit `0` is not an exit
660 insta::assert_json_snapshot!(
661 metric.nexits,
662 @r#"
663 {
664 "sum": 1,
665 "average": 1.0,
666 "min": 0,
667 "max": 1
668 }
669 "#
670 );
671 },
672 );
673 }
674
675 // Regression for #243: `?` inside a function body is the only
676 // implicit-exit form that does count, and the function having an
677 // explicit `Result` return type must not double it.
678 #[cfg(feature = "rust")]
679 #[test]
680 fn rust_question_mark_in_function() {
681 check_metrics::<RustParser>(
682 "fn foo() -> Result<i32, ()> { Ok(do_thing()?) }",
683 "foo.rs",
684 |metric| {
685 // 1 `?` operator, no explicit `return`
686 insta::assert_json_snapshot!(
687 metric.nexits,
688 @r#"
689 {
690 "sum": 1,
691 "average": 1.0,
692 "min": 0,
693 "max": 1
694 }
695 "#
696 );
697 },
698 );
699 }
700
701 // Regression for #243: a unit-returning function with no
702 // explicit `return` or `?` must report 0 exits.
703 #[cfg(feature = "rust")]
704 #[test]
705 fn rust_unit_return_no_exit() {
706 check_metrics::<RustParser>("fn foo() { let _x = 1; }", "foo.rs", |metric| {
707 // 0 exits / 1 space
708 insta::assert_json_snapshot!(
709 metric.nexits,
710 @r#"
711 {
712 "sum": 0,
713 "average": 0.0,
714 "min": 0,
715 "max": 0
716 }
717 "#
718 );
719 });
720 }
721
722 #[cfg(feature = "c")]
723 #[test]
724 fn c_no_exit() {
725 check_metrics::<CParser>("int a = 42;", "foo.c", |metric| {
726 // 0 functions
727 insta::assert_json_snapshot!(
728 metric.nexits,
729 @r#"
730 {
731 "sum": 0,
732 "average": 0.0,
733 "min": 0,
734 "max": 0
735 }
736 "#
737 );
738 });
739 }
740
741 /// Multiple `return` statements across `if` / `else` branches. Every
742 /// `Cpp::ReturnStatement` adds +1 — there is no early-out collapse.
743 #[cfg(feature = "c")]
744 #[test]
745 fn c_multiple_returns_in_branches() {
746 check_metrics::<CParser>(
747 "int f(int x) {
748 if (x < 0) {
749 return -1;
750 } else if (x == 0) {
751 return 0;
752 } else {
753 return 1;
754 }
755 }",
756 "foo.c",
757 |metric| {
758 // 1 function, 3 returns
759 assert_eq!(metric.nexits.nexits_sum(), 3);
760 assert_eq!(metric.nexits.nexits_max(), 3);
761 insta::assert_json_snapshot!(
762 metric.nexits,
763 @r#"
764 {
765 "sum": 3,
766 "average": 3.0,
767 "min": 0,
768 "max": 3
769 }
770 "#
771 );
772 },
773 );
774 }
775
776 /// The raison d'être of `LANG::C` (#721): C code that uses C++
777 /// keywords (`new`, `class`, `delete`) as plain identifiers parses
778 /// cleanly through `tree-sitter-c`, where the C++ grammar
779 /// ERROR-cascades. The load-bearing assertion is `!root.has_error()`:
780 /// the C++ grammar errors on this input yet *still* recovers a
781 /// function node and two `return`s, so a metric-count assertion alone
782 /// does not distinguish the two grammars — only the error-free parse
783 /// does. C has no `throw`, so `return` is the sole exit kind.
784 #[cfg(feature = "c")]
785 #[test]
786 fn c_keyword_identifiers_parse_and_returns_count() {
787 use std::path::PathBuf;
788
789 let source = "int process(int new, int class) {
790 int delete = new + class;
791 if (delete > 0) {
792 return delete;
793 }
794 return 0;
795 }";
796 let parser = CParser::new(source.as_bytes().to_vec(), &PathBuf::from("foo.c"), None);
797 assert!(
798 !parser.root().has_error(),
799 "C grammar must parse C++-keyword identifiers without an error cascade"
800 );
801
802 check_metrics::<CParser>(source, "foo.c", |metric| {
803 assert_eq!(metric.nom.functions_sum(), 1);
804 assert_eq!(metric.nexits.nexits_sum(), 2);
805 });
806 }
807
808 /// `return` statements inside `try` and `catch` blocks both count;
809 /// the impl matches `Cpp::ReturnStatement` regardless of enclosing
810 /// scope. C++-only: bare C has no `try`/`catch`.
811 #[cfg(feature = "cpp")]
812 #[test]
813 fn cpp_return_in_try_catch() {
814 check_metrics::<CppParser>(
815 "int f(int x) {
816 try {
817 if (x == 0) {
818 return 1;
819 }
820 return 2;
821 } catch (...) {
822 return -1;
823 }
824 }",
825 "foo.cpp",
826 |metric| {
827 // 1 function, 3 returns (2 in try, 1 in catch); no
828 // `throw` here, so the return-only path stays at 3.
829 assert_eq!(metric.nexits.nexits_sum(), 3);
830 assert_eq!(metric.nexits.nexits_max(), 3);
831 insta::assert_json_snapshot!(
832 metric.nexits,
833 @r#"
834 {
835 "sum": 3,
836 "average": 3.0,
837 "min": 0,
838 "max": 3
839 }
840 "#
841 );
842 },
843 );
844 }
845
846 /// Early `return` inside a loop body is counted separately from the
847 /// trailing return — every reachable `return` is an exit.
848 #[cfg(feature = "c")]
849 #[test]
850 fn c_early_return_in_loop() {
851 check_metrics::<CParser>(
852 "int find(int* a, int n, int target) {
853 for (int i = 0; i < n; ++i) {
854 if (a[i] == target) {
855 return i;
856 }
857 }
858 return -1;
859 }",
860 "foo.c",
861 |metric| {
862 // 1 function, 2 returns
863 assert_eq!(metric.nexits.nexits_sum(), 2);
864 assert_eq!(metric.nexits.nexits_max(), 2);
865 insta::assert_json_snapshot!(
866 metric.nexits,
867 @r#"
868 {
869 "sum": 2,
870 "average": 2.0,
871 "min": 0,
872 "max": 2
873 }
874 "#
875 );
876 },
877 );
878 }
879
880 /// `void` function with no explicit `return` — exit count is 0.
881 /// The implicit fall-through return is intentionally not modelled.
882 #[cfg(feature = "c")]
883 #[test]
884 fn c_void_no_explicit_return() {
885 check_metrics::<CParser>(
886 "void greet(const char* who) {
887 printf(\"hi %s\\n\", who);
888 }",
889 "foo.c",
890 |metric| {
891 // 1 function with zero ReturnStatement nodes.
892 assert_eq!(metric.nexits.nexits_sum(), 0);
893 assert_eq!(metric.nexits.nexits_max(), 0);
894 insta::assert_json_snapshot!(
895 metric.nexits,
896 @r#"
897 {
898 "sum": 0,
899 "average": 0.0,
900 "min": 0,
901 "max": 0
902 }
903 "#
904 );
905 },
906 );
907 }
908
909 #[cfg(feature = "javascript")]
910 #[test]
911 fn javascript_no_exit() {
912 check_metrics::<JavascriptParser>("var a = 42;", "foo.js", |metric| {
913 // 0 functions
914 insta::assert_json_snapshot!(
915 metric.nexits,
916 @r#"
917 {
918 "sum": 0,
919 "average": 0.0,
920 "min": 0,
921 "max": 0
922 }
923 "#
924 );
925 });
926 }
927
928 #[cfg(feature = "javascript")]
929 #[test]
930 fn javascript_simple_function() {
931 check_metrics::<JavascriptParser>(
932 "function f(a, b) {
933 if (a) {
934 return a;
935 }
936 return b;
937 }",
938 "foo.js",
939 |metric| {
940 // 1 function with 2 return statements
941 insta::assert_json_snapshot!(
942 metric.nexits,
943 @r#"
944 {
945 "sum": 2,
946 "average": 2.0,
947 "min": 0,
948 "max": 2
949 }
950 "#
951 );
952 },
953 );
954 }
955
956 #[cfg(feature = "javascript")]
957 #[test]
958 fn javascript_nested_functions() {
959 check_metrics::<JavascriptParser>(
960 "function outer() {
961 function inner() {
962 return 1;
963 }
964 return inner();
965 }",
966 "foo.js",
967 |metric| {
968 // 2 functions, each with 1 return
969 insta::assert_json_snapshot!(
970 metric.nexits,
971 @r#"
972 {
973 "sum": 2,
974 "average": 1.0,
975 "min": 0,
976 "max": 1
977 }
978 "#
979 );
980 },
981 );
982 }
983
984 #[cfg(feature = "python")]
985 #[test]
986 fn python_simple_function() {
987 check_metrics::<PythonParser>(
988 "def f(a, b):
989 if a:
990 return a",
991 "foo.py",
992 |metric| {
993 // 1 function
994 insta::assert_json_snapshot!(
995 metric.nexits,
996 @r#"
997 {
998 "sum": 1,
999 "average": 1.0,
1000 "min": 0,
1001 "max": 1
1002 }
1003 "#
1004 );
1005 },
1006 );
1007 }
1008
1009 #[cfg(feature = "python")]
1010 #[test]
1011 fn python_more_functions() {
1012 check_metrics::<PythonParser>(
1013 "def f(a, b):
1014 if a:
1015 return a
1016 def f(a, b):
1017 if b:
1018 return b",
1019 "foo.py",
1020 |metric| {
1021 // 2 functions
1022 insta::assert_json_snapshot!(
1023 metric.nexits,
1024 @r#"
1025 {
1026 "sum": 2,
1027 "average": 1.0,
1028 "min": 0,
1029 "max": 1
1030 }
1031 "#
1032 );
1033 },
1034 );
1035 }
1036
1037 #[cfg(feature = "python")]
1038 #[test]
1039 fn python_nested_functions() {
1040 check_metrics::<PythonParser>(
1041 "def f(a, b):
1042 def foo(a):
1043 if a:
1044 return 1
1045 bar = lambda a: lambda b: b or True or True
1046 return bar(foo(a))(a)",
1047 "foo.py",
1048 |metric| {
1049 // 2 functions + 2 lambdas = 4
1050 insta::assert_json_snapshot!(
1051 metric.nexits,
1052 @r#"
1053 {
1054 "sum": 2,
1055 "average": 0.5,
1056 "min": 0,
1057 "max": 1
1058 }
1059 "#
1060 );
1061 },
1062 );
1063 }
1064
1065 #[cfg(feature = "java")]
1066 #[test]
1067 fn java_no_exit() {
1068 check_metrics::<JavaParser>("int a = 42;", "foo.java", |metric| {
1069 // 0 functions
1070 insta::assert_json_snapshot!(
1071 metric.nexits,
1072 @r#"
1073 {
1074 "sum": 0,
1075 "average": 0.0,
1076 "min": 0,
1077 "max": 0
1078 }
1079 "#
1080 );
1081 });
1082 }
1083
1084 #[cfg(feature = "java")]
1085 #[test]
1086 fn java_simple_function() {
1087 check_metrics::<JavaParser>(
1088 "class A {
1089 public int sum(int x, int y) {
1090 return x + y;
1091 }
1092 }",
1093 "foo.java",
1094 |metric| {
1095 // 1 exit / 1 space
1096 insta::assert_json_snapshot!(
1097 metric.nexits,
1098 @r#"
1099 {
1100 "sum": 1,
1101 "average": 1.0,
1102 "min": 0,
1103 "max": 1
1104 }
1105 "#
1106 );
1107 },
1108 );
1109 }
1110
1111 #[cfg(feature = "go")]
1112 #[test]
1113 fn go_no_return() {
1114 check_metrics::<GoParser>(
1115 "package main
1116 func f() {
1117 x := 1
1118 _ = x
1119 }",
1120 "foo.go",
1121 |metric| {
1122 // No return_statement → exit_sum = 0.
1123 insta::assert_json_snapshot!(
1124 metric.nexits,
1125 @r#"
1126 {
1127 "sum": 0,
1128 "average": 0.0,
1129 "min": 0,
1130 "max": 0
1131 }
1132 "#
1133 );
1134 },
1135 );
1136 }
1137
1138 #[cfg(feature = "go")]
1139 #[test]
1140 fn go_single_return() {
1141 check_metrics::<GoParser>(
1142 "package main
1143 func f() int {
1144 return 1
1145 }",
1146 "foo.go",
1147 |metric| {
1148 insta::assert_json_snapshot!(
1149 metric.nexits,
1150 @r#"
1151 {
1152 "sum": 1,
1153 "average": 1.0,
1154 "min": 0,
1155 "max": 1
1156 }
1157 "#
1158 );
1159 },
1160 );
1161 }
1162
1163 #[cfg(feature = "go")]
1164 #[test]
1165 fn go_multiple_returns() {
1166 check_metrics::<GoParser>(
1167 "package main
1168 func f(x int) int {
1169 if x > 0 {
1170 return 1
1171 }
1172 if x < 0 {
1173 return -1
1174 }
1175 return 0
1176 }",
1177 "foo.go",
1178 |metric| {
1179 // 3 distinct return_statements across branches.
1180 insta::assert_json_snapshot!(
1181 metric.nexits,
1182 @r#"
1183 {
1184 "sum": 3,
1185 "average": 3.0,
1186 "min": 0,
1187 "max": 3
1188 }
1189 "#
1190 );
1191 },
1192 );
1193 }
1194
1195 #[cfg(feature = "go")]
1196 #[test]
1197 fn go_naked_return() {
1198 check_metrics::<GoParser>(
1199 "package main
1200 func f() (x int) {
1201 x = 1
1202 return
1203 }",
1204 "foo.go",
1205 |metric| {
1206 // Bare `return` with named results is still a return_statement.
1207 insta::assert_json_snapshot!(
1208 metric.nexits,
1209 @r#"
1210 {
1211 "sum": 1,
1212 "average": 1.0,
1213 "min": 0,
1214 "max": 1
1215 }
1216 "#
1217 );
1218 },
1219 );
1220 }
1221
1222 #[cfg(feature = "go")]
1223 #[test]
1224 fn go_multivalue_return() {
1225 check_metrics::<GoParser>(
1226 "package main
1227 func f() (int, error) {
1228 return 0, nil
1229 }",
1230 "foo.go",
1231 |metric| {
1232 // `return a, b` is one return_statement (Go has no comma operator).
1233 insta::assert_json_snapshot!(
1234 metric.nexits,
1235 @r#"
1236 {
1237 "sum": 1,
1238 "average": 1.0,
1239 "min": 0,
1240 "max": 1
1241 }
1242 "#
1243 );
1244 },
1245 );
1246 }
1247
1248 #[cfg(feature = "go")]
1249 #[test]
1250 fn go_panic_counts_as_exit() {
1251 check_metrics::<GoParser>(
1252 "package main
1253 func f() {
1254 panic(\"boom\")
1255 }",
1256 "foo.go",
1257 |metric| {
1258 // panic(...) is the built-in abrupt-exit call, counted like
1259 // throw/raise — one exit even though there is no `return`.
1260 insta::assert_json_snapshot!(
1261 metric.nexits,
1262 @r#"
1263 {
1264 "sum": 1,
1265 "average": 1.0,
1266 "min": 0,
1267 "max": 1
1268 }
1269 "#
1270 );
1271 },
1272 );
1273 }
1274
1275 #[cfg(feature = "go")]
1276 #[test]
1277 fn go_panic_and_return_both_count() {
1278 check_metrics::<GoParser>(
1279 "package main
1280 func f(x int) int {
1281 if x < 0 {
1282 panic(\"negative\")
1283 }
1284 return x
1285 }",
1286 "foo.go",
1287 |metric| {
1288 // panic(...) + return are both abrupt exits → 2.
1289 insta::assert_json_snapshot!(
1290 metric.nexits,
1291 @r#"
1292 {
1293 "sum": 2,
1294 "average": 2.0,
1295 "min": 0,
1296 "max": 2
1297 }
1298 "#
1299 );
1300 },
1301 );
1302 }
1303
1304 #[cfg(feature = "go")]
1305 #[test]
1306 fn go_package_qualified_panic_is_not_exit() {
1307 check_metrics::<GoParser>(
1308 "package main
1309 func f() {
1310 foo.panic()
1311 }",
1312 "foo.go",
1313 |metric| {
1314 // `foo.panic()` is a user method on package `foo`, not the
1315 // built-in `panic` — its callee is a selector_expression, not
1316 // a bare identifier, so it must not be counted.
1317 insta::assert_json_snapshot!(
1318 metric.nexits,
1319 @r#"
1320 {
1321 "sum": 0,
1322 "average": 0.0,
1323 "min": 0,
1324 "max": 0
1325 }
1326 "#
1327 );
1328 },
1329 );
1330 }
1331
1332 #[cfg(feature = "java")]
1333 #[test]
1334 fn java_split_function() {
1335 check_metrics::<JavaParser>(
1336 "class A {
1337 public int multiply(int x, int y) {
1338 if(x == 0 || y == 0){
1339 return 0;
1340 }
1341 return x * y;
1342 }
1343 }",
1344 "foo.java",
1345 |metric| {
1346 // 2 exit / space 1
1347 insta::assert_json_snapshot!(
1348 metric.nexits,
1349 @r#"
1350 {
1351 "sum": 2,
1352 "average": 2.0,
1353 "min": 0,
1354 "max": 2
1355 }
1356 "#
1357 );
1358 },
1359 );
1360 }
1361
1362 #[cfg(feature = "csharp")]
1363 #[test]
1364 fn csharp_no_exit() {
1365 check_metrics::<CsharpParser>("int a = 42;", "foo.cs", |metric| {
1366 insta::assert_json_snapshot!(
1367 metric.nexits,
1368 @r#"
1369 {
1370 "sum": 0,
1371 "average": 0.0,
1372 "min": 0,
1373 "max": 0
1374 }
1375 "#
1376 );
1377 });
1378 }
1379
1380 #[cfg(feature = "csharp")]
1381 #[test]
1382 fn csharp_simple_function() {
1383 check_metrics::<CsharpParser>(
1384 "class A {
1385 public int Sum(int x, int y) {
1386 return x + y;
1387 }
1388 }",
1389 "foo.cs",
1390 |metric| {
1391 insta::assert_json_snapshot!(
1392 metric.nexits,
1393 @r#"
1394 {
1395 "sum": 1,
1396 "average": 1.0,
1397 "min": 0,
1398 "max": 1
1399 }
1400 "#
1401 );
1402 },
1403 );
1404 }
1405
1406 #[cfg(feature = "csharp")]
1407 #[test]
1408 fn csharp_split_function() {
1409 check_metrics::<CsharpParser>(
1410 "class A {
1411 public int Multiply(int x, int y) {
1412 if (x == 0 || y == 0) {
1413 return 0;
1414 }
1415 return x * y;
1416 }
1417 }",
1418 "foo.cs",
1419 |metric| {
1420 insta::assert_json_snapshot!(
1421 metric.nexits,
1422 @r#"
1423 {
1424 "sum": 2,
1425 "average": 2.0,
1426 "min": 0,
1427 "max": 2
1428 }
1429 "#
1430 );
1431 },
1432 );
1433 }
1434
1435 #[cfg(feature = "csharp")]
1436 #[test]
1437 fn csharp_yield_and_throw() {
1438 check_metrics::<CsharpParser>(
1439 "class A {
1440 public IEnumerable<int> Gen() {
1441 yield return 1;
1442 yield break;
1443 }
1444 public int Bad(int x) {
1445 if (x < 0) throw new System.Exception();
1446 return x;
1447 }
1448 }",
1449 "foo.cs",
1450 |metric| {
1451 // 2 yields + 1 throw + 1 return = 4 across two methods.
1452 insta::assert_json_snapshot!(
1453 metric.nexits,
1454 @r#"
1455 {
1456 "sum": 4,
1457 "average": 2.0,
1458 "min": 0,
1459 "max": 2
1460 }
1461 "#
1462 );
1463 },
1464 );
1465 }
1466
1467 #[cfg(feature = "perl")]
1468 #[test]
1469 fn perl_no_exit() {
1470 check_metrics::<PerlParser>(
1471 "sub f {
1472 print 'hi';
1473 }",
1474 "foo.pl",
1475 |metric| {
1476 insta::assert_json_snapshot!(
1477 metric.nexits,
1478 @r#"
1479 {
1480 "sum": 0,
1481 "average": 0.0,
1482 "min": 0,
1483 "max": 0
1484 }
1485 "#
1486 );
1487 },
1488 );
1489 }
1490
1491 #[cfg(feature = "perl")]
1492 #[test]
1493 fn perl_no_function_no_exit() {
1494 check_metrics::<PerlParser>("my $x = 1;\nprint $x;\n", "foo.pl", |metric| {
1495 insta::assert_json_snapshot!(metric.nexits, @r#"
1496 {
1497 "sum": 0,
1498 "average": 0.0,
1499 "min": 0,
1500 "max": 0
1501 }
1502 "#);
1503 });
1504 }
1505
1506 #[cfg(feature = "perl")]
1507 #[test]
1508 fn perl_multiple_returns() {
1509 check_metrics::<PerlParser>(
1510 "sub f {
1511 return 1 if $_[0];
1512 return 0;
1513 }",
1514 "foo.pl",
1515 |metric| {
1516 insta::assert_json_snapshot!(
1517 metric.nexits,
1518 @r#"
1519 {
1520 "sum": 2,
1521 "average": 2.0,
1522 "min": 0,
1523 "max": 2
1524 }
1525 "#
1526 );
1527 },
1528 );
1529 }
1530
1531 /// `die` and `exit` are Perl's abrupt-exit builtins (#1270). Every
1532 /// call form nests one `call_expression_with_bareword`, so the
1533 /// spaced-args (`die "m"`), bracketed (`exit(1)`) and bare
1534 /// (`die;`) spellings each count exactly once — a wrapper node is
1535 /// never counted alongside its bareword. The `CORE::`-qualified
1536 /// spelling of each is the same builtin reached past an override
1537 /// and keeps its qualifier in the bareword text, so it is matched
1538 /// by name alongside the bare one.
1539 #[cfg(feature = "perl")]
1540 #[test]
1541 fn perl_die_and_exit_are_exits() {
1542 check_metrics::<PerlParser>(
1543 "sub f {
1544 die \"bad\" if $_[0];
1545 open(my $fh, '<', $p) or die;
1546 exit(1) if $_[1];
1547 exit 2;
1548 CORE::die \"forced\" if $_[2];
1549 CORE::exit(3);
1550 return 0;
1551 }",
1552 "foo.pl",
1553 |metric| {
1554 // expected: 6 abrupt exits (two `die`, two `exit`, and
1555 // the `CORE::` spelling of each) plus one `return`.
1556 assert_eq!(metric.nexits.nexits_sum(), 7);
1557 },
1558 );
1559 }
1560
1561 /// Only the unqualified builtins count. A package-qualified callee
1562 /// keeps its qualifier in the bareword text (`Carp::croak`), the
1563 /// Carp helpers are library functions rather than builtins, and
1564 /// `$obj->die` parses as a `method_invocation` whose callee is a
1565 /// plain `identifier` — none of them is an exit.
1566 #[cfg(feature = "perl")]
1567 #[test]
1568 fn perl_lookalike_call_is_not_exit() {
1569 check_metrics::<PerlParser>(
1570 "sub f {
1571 $obj->die;
1572 $obj->exit(1);
1573 Carp::croak(\"x\");
1574 croak \"x\";
1575 my $s = \"die\";
1576 }",
1577 "foo.pl",
1578 |metric| {
1579 assert_eq!(metric.nexits.nexits_sum(), 0);
1580 },
1581 );
1582 }
1583
1584 #[cfg(feature = "typescript")]
1585 #[test]
1586 fn tsx_function_with_returns() {
1587 check_metrics::<TsxParser>(
1588 "function clamp(val: number, min: number, max: number) {
1589 if (val < min) {
1590 return min;
1591 }
1592 if (val > max) {
1593 return max;
1594 }
1595 return val;
1596 }",
1597 "foo.tsx",
1598 |metric| {
1599 insta::assert_json_snapshot!(
1600 metric.nexits,
1601 @r#"
1602 {
1603 "sum": 3,
1604 "average": 3.0,
1605 "min": 0,
1606 "max": 3
1607 }
1608 "#
1609 );
1610 },
1611 );
1612 }
1613
1614 #[cfg(feature = "typescript")]
1615 #[test]
1616 fn typescript_no_exit() {
1617 check_metrics::<TypescriptParser>("const x: number = 42;", "foo.ts", |metric| {
1618 insta::assert_json_snapshot!(
1619 metric.nexits,
1620 @r#"
1621 {
1622 "sum": 0,
1623 "average": 0.0,
1624 "min": 0,
1625 "max": 0
1626 }
1627 "#
1628 );
1629 });
1630 }
1631
1632 #[cfg(feature = "typescript")]
1633 #[test]
1634 fn typescript_function_with_returns() {
1635 check_metrics::<TypescriptParser>(
1636 "function safeDivide(a: number, b: number): number | null {
1637 if (b === 0) {
1638 return null;
1639 }
1640 return a / b;
1641 }",
1642 "foo.ts",
1643 |metric| {
1644 insta::assert_json_snapshot!(
1645 metric.nexits,
1646 @r#"
1647 {
1648 "sum": 2,
1649 "average": 2.0,
1650 "min": 0,
1651 "max": 2
1652 }
1653 "#
1654 );
1655 },
1656 );
1657 }
1658
1659 #[cfg(feature = "mozjs")]
1660 #[test]
1661 fn mozjs_no_exit() {
1662 check_metrics::<MozjsParser>("var a = 42;", "foo.js", |metric| {
1663 insta::assert_json_snapshot!(
1664 metric.nexits,
1665 @r#"
1666 {
1667 "sum": 0,
1668 "average": 0.0,
1669 "min": 0,
1670 "max": 0
1671 }
1672 "#
1673 );
1674 });
1675 }
1676
1677 #[cfg(feature = "mozjs")]
1678 #[test]
1679 fn mozjs_function_with_returns() {
1680 check_metrics::<MozjsParser>(
1681 "function f(a, b) {
1682 if (a) {
1683 return a;
1684 }
1685 return b;
1686 }",
1687 "foo.js",
1688 |metric| {
1689 insta::assert_json_snapshot!(
1690 metric.nexits,
1691 @r#"
1692 {
1693 "sum": 2,
1694 "average": 2.0,
1695 "min": 0,
1696 "max": 2
1697 }
1698 "#
1699 );
1700 },
1701 );
1702 }
1703
1704 #[cfg(feature = "kotlin")]
1705 #[test]
1706 fn kotlin_exit_return_and_throw() {
1707 check_metrics::<KotlinParser>(
1708 "fun divide(a: Int, b: Int): Int {
1709 if (b == 0) {
1710 throw IllegalArgumentException(\"zero\")
1711 }
1712 return a / b
1713 }",
1714 "foo.kt",
1715 |metric| {
1716 insta::assert_json_snapshot!(
1717 metric.nexits,
1718 @r#"
1719 {
1720 "sum": 2,
1721 "average": 2.0,
1722 "min": 0,
1723 "max": 2
1724 }
1725 "#
1726 );
1727 },
1728 );
1729 }
1730
1731 #[cfg(feature = "lua")]
1732 #[test]
1733 fn lua_no_exit() {
1734 check_metrics::<LuaParser>(
1735 "local function f(x)
1736 local y = x + 1
1737end",
1738 "foo.lua",
1739 |metric| {
1740 insta::assert_json_snapshot!(
1741 metric.nexits,
1742 @r#"
1743 {
1744 "sum": 0,
1745 "average": 0.0,
1746 "min": 0,
1747 "max": 0
1748 }
1749 "#
1750 );
1751 },
1752 );
1753 }
1754
1755 #[cfg(feature = "lua")]
1756 #[test]
1757 fn lua_return() {
1758 check_metrics::<LuaParser>(
1759 "local function f(x)
1760 if x > 0 then
1761 return x
1762 end
1763 return 0
1764end",
1765 "foo.lua",
1766 |metric| {
1767 insta::assert_json_snapshot!(
1768 metric.nexits,
1769 @r#"
1770 {
1771 "sum": 2,
1772 "average": 2.0,
1773 "min": 0,
1774 "max": 2
1775 }
1776 "#
1777 );
1778 },
1779 );
1780 }
1781
1782 #[cfg(feature = "lua")]
1783 #[test]
1784 fn lua_error_counts_as_exit() {
1785 check_metrics::<LuaParser>(
1786 "local function f(x)
1787 error(\"bad\")
1788end",
1789 "foo.lua",
1790 |metric| {
1791 // error(...) raises a Lua error that unwinds the stack — a
1792 // built-in abrupt exit, counted like throw/raise.
1793 insta::assert_json_snapshot!(
1794 metric.nexits,
1795 @r#"
1796 {
1797 "sum": 1,
1798 "average": 1.0,
1799 "min": 0,
1800 "max": 1
1801 }
1802 "#
1803 );
1804 },
1805 );
1806 }
1807
1808 #[cfg(feature = "lua")]
1809 #[test]
1810 fn lua_os_exit_counts_as_exit() {
1811 check_metrics::<LuaParser>(
1812 "local function f()
1813 os.exit(1)
1814end",
1815 "foo.lua",
1816 |metric| {
1817 // os.exit(...) terminates the process — its callee is a
1818 // dot_index_expression spelling `os.exit`, counted as an exit.
1819 insta::assert_json_snapshot!(
1820 metric.nexits,
1821 @r#"
1822 {
1823 "sum": 1,
1824 "average": 1.0,
1825 "min": 0,
1826 "max": 1
1827 }
1828 "#
1829 );
1830 },
1831 );
1832 }
1833
1834 #[cfg(feature = "lua")]
1835 #[test]
1836 fn lua_error_and_return_both_count() {
1837 check_metrics::<LuaParser>(
1838 "local function f(x)
1839 if x < 0 then
1840 error(\"negative\")
1841 end
1842 return x
1843end",
1844 "foo.lua",
1845 |metric| {
1846 // error(...) + return are both abrupt exits → 2.
1847 insta::assert_json_snapshot!(
1848 metric.nexits,
1849 @r#"
1850 {
1851 "sum": 2,
1852 "average": 2.0,
1853 "min": 0,
1854 "max": 2
1855 }
1856 "#
1857 );
1858 },
1859 );
1860 }
1861
1862 #[cfg(feature = "lua")]
1863 #[test]
1864 fn lua_user_call_is_not_exit() {
1865 check_metrics::<LuaParser>(
1866 "local function f()
1867 foo()
1868 myError(\"x\")
1869end",
1870 "foo.lua",
1871 |metric| {
1872 // Neither `foo()` nor a user `myError(...)` is the built-in
1873 // `error`/`os.exit`, so neither is counted.
1874 insta::assert_json_snapshot!(
1875 metric.nexits,
1876 @r#"
1877 {
1878 "sum": 0,
1879 "average": 0.0,
1880 "min": 0,
1881 "max": 0
1882 }
1883 "#
1884 );
1885 },
1886 );
1887 }
1888
1889 #[cfg(feature = "bash")]
1890 #[test]
1891 fn bash_no_exit() {
1892 check_metrics::<BashParser>("echo \"no exits\"", "foo.sh", |metric| {
1893 insta::assert_json_snapshot!(
1894 metric.nexits,
1895 @r#"
1896 {
1897 "sum": 0,
1898 "average": 0.0,
1899 "min": 0,
1900 "max": 0
1901 }
1902 "#
1903 );
1904 });
1905 }
1906
1907 #[cfg(feature = "bash")]
1908 #[test]
1909 fn bash_explicit_return() {
1910 check_metrics::<BashParser>(
1911 "f() {
1912 if [ -z \"$1\" ]; then
1913 return 1
1914 fi
1915 echo ok
1916 }",
1917 "foo.sh",
1918 |metric| {
1919 insta::assert_json_snapshot!(
1920 metric.nexits,
1921 @r#"
1922 {
1923 "sum": 1,
1924 "average": 1.0,
1925 "min": 0,
1926 "max": 1
1927 }
1928 "#
1929 );
1930 },
1931 );
1932 }
1933
1934 #[cfg(feature = "bash")]
1935 #[test]
1936 fn bash_explicit_exit() {
1937 check_metrics::<BashParser>(
1938 "f() {
1939 exit 0
1940 }",
1941 "foo.sh",
1942 |metric| {
1943 insta::assert_json_snapshot!(
1944 metric.nexits,
1945 @r#"
1946 {
1947 "sum": 1,
1948 "average": 1.0,
1949 "min": 0,
1950 "max": 1
1951 }
1952 "#
1953 );
1954 },
1955 );
1956 }
1957
1958 #[cfg(feature = "bash")]
1959 #[test]
1960 fn bash_multiple_exits() {
1961 check_metrics::<BashParser>(
1962 "f() {
1963 if [ \"$1\" = die ]; then
1964 exit 1
1965 fi
1966 return 0
1967 }",
1968 "foo.sh",
1969 |metric| {
1970 insta::assert_json_snapshot!(
1971 metric.nexits,
1972 @r#"
1973 {
1974 "sum": 2,
1975 "average": 2.0,
1976 "min": 0,
1977 "max": 2
1978 }
1979 "#
1980 );
1981 },
1982 );
1983 }
1984
1985 #[cfg(feature = "bash")]
1986 #[test]
1987 fn bash_returnish_names_are_not_exits() {
1988 // `returncode=1` is a `variable_assignment`, not a Command. The
1989 // function `returns` is invoked via a Command whose CommandName is
1990 // the literal "returns" — it must NOT be matched as a return/exit
1991 // builtin (whole-token match, no prefix collision).
1992 check_metrics::<BashParser>(
1993 "returncode=1
1994 returns() {
1995 echo named
1996 }
1997 returns",
1998 "foo.sh",
1999 |metric| {
2000 insta::assert_json_snapshot!(
2001 metric.nexits,
2002 @r#"
2003 {
2004 "sum": 0,
2005 "average": 0.0,
2006 "min": 0,
2007 "max": 0
2008 }
2009 "#
2010 );
2011 },
2012 );
2013 }
2014
2015 #[cfg(feature = "tcl")]
2016 #[test]
2017 fn tcl_no_exit() {
2018 check_metrics::<TclParser>(
2019 "proc f {x} {
2020 puts $x
2021}",
2022 "foo.tcl",
2023 |metric| {
2024 insta::assert_json_snapshot!(
2025 metric.nexits,
2026 @r#"
2027 {
2028 "sum": 0,
2029 "average": 0.0,
2030 "min": 0,
2031 "max": 0
2032 }
2033 "#
2034 );
2035 },
2036 );
2037 }
2038
2039 #[cfg(feature = "tcl")]
2040 #[test]
2041 fn tcl_return() {
2042 check_metrics::<TclParser>(
2043 "proc f {x} {
2044 return $x
2045}",
2046 "foo.tcl",
2047 |metric| {
2048 assert_eq!(metric.nexits.nexits_sum(), 1);
2049 assert_eq!(metric.nexits.nexits_max(), 1);
2050 insta::assert_json_snapshot!(metric.nexits);
2051 },
2052 );
2053 }
2054
2055 #[cfg(feature = "tcl")]
2056 #[test]
2057 fn tcl_multiple_returns() {
2058 check_metrics::<TclParser>(
2059 "proc f {x} {
2060 if {$x > 0} {
2061 return positive
2062 }
2063 return nonpositive
2064}",
2065 "foo.tcl",
2066 |metric| {
2067 assert_eq!(metric.nexits.nexits_sum(), 2);
2068 assert_eq!(metric.nexits.nexits_max(), 2);
2069 insta::assert_json_snapshot!(metric.nexits);
2070 },
2071 );
2072 }
2073
2074 /// Tcl's abrupt-exit builtins have no dedicated grammar rule:
2075 /// `error`, the 8.6 `throw`, and `exit` all parse as generic
2076 /// commands told apart by their leading word, the same seam
2077 /// `return` uses (#1270).
2078 #[cfg(feature = "tcl")]
2079 #[test]
2080 fn tcl_error_and_throw_are_exits() {
2081 check_metrics::<TclParser>(
2082 "proc f {x} {
2083 if {$x < 0} {
2084 error \"negative\"
2085 }
2086 if {$x == 0} {
2087 throw {ARITH DIVZERO} \"div by zero\"
2088 }
2089 if {$x > 100} {
2090 exit 1
2091 }
2092 return $x
2093}",
2094 "foo.tcl",
2095 |metric| {
2096 // expected: `error` + `throw` + `exit` + `return` = 4.
2097 assert_eq!(metric.nexits.nexits_sum(), 4);
2098 },
2099 );
2100 }
2101
2102 /// The command *name* is the seam, so the same words in argument
2103 /// position (`puts error`) or inside a string are not exits. The
2104 /// leading word of a nested braced command (`{ARITH DIVZERO}`) is
2105 /// likewise a different command name and contributes nothing.
2106 /// Every abrupt-exit builtin reached through the global namespace.
2107 /// `::return` *is* `return`, so all four must count; the leading word
2108 /// is the only seam these have, and an unstripped qualifier made each
2109 /// read as an ordinary call (#1381 review).
2110 #[cfg(feature = "tcl")]
2111 #[test]
2112 fn tcl_qualified_exits_are_exits() {
2113 check_metrics::<TclParser>(
2114 "proc f {x} {
2115 if {$x < 0} {
2116 ::error \"negative\"
2117 }
2118 if {$x == 0} {
2119 ::throw {ARITH DIVZERO} \"div by zero\"
2120 }
2121 if {$x > 100} {
2122 ::exit 1
2123 }
2124 ::return $x
2125}",
2126 "foo.tcl",
2127 |metric| {
2128 assert_eq!(metric.nexits.nexits_sum(), 4);
2129 assert_eq!(metric.nexits.nexits_max(), 4);
2130 },
2131 );
2132 }
2133
2134 /// Control for the test above: only the *leading* qualifier resolves
2135 /// to the core command, so a proc in `ns` is not an exit.
2136 #[cfg(feature = "tcl")]
2137 #[test]
2138 fn tcl_namespaced_return_is_not_an_exit() {
2139 check_metrics::<TclParser>(
2140 "proc f {x} {
2141 ns::return $x
2142}",
2143 "foo.tcl",
2144 |metric| {
2145 assert_eq!(metric.nexits.nexits_sum(), 0);
2146 },
2147 );
2148 }
2149
2150 #[cfg(feature = "tcl")]
2151 #[test]
2152 fn tcl_error_in_argument_position_is_not_exit() {
2153 check_metrics::<TclParser>(
2154 "proc f {x} {
2155 puts error
2156 puts throw
2157 set y \"error\"
2158}",
2159 "foo.tcl",
2160 |metric| {
2161 assert_eq!(metric.nexits.nexits_sum(), 0);
2162 },
2163 );
2164 }
2165
2166 #[cfg(feature = "typescript")]
2167 #[test]
2168 fn typescript_multiple_returns() {
2169 check_metrics::<TypescriptParser>(
2170 "function classify(n: number): string {
2171 if (n > 0) {
2172 return 'positive';
2173 } else if (n < 0) {
2174 return 'negative';
2175 }
2176 return 'zero';
2177 }",
2178 "foo.ts",
2179 |metric| {
2180 assert_eq!(metric.nexits.nexits_sum(), 3);
2181 assert_eq!(metric.nexits.nexits_max(), 3);
2182 insta::assert_json_snapshot!(metric.nexits);
2183 },
2184 );
2185 }
2186
2187 #[cfg(feature = "typescript")]
2188 #[test]
2189 fn typescript_nested_functions() {
2190 check_metrics::<TypescriptParser>(
2191 "function outer(): number {
2192 function inner(): number {
2193 return 42;
2194 }
2195 return inner();
2196 }",
2197 "foo.ts",
2198 |metric| {
2199 // outer has 1 return, inner has 1 return → sum=2, max=1
2200 assert_eq!(metric.nexits.nexits_sum(), 2);
2201 assert_eq!(metric.nexits.nexits_max(), 1);
2202 insta::assert_json_snapshot!(metric.nexits);
2203 },
2204 );
2205 }
2206
2207 #[cfg(feature = "typescript")]
2208 #[test]
2209 fn tsx_no_exit() {
2210 check_metrics::<TsxParser>(
2211 "function f(): void {
2212 console.log('hello');
2213 }",
2214 "foo.tsx",
2215 |metric| {
2216 assert_eq!(metric.nexits.nexits_sum(), 0);
2217 assert_eq!(metric.nexits.nexits_max(), 0);
2218 insta::assert_json_snapshot!(metric.nexits);
2219 },
2220 );
2221 }
2222
2223 #[cfg(feature = "typescript")]
2224 #[test]
2225 fn tsx_multiple_returns() {
2226 check_metrics::<TsxParser>(
2227 "function classify(n: number): string {
2228 if (n > 0) {
2229 return 'positive';
2230 } else if (n < 0) {
2231 return 'negative';
2232 }
2233 return 'zero';
2234 }",
2235 "foo.tsx",
2236 |metric| {
2237 assert_eq!(metric.nexits.nexits_sum(), 3);
2238 assert_eq!(metric.nexits.nexits_max(), 3);
2239 insta::assert_json_snapshot!(metric.nexits);
2240 },
2241 );
2242 }
2243
2244 #[cfg(feature = "kotlin")]
2245 #[test]
2246 fn kotlin_multiple_returns() {
2247 check_metrics::<KotlinParser>(
2248 "fun classify(n: Int): String {
2249 if (n > 0) {
2250 return \"positive\"
2251 } else if (n < 0) {
2252 return \"negative\"
2253 }
2254 return \"zero\"
2255 }",
2256 "foo.kt",
2257 |metric| {
2258 assert_eq!(metric.nexits.nexits_sum(), 3);
2259 assert_eq!(metric.nexits.nexits_max(), 3);
2260 insta::assert_json_snapshot!(metric.nexits);
2261 },
2262 );
2263 }
2264
2265 #[cfg(feature = "kotlin")]
2266 #[test]
2267 fn kotlin_no_exit() {
2268 check_metrics::<KotlinParser>(
2269 "fun f(): Unit {
2270 println(\"hello\")
2271 }",
2272 "foo.kt",
2273 |metric| {
2274 assert_eq!(metric.nexits.nexits_sum(), 0);
2275 assert_eq!(metric.nexits.nexits_max(), 0);
2276 insta::assert_json_snapshot!(metric.nexits);
2277 },
2278 );
2279 }
2280
2281 #[cfg(feature = "mozjs")]
2282 #[test]
2283 fn mozjs_nested_functions() {
2284 check_metrics::<MozjsParser>(
2285 "function outer() {
2286 function inner() {
2287 return 42;
2288 }
2289 return inner();
2290 }",
2291 "foo.js",
2292 |metric| {
2293 // outer has 1 return, inner has 1 return → sum=2, max=1
2294 assert_eq!(metric.nexits.nexits_sum(), 2);
2295 assert_eq!(metric.nexits.nexits_max(), 1);
2296 insta::assert_json_snapshot!(metric.nexits);
2297 },
2298 );
2299 }
2300
2301 #[cfg(feature = "php")]
2302 #[test]
2303 fn php_no_exit() {
2304 check_metrics::<PhpParser>("<?php $a = 42;", "foo.php", |metric| {
2305 insta::assert_json_snapshot!(
2306 metric.nexits,
2307 @r#"
2308 {
2309 "sum": 0,
2310 "average": 0.0,
2311 "min": 0,
2312 "max": 0
2313 }
2314 "#
2315 );
2316 });
2317 }
2318
2319 #[cfg(feature = "php")]
2320 #[test]
2321 fn php_yield_throw() {
2322 // Generator yields and a throw expression in statement position both
2323 // count as exits.
2324 check_metrics::<PhpParser>(
2325 "<?php
2326 function gen() {
2327 yield 1;
2328 yield 2;
2329 throw new \\Exception('x');
2330 }",
2331 "foo.php",
2332 |metric| {
2333 // 3 exits (2 yields + 1 throw) inside one function space.
2334 insta::assert_json_snapshot!(
2335 metric.nexits,
2336 @r#"
2337 {
2338 "sum": 3,
2339 "average": 3.0,
2340 "min": 0,
2341 "max": 3
2342 }
2343 "#
2344 );
2345 },
2346 );
2347 }
2348
2349 #[cfg(feature = "php")]
2350 #[test]
2351 fn php_exit_statement() {
2352 // `exit_statement` covers both `exit;` (bare) and `exit(N);` (with
2353 // optional argument). `die` is NOT in the `exit_statement` rule of
2354 // tree-sitter-php 0.24.2 — `die(...)` parses as a function call —
2355 // so we only count `exit` here.
2356 check_metrics::<PhpParser>(
2357 "<?php
2358 function bail(int $code): void {
2359 if ($code === 1) {
2360 exit(1);
2361 }
2362 exit;
2363 }",
2364 "foo.php",
2365 |metric| {
2366 // 2 exit_statements inside one function space.
2367 insta::assert_json_snapshot!(
2368 metric.nexits,
2369 @r#"
2370 {
2371 "sum": 2,
2372 "average": 2.0,
2373 "min": 0,
2374 "max": 2
2375 }
2376 "#
2377 );
2378 },
2379 );
2380 }
2381
2382 #[cfg(feature = "elixir")]
2383 #[test]
2384 fn elixir_no_exit() {
2385 // Plain function returning a value has no early-exit calls. The
2386 // `average` is `null` because Elixir's only function space is
2387 // the Unit; there is no per-function aggregation to average
2388 // over.
2389 check_metrics::<ElixirParser>(
2390 "defmodule Foo do\n def add(a, b) do\n a + b\n end\nend\n",
2391 "foo.ex",
2392 |metric| {
2393 assert_eq!(metric.nexits.nexits_sum(), 0);
2394 insta::assert_json_snapshot!(
2395 metric.nexits,
2396 @r#"
2397 {
2398 "sum": 0,
2399 "average": 0.0,
2400 "min": 0,
2401 "max": 0
2402 }
2403 "#
2404 );
2405 },
2406 );
2407 }
2408
2409 #[cfg(feature = "elixir")]
2410 #[test]
2411 fn elixir_raise_throw_exit() {
2412 // `raise`/`throw`/`exit` are recognised by inspecting the `target`
2413 // field text of `Call` nodes — there is no dedicated AST kind.
2414 check_metrics::<ElixirParser>(
2415 "defmodule Foo do\n def bad(x) do\n raise \"first\"\n throw(:second)\n exit(:third)\n end\nend\n",
2416 "foo.ex",
2417 |metric| {
2418 assert_eq!(metric.nexits.nexits_sum(), 3);
2419 insta::assert_json_snapshot!(
2420 metric.nexits,
2421 @r#"
2422 {
2423 "sum": 3,
2424 "average": 3.0,
2425 "min": 0,
2426 "max": 3
2427 }
2428 "#
2429 );
2430 },
2431 );
2432 }
2433
2434 #[cfg(feature = "elixir")]
2435 #[test]
2436 fn elixir_reraise_counts() {
2437 // `reraise` is the Elixir variant of `raise` that re-throws an
2438 // existing exception while preserving the stacktrace; we count
2439 // it as an exit alongside `raise`.
2440 check_metrics::<ElixirParser>(
2441 "defmodule Foo do\n def wrap(stack) do\n reraise(\"oops\", stack)\n end\nend\n",
2442 "foo.ex",
2443 |metric| {
2444 assert_eq!(metric.nexits.nexits_sum(), 1);
2445 },
2446 );
2447 }
2448
2449 #[cfg(feature = "elixir")]
2450 #[test]
2451 fn elixir_lookalike_call_is_not_exit() {
2452 // Only the exact identifiers `throw`/`raise`/`reraise`/`exit` are
2453 // exits; a user-defined `throw_event` or remote-call must NOT
2454 // count. This guards against future text-match regressions.
2455 check_metrics::<ElixirParser>(
2456 "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",
2457 "foo.ex",
2458 |metric| {
2459 assert_eq!(metric.nexits.nexits_sum(), 0);
2460 },
2461 );
2462 }
2463
2464 #[cfg(feature = "ruby")]
2465 #[test]
2466 fn ruby_no_exit() {
2467 // Function body without any `return` produces zero exits.
2468 check_metrics::<RubyParser>("def foo\n a = 1\n a + 1\nend\n", "foo.rb", |metric| {
2469 assert_eq!(metric.nexits.nexits_sum(), 0);
2470 });
2471 }
2472
2473 #[cfg(feature = "ruby")]
2474 #[test]
2475 fn ruby_multiple_returns() {
2476 // Four explicit `return` statements (no modifier sugar) — one
2477 // per branch. Anchors the headline sum.
2478 check_metrics::<RubyParser>(
2479 "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",
2480 "foo.rb",
2481 |metric| {
2482 assert_eq!(metric.nexits.nexits_sum(), 4);
2483 },
2484 );
2485 }
2486
2487 #[cfg(feature = "ruby")]
2488 #[test]
2489 fn ruby_explicit_returns() {
2490 // Each `return` (statement or modifier-wrapped) contributes one
2491 // exit. `yield` is intentionally NOT counted (it does not exit
2492 // the method).
2493 check_metrics::<RubyParser>(
2494 "def foo(x)\n return 0 if x.nil?\n yield x\n return x * 2\nend\n",
2495 "foo.rb",
2496 |metric| {
2497 assert_eq!(metric.nexits.nexits_sum(), 2);
2498 insta::assert_json_snapshot!(metric.nexits);
2499 },
2500 );
2501 }
2502
2503 /// `raise` and `exit` are receiver-less Kernel calls with no
2504 /// grammar node of their own, matched by callee text the way Go
2505 /// matches `panic` (#1270). Both the paren-less command form
2506 /// (`raise ArgumentError, "m"`) and the parenthesised form
2507 /// (`exit(1)`) parse as `call`, so both count.
2508 #[cfg(feature = "ruby")]
2509 #[test]
2510 fn ruby_raise_and_exit_are_exits() {
2511 check_metrics::<RubyParser>(
2512 "def f(x)\n raise ArgumentError, \"bad\" if x\n raise(RuntimeError)\n exit 1 if x\n exit(2)\n return 0\nend\n",
2513 "foo.rb",
2514 |metric| {
2515 // expected: two `raise` + two `exit` + one `return` = 5.
2516 assert_eq!(metric.nexits.nexits_sum(), 5);
2517 },
2518 );
2519 }
2520
2521 /// The explicit `Kernel.` receiver *is* the builtin — it is what code
2522 /// writes when a DSL has shadowed the bare name — and `exit!` is
2523 /// `Kernel#exit!`, the immediate process exit. Both count. A bare
2524 /// `exit!` counts too, unlike a bare `exit`: the `!` cannot name a
2525 /// local, so the grammar emits a `call` rather than an `identifier`.
2526 #[cfg(feature = "ruby")]
2527 #[test]
2528 fn ruby_kernel_qualified_and_bang_exits_count() {
2529 check_metrics::<RubyParser>(
2530 "def f(x)\n Kernel.raise ArgumentError, \"bad\" if x\n Kernel.exit(1) if x\n exit! if x\n exit!\nend\n",
2531 "foo.rb",
2532 |metric| {
2533 // expected: `Kernel.raise` + `Kernel.exit` + two `exit!` = 4.
2534 assert_eq!(metric.nexits.nexits_sum(), 4);
2535 },
2536 );
2537 }
2538
2539 /// A call with any receiver other than `Kernel` is a user method,
2540 /// never the builtin, so `obj.raise` / `self.exit` must not count —
2541 /// the same bare-callee gate Go uses to keep `foo.panic()` out. A
2542 /// symbol or hash key spelling the builtin parses as
2543 /// `simple_symbol` / `hash_key_symbol` and is likewise not a call.
2544 #[cfg(feature = "ruby")]
2545 #[test]
2546 fn ruby_receiver_call_is_not_exit() {
2547 check_metrics::<RubyParser>(
2548 "def f(x)\n obj.raise(x)\n self.exit(1)\n logger.raise\n h = { raise: 1 }\n s = :exit\nend\n",
2549 "foo.rb",
2550 |metric| {
2551 assert_eq!(metric.nexits.nexits_sum(), 0);
2552 },
2553 );
2554 }
2555
2556 /// A *bare* `raise` / `exit` — the argument-less re-raise idiom —
2557 /// parses as a plain `identifier`, indistinguishable from reading a
2558 /// local variable of that name, so it is deliberately not counted.
2559 /// Pinning the exclusion here keeps a future "just match bare
2560 /// identifiers too" change from silently counting every variable
2561 /// read.
2562 #[cfg(feature = "ruby")]
2563 #[test]
2564 fn ruby_bare_raise_identifier_is_not_exit() {
2565 check_metrics::<RubyParser>(
2566 "def f(x)\n begin\n g(x)\n rescue StandardError\n raise\n end\n return 0\nend\n",
2567 "foo.rb",
2568 |metric| {
2569 // expected: only the `return`; the bare `raise` is an
2570 // `identifier`, not a `call`.
2571 assert_eq!(metric.nexits.nexits_sum(), 1);
2572 },
2573 );
2574 }
2575
2576 #[cfg(feature = "python")]
2577 #[test]
2578 fn python_return_and_raise() {
2579 // `raise` exits the function (stack unwinds)
2580 // just like `return`. Mirrors the C# / Kotlin / PHP / Elixir
2581 // behaviour. One `raise` + one `return` => 2 exits.
2582 check_metrics::<PythonParser>(
2583 "def parse(s):
2584 if not s:
2585 raise ValueError(\"empty\")
2586 return int(s)",
2587 "foo.py",
2588 |metric| {
2589 assert_eq!(metric.nexits.nexits_sum(), 2);
2590 insta::assert_json_snapshot!(
2591 metric.nexits,
2592 @r#"
2593 {
2594 "sum": 2,
2595 "average": 2.0,
2596 "min": 0,
2597 "max": 2
2598 }
2599 "#
2600 );
2601 },
2602 );
2603 }
2604
2605 #[cfg(feature = "javascript")]
2606 #[test]
2607 fn javascript_return_and_throw() {
2608 // `throw` is a function exit.
2609 check_metrics::<JavascriptParser>(
2610 "function parseLength(s) {
2611 if (s === null) throw new Error('null');
2612 return s.length;
2613 }",
2614 "foo.js",
2615 |metric| {
2616 assert_eq!(metric.nexits.nexits_sum(), 2);
2617 insta::assert_json_snapshot!(
2618 metric.nexits,
2619 @r#"
2620 {
2621 "sum": 2,
2622 "average": 2.0,
2623 "min": 0,
2624 "max": 2
2625 }
2626 "#
2627 );
2628 },
2629 );
2630 }
2631
2632 #[cfg(feature = "mozjs")]
2633 #[test]
2634 fn mozjs_return_and_throw() {
2635 // Same shape as plain JavaScript.
2636 check_metrics::<MozjsParser>(
2637 "function parseLength(s) {
2638 if (s === null) throw new Error('null');
2639 return s.length;
2640 }",
2641 "foo.js",
2642 |metric| {
2643 assert_eq!(metric.nexits.nexits_sum(), 2);
2644 insta::assert_json_snapshot!(
2645 metric.nexits,
2646 @r#"
2647 {
2648 "sum": 2,
2649 "average": 2.0,
2650 "min": 0,
2651 "max": 2
2652 }
2653 "#
2654 );
2655 },
2656 );
2657 }
2658
2659 #[cfg(feature = "typescript")]
2660 #[test]
2661 fn typescript_return_and_throw() {
2662 check_metrics::<TypescriptParser>(
2663 "function parseLength(s: string | null): number {
2664 if (s === null) throw new Error('null');
2665 return s.length;
2666 }",
2667 "foo.ts",
2668 |metric| {
2669 assert_eq!(metric.nexits.nexits_sum(), 2);
2670 insta::assert_json_snapshot!(
2671 metric.nexits,
2672 @r#"
2673 {
2674 "sum": 2,
2675 "average": 2.0,
2676 "min": 0,
2677 "max": 2
2678 }
2679 "#
2680 );
2681 },
2682 );
2683 }
2684
2685 #[cfg(feature = "typescript")]
2686 #[test]
2687 fn tsx_return_and_throw() {
2688 check_metrics::<TsxParser>(
2689 "function parseLength(s: string | null): number {
2690 if (s === null) throw new Error('null');
2691 return s.length;
2692 }",
2693 "foo.tsx",
2694 |metric| {
2695 assert_eq!(metric.nexits.nexits_sum(), 2);
2696 insta::assert_json_snapshot!(
2697 metric.nexits,
2698 @r#"
2699 {
2700 "sum": 2,
2701 "average": 2.0,
2702 "min": 0,
2703 "max": 2
2704 }
2705 "#
2706 );
2707 },
2708 );
2709 }
2710
2711 #[cfg(feature = "java")]
2712 #[test]
2713 fn java_return_and_throw() {
2714 // `throw` exits the method.
2715 check_metrics::<JavaParser>(
2716 "class A {
2717 int parseLength(String s) {
2718 if (s == null) throw new NullPointerException();
2719 return s.length();
2720 }
2721 }",
2722 "foo.java",
2723 |metric| {
2724 assert_eq!(metric.nexits.nexits_sum(), 2);
2725 insta::assert_json_snapshot!(
2726 metric.nexits,
2727 @r#"
2728 {
2729 "sum": 2,
2730 "average": 2.0,
2731 "min": 0,
2732 "max": 2
2733 }
2734 "#
2735 );
2736 },
2737 );
2738 }
2739
2740 /// #1160 is an *attribution* bug, and `nexits` is where that shows
2741 /// most plainly: the file-level sum never moved, so only a per-space
2742 /// assertion can see it. The compact constructor's `throw` belonged
2743 /// to the enclosing `class R` because the constructor opened no space
2744 /// of its own.
2745 ///
2746 /// Both halves are asserted. `class R`'s own count must be 0 — the
2747 /// aggregate `nexits_sum` is 2 either way, so checking only the new
2748 /// space would pass against the unfixed code as long as the space
2749 /// existed at all.
2750 #[cfg(feature = "java")]
2751 #[test]
2752 fn java_record_compact_constructor_owns_its_exits() {
2753 check_func_space::<JavaParser, _>(
2754 "record R(int a, int b) {
2755 R {
2756 if (a < 0) { throw new IllegalArgumentException(); }
2757 }
2758 int sum() { return a + b; }
2759 }",
2760 "R.java",
2761 |space| {
2762 assert_eq!(
2763 space.metrics.nexits.nexits_sum(),
2764 2,
2765 "one throw, one return"
2766 );
2767 assert_eq!(
2768 child_space(&space, "R").metrics.nexits.nexits(),
2769 0,
2770 "class R owns neither",
2771 );
2772 assert_eq!(
2773 function_space(&space, "R").metrics.nexits.nexits(),
2774 1,
2775 "the compact constructor owns its throw",
2776 );
2777 },
2778 );
2779 }
2780
2781 #[cfg(feature = "java")]
2782 #[test]
2783 fn java_yield_in_switch_expression() {
2784 // Java-14+ switch-expression `yield` is an explicit exit. Each
2785 // `yield` counts as one, alongside the enclosing `return`.
2786 check_metrics::<JavaParser>(
2787 "class A {
2788 int describe(int n) {
2789 return switch (n) {
2790 case 0: yield 100;
2791 default: yield 200;
2792 };
2793 }
2794 }",
2795 "foo.java",
2796 |metric| {
2797 assert_eq!(metric.nexits.nexits_sum(), 3);
2798 },
2799 );
2800 }
2801
2802 #[cfg(feature = "groovy")]
2803 #[test]
2804 fn groovy_no_exit() {
2805 // No functions at all — `nexits.sum` is 0.
2806 check_metrics::<GroovyParser>("int a = 42", "foo.groovy", |metric| {
2807 assert_eq!(metric.nexits.nexits_sum(), 0);
2808 });
2809 }
2810
2811 #[cfg(feature = "groovy")]
2812 #[test]
2813 fn groovy_simple_function() {
2814 // One explicit return in a top-level function.
2815 check_metrics::<GroovyParser>(
2816 "int answer() {
2817 return 42
2818 }",
2819 "foo.groovy",
2820 |metric| {
2821 assert_eq!(metric.nexits.nexits_sum(), 1);
2822 },
2823 );
2824 }
2825
2826 #[cfg(feature = "groovy")]
2827 #[test]
2828 fn groovy_return_and_throw() {
2829 check_metrics::<GroovyParser>(
2830 "class A {
2831 int parseLength(String s) {
2832 if (s == null) throw new NullPointerException()
2833 return s.length()
2834 }
2835 }",
2836 "foo.groovy",
2837 |metric| {
2838 assert_eq!(metric.nexits.nexits_sum(), 2);
2839 },
2840 );
2841 }
2842
2843 #[cfg(feature = "groovy")]
2844 #[test]
2845 fn groovy_yield_in_switch_expression() {
2846 // Groovy inherits Java-14+ switch-expression `yield`. Each
2847 // explicit `yield` counts as one exit.
2848 check_metrics::<GroovyParser>(
2849 "class A {
2850 int describe(int n) {
2851 return switch (n) {
2852 case 0: yield 100;
2853 default: yield 200;
2854 }
2855 }
2856 }",
2857 "foo.groovy",
2858 |metric| {
2859 assert_eq!(metric.nexits.nexits_sum(), 3);
2860 },
2861 );
2862 }
2863
2864 #[cfg(feature = "groovy")]
2865 #[test]
2866 fn groovy_implicit_return_not_counted() {
2867 // Groovy allows implicit return of the last expression in a
2868 // closure / function body. The Exit metric only counts
2869 // *explicit* `return` / `yield` / `throw` — consistent with
2870 // Java's docstring.
2871 check_metrics::<GroovyParser>("int identity(int x) { x }", "foo.groovy", |metric| {
2872 assert_eq!(metric.nexits.nexits_sum(), 0);
2873 });
2874 }
2875
2876 #[cfg(feature = "cpp")]
2877 #[test]
2878 fn cpp_return_and_throw() {
2879 // `throw` exits the function.
2880 check_metrics::<CppParser>(
2881 "int parseLength(const char* s) {
2882 if (s == nullptr) throw std::invalid_argument(\"null\");
2883 return 0;
2884 }",
2885 "foo.cpp",
2886 |metric| {
2887 assert_eq!(metric.nexits.nexits_sum(), 2);
2888 insta::assert_json_snapshot!(
2889 metric.nexits,
2890 @r#"
2891 {
2892 "sum": 2,
2893 "average": 2.0,
2894 "min": 0,
2895 "max": 2
2896 }
2897 "#
2898 );
2899 },
2900 );
2901 }
2902
2903 #[cfg(feature = "python")]
2904 #[test]
2905 fn python_yield_counts_as_exit() {
2906 // Generator suspension via `yield` hands control back to the
2907 // caller — the function does leave its frame, just resumably.
2908 // Mirrors the long-standing C# / PHP behaviour. Two yields plus
2909 // one return == 3 exits inside the one generator function.
2910 check_metrics::<PythonParser>(
2911 "def gen():
2912 yield 1
2913 yield 2
2914 return",
2915 "foo.py",
2916 |metric| {
2917 assert_eq!(metric.nexits.nexits_sum(), 3);
2918 insta::assert_json_snapshot!(
2919 metric.nexits,
2920 @r#"
2921 {
2922 "sum": 3,
2923 "average": 3.0,
2924 "min": 0,
2925 "max": 3
2926 }
2927 "#
2928 );
2929 },
2930 );
2931 }
2932
2933 #[cfg(feature = "javascript")]
2934 #[test]
2935 fn javascript_yield_counts_as_exit() {
2936 // `function*` generator: each `yield` is an exit edge, same as
2937 // Python/C#/PHP. Two yields + one return == 3.
2938 check_metrics::<JavascriptParser>(
2939 "function* gen() {
2940 yield 1;
2941 yield 2;
2942 return;
2943 }",
2944 "foo.js",
2945 |metric| {
2946 assert_eq!(metric.nexits.nexits_sum(), 3);
2947 insta::assert_json_snapshot!(
2948 metric.nexits,
2949 @r#"
2950 {
2951 "sum": 3,
2952 "average": 3.0,
2953 "min": 0,
2954 "max": 3
2955 }
2956 "#
2957 );
2958 },
2959 );
2960 }
2961
2962 #[cfg(feature = "mozjs")]
2963 #[test]
2964 fn mozjs_yield_counts_as_exit() {
2965 // Same shape as plain JavaScript.
2966 check_metrics::<MozjsParser>(
2967 "function* gen() {
2968 yield 1;
2969 yield 2;
2970 return;
2971 }",
2972 "foo.js",
2973 |metric| {
2974 assert_eq!(metric.nexits.nexits_sum(), 3);
2975 insta::assert_json_snapshot!(
2976 metric.nexits,
2977 @r#"
2978 {
2979 "sum": 3,
2980 "average": 3.0,
2981 "min": 0,
2982 "max": 3
2983 }
2984 "#
2985 );
2986 },
2987 );
2988 }
2989
2990 #[cfg(feature = "typescript")]
2991 #[test]
2992 fn typescript_yield_counts_as_exit() {
2993 check_metrics::<TypescriptParser>(
2994 "function* gen(): Generator<number> {
2995 yield 1;
2996 yield 2;
2997 return;
2998 }",
2999 "foo.ts",
3000 |metric| {
3001 assert_eq!(metric.nexits.nexits_sum(), 3);
3002 insta::assert_json_snapshot!(
3003 metric.nexits,
3004 @r#"
3005 {
3006 "sum": 3,
3007 "average": 3.0,
3008 "min": 0,
3009 "max": 3
3010 }
3011 "#
3012 );
3013 },
3014 );
3015 }
3016
3017 #[cfg(feature = "typescript")]
3018 #[test]
3019 fn tsx_yield_counts_as_exit() {
3020 check_metrics::<TsxParser>(
3021 "function* gen(): Generator<number> {
3022 yield 1;
3023 yield 2;
3024 return;
3025 }",
3026 "foo.tsx",
3027 |metric| {
3028 assert_eq!(metric.nexits.nexits_sum(), 3);
3029 insta::assert_json_snapshot!(
3030 metric.nexits,
3031 @r#"
3032 {
3033 "sum": 3,
3034 "average": 3.0,
3035 "min": 0,
3036 "max": 3
3037 }
3038 "#
3039 );
3040 },
3041 );
3042 }
3043
3044 #[cfg(feature = "python")]
3045 #[test]
3046 fn python_yield_forms_count_as_exit() {
3047 // tree-sitter-python emits a single `Python::Yield` node kind for
3048 // every yield form: bare `yield`, `yield value`, and `yield from
3049 // iter`. The match arm therefore covers all three with no extra
3050 // variants needed. Three yield forms == 3 exits.
3051 check_metrics::<PythonParser>(
3052 "def gen():
3053 yield
3054 yield 1
3055 yield from range(3)",
3056 "foo.py",
3057 |metric| {
3058 assert_eq!(metric.nexits.nexits_sum(), 3);
3059 insta::assert_json_snapshot!(
3060 metric.nexits,
3061 @r#"
3062 {
3063 "sum": 3,
3064 "average": 3.0,
3065 "min": 0,
3066 "max": 3
3067 }
3068 "#
3069 );
3070 },
3071 );
3072 }
3073
3074 #[cfg(feature = "javascript")]
3075 #[test]
3076 fn javascript_yield_delegate_counts_as_exit() {
3077 // Delegating yield (`yield*`) parses as the same
3078 // `Javascript::YieldExpression` node as plain `yield`, so the
3079 // existing match arm covers it. Two regular yields + one
3080 // delegate == 3 exits.
3081 check_metrics::<JavascriptParser>(
3082 "function* gen() {
3083 yield 1;
3084 yield* other();
3085 yield 2;
3086 }",
3087 "foo.js",
3088 |metric| {
3089 assert_eq!(metric.nexits.nexits_sum(), 3);
3090 insta::assert_json_snapshot!(
3091 metric.nexits,
3092 @r#"
3093 {
3094 "sum": 3,
3095 "average": 3.0,
3096 "min": 0,
3097 "max": 3
3098 }
3099 "#
3100 );
3101 },
3102 );
3103 }
3104
3105 #[cfg(feature = "mozjs")]
3106 #[test]
3107 fn mozjs_yield_delegate_counts_as_exit() {
3108 check_metrics::<MozjsParser>(
3109 "function* gen() {
3110 yield 1;
3111 yield* other();
3112 yield 2;
3113 }",
3114 "foo.js",
3115 |metric| {
3116 assert_eq!(metric.nexits.nexits_sum(), 3);
3117 insta::assert_json_snapshot!(
3118 metric.nexits,
3119 @r#"
3120 {
3121 "sum": 3,
3122 "average": 3.0,
3123 "min": 0,
3124 "max": 3
3125 }
3126 "#
3127 );
3128 },
3129 );
3130 }
3131
3132 #[cfg(feature = "typescript")]
3133 #[test]
3134 fn typescript_yield_delegate_counts_as_exit() {
3135 check_metrics::<TypescriptParser>(
3136 "function* gen(): Generator<number> {
3137 yield 1;
3138 yield* other();
3139 yield 2;
3140 }",
3141 "foo.ts",
3142 |metric| {
3143 assert_eq!(metric.nexits.nexits_sum(), 3);
3144 insta::assert_json_snapshot!(
3145 metric.nexits,
3146 @r#"
3147 {
3148 "sum": 3,
3149 "average": 3.0,
3150 "min": 0,
3151 "max": 3
3152 }
3153 "#
3154 );
3155 },
3156 );
3157 }
3158
3159 #[cfg(feature = "typescript")]
3160 #[test]
3161 fn tsx_yield_delegate_counts_as_exit() {
3162 check_metrics::<TsxParser>(
3163 "function* gen(): Generator<number> {
3164 yield 1;
3165 yield* other();
3166 yield 2;
3167 }",
3168 "foo.tsx",
3169 |metric| {
3170 assert_eq!(metric.nexits.nexits_sum(), 3);
3171 insta::assert_json_snapshot!(
3172 metric.nexits,
3173 @r#"
3174 {
3175 "sum": 3,
3176 "average": 3.0,
3177 "min": 0,
3178 "max": 3
3179 }
3180 "#
3181 );
3182 },
3183 );
3184 }
3185
3186 /// A handler with no `return` has zero exits (iRules has no `return`
3187 /// keyword node; `return` is a generic command matched by name).
3188 #[cfg(feature = "irules")]
3189 #[test]
3190 fn irules_no_exit() {
3191 check_metrics::<IrulesParser>(
3192 "when HTTP_REQUEST {
3193 set x 1
3194 log local0. $x
3195}
3196",
3197 "foo.irule",
3198 |metric| {
3199 assert_eq!(metric.nexits.nexits_sum(), 0);
3200 },
3201 );
3202 }
3203
3204 /// A `return` command contributes one exit.
3205 #[cfg(feature = "irules")]
3206 #[test]
3207 fn irules_return() {
3208 check_metrics::<IrulesParser>(
3209 "when HTTP_REQUEST {
3210 if { [HTTP::uri] eq \"/\" } {
3211 return
3212 }
3213 log local0. \"served\"
3214}
3215",
3216 "foo.irule",
3217 |metric| {
3218 assert_eq!(metric.nexits.nexits_sum(), 1);
3219 },
3220 );
3221 }
3222
3223 /// A multi-value `return` (`return [list ...]`) is a single command and
3224 /// counts once, not once per returned value.
3225 #[cfg(feature = "irules")]
3226 #[test]
3227 fn irules_multi_value_return_counts_once() {
3228 check_metrics::<IrulesParser>(
3229 "proc pair { a b } {
3230 return [list $a $b]
3231}
3232",
3233 "foo.irule",
3234 |metric| {
3235 assert_eq!(metric.nexits.nexits_sum(), 1);
3236 },
3237 );
3238 }
3239
3240 /// `error` is a plain Tcl builtin that reaches iRules unchanged and
3241 /// parses to the same `command` + name-word shape (#1270),
3242 /// re-derived against the iRules grammar rather than assumed from
3243 /// Tcl's.
3244 #[cfg(feature = "irules")]
3245 #[test]
3246 fn irules_error_is_an_exit() {
3247 check_metrics::<IrulesParser>(
3248 "proc f { x } {
3249 if { $x < 0 } {
3250 error \"negative\"
3251 }
3252 return $x
3253}
3254",
3255 "foo.irule",
3256 |metric| {
3257 // expected: `error` + `return` = 2.
3258 assert_eq!(metric.nexits.nexits_sum(), 2);
3259 },
3260 );
3261 }
3262
3263 /// Tcl 8.6's `throw` is deliberately absent from the iRules exit
3264 /// set — TMOS runs a Tcl 8.4-derived interpreter with no such
3265 /// builtin, so the word can only ever name a user proc — and
3266 /// `error` in argument position is not an exit either.
3267 /// iRules resolves the exit name in its own walker rather than
3268 /// through `tcl_command_name`, so the `::` strip needs pinning on
3269 /// that second path too — the sibling sweep grammar-dispatch.md
3270 /// requires (#1381 review).
3271 #[cfg(feature = "irules")]
3272 #[test]
3273 fn irules_qualified_exits_are_exits() {
3274 check_metrics::<IrulesParser>(
3275 "proc f { x } {
3276 if { $x < 0 } {
3277 ::error \"negative\"
3278 }
3279 ::return $x
3280}
3281",
3282 "foo.irule",
3283 |metric| {
3284 assert_eq!(metric.nexits.nexits_sum(), 2);
3285 },
3286 );
3287 }
3288
3289 /// Control: `ns::return` is a proc in `ns`, not the core command.
3290 #[cfg(feature = "irules")]
3291 #[test]
3292 fn irules_namespaced_return_is_not_an_exit() {
3293 check_metrics::<IrulesParser>(
3294 "proc f { x } {
3295 ns::return $x
3296}
3297",
3298 "foo.irule",
3299 |metric| {
3300 assert_eq!(metric.nexits.nexits_sum(), 0);
3301 },
3302 );
3303 }
3304
3305 #[cfg(feature = "irules")]
3306 #[test]
3307 fn irules_throw_and_argument_position_error_are_not_exits() {
3308 check_metrics::<IrulesParser>(
3309 "proc f { x } {
3310 throw {ARITH DIVZERO} \"boom\"
3311 log local0. error
3312}
3313",
3314 "foo.irule",
3315 |metric| {
3316 assert_eq!(metric.nexits.nexits_sum(), 0);
3317 },
3318 );
3319 }
3320
3321 /// Objective-C method with no `return` and no `@throw` has zero exit
3322 /// points.
3323 #[cfg(feature = "objc")]
3324 #[test]
3325 fn objc_no_exit() {
3326 check_metrics::<ObjcParser>(
3327 "@implementation Foo
3328- (void)bar {
3329 [self doWork];
3330}
3331@end
3332",
3333 "foo.m",
3334 |metric| {
3335 assert_eq!(metric.nexits.nexits_sum(), 0);
3336 insta::assert_json_snapshot!(metric.nexits, @r#"
3337 {
3338 "sum": 0,
3339 "average": 0.0,
3340 "min": 0,
3341 "max": 0
3342 }
3343 "#);
3344 },
3345 );
3346 }
3347
3348 /// Objective-C exit set is `return_statement` + `@throw`
3349 /// (`throw_statement`): a method with one of each counts 2.
3350 #[cfg(feature = "objc")]
3351 #[test]
3352 fn objc_return_and_throw() {
3353 check_metrics::<ObjcParser>(
3354 "@implementation Foo
3355- (int)bar:(int)x {
3356 if (x < 0) {
3357 @throw [NSException exceptionWithName:@\"e\" reason:@\"r\" userInfo:nil];
3358 }
3359 return x;
3360}
3361@end
3362",
3363 "foo.m",
3364 |metric| {
3365 assert_eq!(metric.nexits.nexits_sum(), 2);
3366 insta::assert_json_snapshot!(metric.nexits, @r#"
3367 {
3368 "sum": 2,
3369 "average": 2.0,
3370 "min": 0,
3371 "max": 2
3372 }
3373 "#);
3374 },
3375 );
3376 }
3377}