Skip to main content

big_code_analysis/metrics/
loc.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(
8    clippy::enum_glob_use,
9    clippy::match_same_arms,
10    clippy::struct_field_names,
11    clippy::wildcard_imports
12)]
13// Metric counts (token, function, branch, argument, etc.) are stored as
14// `usize` and crossed with `f64` averages, ratios, and Halstead scores
15// across the cyclomatic / MI / Halstead computations. The `usize as f64`
16// and `f64 as usize` casts are intentional and snapshot-anchored — every
17// site is bounded by the count it came from. Allowing the lints at the
18// module level keeps the metric arithmetic legible.
19#![allow(
20    clippy::cast_precision_loss,
21    clippy::cast_possible_truncation,
22    clippy::cast_sign_loss
23)]
24// `Loc` is the one metric that computes on tree-sitter *span
25// coordinates* rather than on its own accumulators, and a row index
26// arriving from a parse is attacker-controlled through the source
27// layout. #1051 was a `usize` underflow of exactly this shape — a Rust
28// doc comment at EOF drove `end - 1` below zero, panicking in debug and
29// wrapping to `usize::MAX` in release, from an input as small as
30// `/// x`. Warning here forces every span adjustment to be explicitly
31// saturating, checked, or annotated, rather than relying on a bound
32// that holds only until a grammar changes shape.
33//
34// Deliberately scoped to `loc` rather than to `src/metrics/`: the other
35// metrics contribute 244 hits, all of them `+=` on their own counters,
36// which is not this bug class and would bury it (#1152).
37#![warn(clippy::arithmetic_side_effects)]
38
39use crate::checker::Checker;
40use crate::metrics::npa::python_is_block;
41use std::fmt;
42
43use crate::macros::implement_metric_trait;
44use crate::*;
45
46// Collapse the `usize::MAX` sentinel that `*_min` fields are
47// initialised to on `Default` into `0`, so a never-observed space
48// serializes to a meaningful number rather than `18446744073709551615`.
49// Mirrors `tokens::Stats::tokens_min`'s guard.
50#[inline]
51fn min_or_zero(v: usize) -> u64 {
52    if v == usize::MAX { 0 } else { v as u64 }
53}
54
55/// Number of physical source rows covered by a span running from the
56/// 0-based `start_row` to the 1-based inclusive `end_line`.
57///
58/// The subtlety is not here — it is in which row counts as the last
59/// one, and that rule lives once, in [`Node::end_line`]: a span whose
60/// end position sits at column 0 stops *before* that row contributes a
61/// single character, so the row belongs to whatever follows. Both
62/// callers take `end_line` straight from the node, so this is plain
63/// subtraction rather than a second copy of the rule.
64///
65/// Keying on the end column rather than on "is this the unit?" is what
66/// fixed issue #1067 here and #1163 in `Node`: the unit/non-unit split
67/// assumed the unit always ends at column 0 (false for un-newline-
68/// terminated input, which then lost a row) and that nothing else ever
69/// does (false for Perl, whose last `sub` gained one).
70///
71/// Requires `end_line >= start_row`, which tree-sitter guarantees for a
72/// single node's own span. The `debug_assert` pins that in tests; the
73/// `saturating_sub` decides what release does if it is ever violated
74/// anyway. Zero is the right answer there — an inverted span covers no
75/// rows. Note what this does *not* buy: `sloc()` already clamps with
76/// `saturating_sub`, so a wrapped value could not have escaped as
77/// `usize::MAX` either. It would have escaped as `sloc: 0` for a
78/// non-empty file, and on into MI's SLOC term — a wrong number surfacing
79/// far from its cause, which is how #1051 was reported.
80#[inline]
81fn span_rows(start_row: usize, end_line: usize) -> usize {
82    debug_assert!(
83        end_line >= start_row,
84        "span_rows: end_line {end_line} < start_row {start_row}"
85    );
86    end_line.saturating_sub(start_row)
87}
88
89mod line_set;
90use line_set::LineSet;
91
92/// The `SLoc` metric suite.
93#[derive(Debug, Clone, PartialEq)]
94pub struct Sloc {
95    start: usize,
96    // 1-based inclusive last line of the span, from `Node::end_line`.
97    // Storing the resolved line rather than the raw end row plus its
98    // column keeps the "does the final row count" rule in one place.
99    end_line: usize,
100    // Physical lines removed from this space's span by `exclude_tests`
101    // pruning. `sloc` is the lone loc sub-metric computed by span
102    // subtraction rather than node-by-node accumulation, so a pruned
103    // subtree (which a `continue` in the walk suppresses for every
104    // accumulated metric) leaves the span untouched. We accumulate the
105    // inclusive row count of each pruned subtree here and subtract it
106    // in `sloc()` so SLOC drops in step with `ploc`/`cloc`/`lloc`
107    // (issue #722).
108    excluded_lines: usize,
109    sloc_min: usize,
110    sloc_max: usize,
111}
112
113impl Default for Sloc {
114    fn default() -> Self {
115        Self {
116            start: 0,
117            end_line: 0,
118            excluded_lines: 0,
119            sloc_min: usize::MAX,
120            sloc_max: 0,
121        }
122    }
123}
124
125impl Sloc {
126    /// The `SLOC` metric value for this space (source lines, including blanks and comments).
127    #[inline]
128    #[must_use]
129    pub fn sloc(&self) -> u64 {
130        // This metric counts the number of physical lines this space
131        // occupies, including blanks and comments.
132        let span = span_rows(self.start, self.end_line);
133        // Subtract the lines belonging to `exclude_tests`-pruned subtrees
134        // (issue #722). `saturating_sub` is defensive: `excluded_lines`
135        // can never exceed the span (each pruned subtree is contained in
136        // it), but a future caller that double-records a span must not
137        // wrap to `u64::MAX`.
138        span.saturating_sub(self.excluded_lines) as u64
139    }
140
141    /// Records a pruned (`exclude_tests`) subtree's row span so that
142    /// `sloc()` drops in step with the node-accumulated loc sub-metrics.
143    /// The arguments are the pruned node's own start row and
144    /// `Node::end_line`; its row count follows the same rule the
145    /// enclosing span was measured with, so the subtraction cannot
146    /// overshoot.
147    ///
148    /// Pruned subtrees are whole Rust items (`mod`/`fn`/`impl`/…) that
149    /// rustfmt places on dedicated rows, so they share no physical line
150    /// with a retained sibling and their spans are pairwise disjoint (the
151    /// walk `continue`s on a pruned node, never descending, so a nested
152    /// pruned item is never recorded twice). The counts therefore add
153    /// without an interval merge (issue #722).
154    #[inline]
155    pub(crate) fn exclude_span(&mut self, start_row: usize, end_line: usize) {
156        self.excluded_lines = self
157            .excluded_lines
158            .saturating_add(span_rows(start_row, end_line));
159    }
160
161    /// The `Sloc` metric minimum value. See `min_or_zero` for the
162    /// `usize::MAX` sentinel guard.
163    #[inline]
164    #[must_use]
165    pub fn sloc_min(&self) -> u64 {
166        min_or_zero(self.sloc_min)
167    }
168
169    /// The `Sloc` metric maximum value.
170    #[inline]
171    #[must_use]
172    pub fn sloc_max(&self) -> u64 {
173        self.sloc_max as u64
174    }
175
176    /// Folds `other` into `self`, updating the min/max accumulators and
177    /// accumulating the child's `exclude_tests`-pruned line count.
178    #[inline]
179    pub fn merge(&mut self, other: &Sloc) {
180        // Fold the child's own min/max (not its aggregate `sloc()`), so the
181        // granularity of deeply nested function spaces propagates to the
182        // root. This matches every sibling metric (cyclomatic, cognitive,
183        // exit, nargs, nom, tokens, abc) and fixed issue #437.
184        self.sloc_min = self.sloc_min.min(other.sloc_min);
185        self.sloc_max = self.sloc_max.max(other.sloc_max);
186
187        // Propagate the child's pruned line count upward so an ancestor's
188        // span-based `sloc()` drops by the same lines, mirroring how `Ploc`
189        // unions its line-set upward (`Ploc::merge`). The prune hook records
190        // each pruned subtree's span only on its innermost enclosing
191        // func-space; without this fold a `#[test] fn` inside a retained
192        // `impl`/`trait`/closure would shrink only that space's `sloc`,
193        // leaving every enclosing space (including the unit, which feeds
194        // MI's SLOC term) inflated (issue #741, #722 follow-up). Each
195        // ancestor's span already includes the pruned rows exactly once, so
196        // subtracting the accumulated count once per level cannot
197        // double-count: pruned subtrees never descend, so a nested pruned
198        // item is recorded on a single space and folded up one altitude at
199        // a time.
200        self.excluded_lines = self.excluded_lines.saturating_add(other.excluded_lines);
201    }
202
203    #[inline]
204    pub(crate) fn compute_minmax(&mut self) {
205        // Fold this space's own span unconditionally so containers (Unit,
206        // classes) participate in min/max, matching the sibling metrics'
207        // convention. Each space runs this before being merged upward, so
208        // the guarded form previously here dropped nested leaves (#437).
209        self.sloc_min = self.sloc_min.min(self.sloc() as usize);
210        self.sloc_max = self.sloc_max.max(self.sloc() as usize);
211    }
212}
213
214/// The `PLoc` metric suite.
215#[derive(Debug, Clone, PartialEq)]
216pub struct Ploc {
217    lines: LineSet,
218    ploc_min: usize,
219    ploc_max: usize,
220}
221
222impl Default for Ploc {
223    fn default() -> Self {
224        Self {
225            lines: LineSet::default(),
226            ploc_min: usize::MAX,
227            ploc_max: 0,
228        }
229    }
230}
231
232impl Ploc {
233    /// The `PLOC` metric value for this space (physical lines of code, excluding blanks and comments).
234    #[inline]
235    #[must_use]
236    pub fn ploc(&self) -> u64 {
237        // This metric counts the number of instruction lines in a code
238        // https://en.wikipedia.org/wiki/Source_lines_of_code
239        self.lines.len() as u64
240    }
241
242    /// The `Ploc` metric minimum value. See `min_or_zero` for the
243    /// `usize::MAX` sentinel guard.
244    #[inline]
245    #[must_use]
246    pub fn ploc_min(&self) -> u64 {
247        min_or_zero(self.ploc_min)
248    }
249
250    /// The `Ploc` metric maximum value.
251    #[inline]
252    #[must_use]
253    pub fn ploc_max(&self) -> u64 {
254        self.ploc_max as u64
255    }
256
257    /// Folds `other` into `self`, unioning the line set and updating min/max.
258    #[inline]
259    pub fn merge(&mut self, other: &Ploc) {
260        // Union the child's physical lines in, so a line shared with a
261        // sibling space is counted once. A word-wise OR rather than an
262        // insert per row: a line inside D nested spaces is folded upward
263        // D times (#1109).
264        self.lines.union_with(&other.lines);
265
266        // Fold the child's own min/max so nested spaces propagate (#437).
267        self.ploc_min = self.ploc_min.min(other.ploc_min);
268        self.ploc_max = self.ploc_max.max(other.ploc_max);
269    }
270
271    #[inline]
272    pub(crate) fn compute_minmax(&mut self) {
273        // Fold this space's own value unconditionally so containers
274        // participate, matching the sibling metrics' convention (#437).
275        // Bound once: `ploc()` is a popcount over the whole word array
276        // since #1109, not the O(1) `HashSet::len` it used to be.
277        let ploc = self.ploc() as usize;
278        self.ploc_min = self.ploc_min.min(ploc);
279        self.ploc_max = self.ploc_max.max(ploc);
280    }
281}
282
283/// The `CLoc` metric suite.
284#[derive(Debug, Clone, PartialEq)]
285pub struct Cloc {
286    // Physical lines that are comment-only (no code). Feeds both
287    // `cloc()` and the `blank` metric (`sloc - ploc - only.len()`).
288    // A set rather than a counter so two standalone block comments on
289    // one physical line (`/*a*/ /*b*/`) contribute a single comment
290    // line, not one per node (issue #461 follow-up). Each spanned row
291    // of a genuine multi-line block comment is a distinct key, so it
292    // still counts once per line.
293    only_comment_line_starts: LineSet,
294    // Physical lines carrying both code and comment (`int x; /*c*/`).
295    // A line with several inline block comments (`f(int /*a*/, int
296    // /*b*/)`) must contribute a single comment line, not one per
297    // node, otherwise cloc can exceed sloc/ploc and push the MI
298    // comments_percentage above 100% (issue #461). Mirrors `Ploc`'s
299    // per-line de-dup via `Ploc::lines`; disjoint from
300    // `only_comment_line_starts` by construction.
301    code_comment_line_starts: LineSet,
302    comment_line_end: Option<usize>,
303    cloc_min: usize,
304    cloc_max: usize,
305}
306
307impl Default for Cloc {
308    fn default() -> Self {
309        Self {
310            only_comment_line_starts: LineSet::default(),
311            code_comment_line_starts: LineSet::default(),
312            comment_line_end: Option::default(),
313            cloc_min: usize::MAX,
314            cloc_max: 0,
315        }
316    }
317}
318
319impl Cloc {
320    /// The `CLOC` metric value for this space (comment lines, standalone + trailing).
321    #[inline]
322    #[must_use]
323    pub fn cloc(&self) -> u64 {
324        // Comments are counted regardless of their placement
325        // https://en.wikipedia.org/wiki/Source_lines_of_code
326        //
327        // Derive from the per-physical-line sets rather than summed
328        // counters so co-located comments (standalone or inline) count
329        // their shared line once and a comment line shared across
330        // merged spaces is not double-counted (issue #461). The two
331        // sets are disjoint by construction, but a union is used
332        // defensively so a stray overlap cannot inflate the count.
333        self.only_comment_line_starts
334            .union_len(&self.code_comment_line_starts) as u64
335    }
336
337    /// The `Cloc` metric minimum value. See `min_or_zero` for the
338    /// `usize::MAX` sentinel guard.
339    #[inline]
340    #[must_use]
341    pub fn cloc_min(&self) -> u64 {
342        min_or_zero(self.cloc_min)
343    }
344
345    /// The `Cloc` metric maximum value.
346    #[inline]
347    #[must_use]
348    pub fn cloc_max(&self) -> u64 {
349        self.cloc_max as u64
350    }
351
352    /// Folds `other` into `self`, summing comment counts and updating min/max.
353    #[inline]
354    pub fn merge(&mut self, other: &Cloc) {
355        // Union both per-line sets so a comment line shared across
356        // merged spaces is counted once (mirrors `Ploc`'s line union).
357        self.only_comment_line_starts
358            .union_with(&other.only_comment_line_starts);
359        self.code_comment_line_starts
360            .union_with(&other.code_comment_line_starts);
361
362        // Fold the child's own min/max so nested spaces propagate (#437).
363        self.cloc_min = self.cloc_min.min(other.cloc_min);
364        self.cloc_max = self.cloc_max.max(other.cloc_max);
365    }
366
367    #[inline]
368    pub(crate) fn compute_minmax(&mut self) {
369        // Fold this space's own value unconditionally so containers
370        // participate, matching the sibling metrics' convention (#437).
371        // Bound once: `cloc()` is a `union_len` over both word arrays
372        // since #1109, so calling it twice is six array scans per space.
373        let cloc = self.cloc() as usize;
374        self.cloc_min = self.cloc_min.min(cloc);
375        self.cloc_max = self.cloc_max.max(cloc);
376    }
377}
378
379/// The `LLoc` metric suite.
380#[derive(Debug, Clone, PartialEq)]
381pub struct Lloc {
382    logical_lines: usize,
383    lloc_min: usize,
384    lloc_max: usize,
385}
386
387impl Default for Lloc {
388    fn default() -> Self {
389        Self {
390            logical_lines: 0,
391            lloc_min: usize::MAX,
392            lloc_max: 0,
393        }
394    }
395}
396
397impl Lloc {
398    /// The `LLOC` metric value for this space (logical statements).
399    #[inline]
400    #[must_use]
401    pub fn lloc(&self) -> u64 {
402        // This metric counts the number of statements in a code
403        // https://en.wikipedia.org/wiki/Source_lines_of_code
404        self.logical_lines as u64
405    }
406
407    /// The `Lloc` metric minimum value. See `min_or_zero` for the
408    /// `usize::MAX` sentinel guard.
409    #[inline]
410    #[must_use]
411    pub fn lloc_min(&self) -> u64 {
412        min_or_zero(self.lloc_min)
413    }
414
415    /// The `Lloc` metric maximum value.
416    #[inline]
417    #[must_use]
418    pub fn lloc_max(&self) -> u64 {
419        self.lloc_max as u64
420    }
421
422    /// Records one logical statement.
423    ///
424    /// Exists so the 23 per-language `Loc` impls name the operation
425    /// instead of each reaching into a private field, which also keeps
426    /// the module's `arithmetic_side_effects` carve-out to this one
427    /// line rather than 36 of them (#1152). Saturating is unreachable —
428    /// the count is bounded by the AST's node count — and is the right
429    /// answer if it ever were: a pinned `usize::MAX` is a visibly broken
430    /// LLOC, where a wrap to 0 reads as a legitimately empty space.
431    #[inline]
432    fn count_logical_line(&mut self) {
433        self.logical_lines = self.logical_lines.saturating_add(1);
434    }
435
436    /// Folds `other` into `self`, summing statement counts and updating min/max.
437    #[inline]
438    pub fn merge(&mut self, other: &Lloc) {
439        // Merge lloc lines
440        self.logical_lines = self.logical_lines.saturating_add(other.logical_lines);
441        // Fold the child's own min/max so nested spaces propagate (#437).
442        self.lloc_min = self.lloc_min.min(other.lloc_min);
443        self.lloc_max = self.lloc_max.max(other.lloc_max);
444    }
445
446    #[inline]
447    pub(crate) fn compute_minmax(&mut self) {
448        // Fold this space's own value unconditionally so containers
449        // participate, matching the sibling metrics' convention (#437).
450        self.lloc_min = self.lloc_min.min(self.lloc() as usize);
451        self.lloc_max = self.lloc_max.max(self.lloc() as usize);
452    }
453}
454
455/// The `Loc` metric suite.
456#[derive(Debug, Clone, PartialEq)]
457#[non_exhaustive]
458pub struct Stats {
459    sloc: Sloc,
460    ploc: Ploc,
461    cloc: Cloc,
462    lloc: Lloc,
463    space_count: usize,
464    blank_min: usize,
465    blank_max: usize,
466}
467
468impl Default for Stats {
469    fn default() -> Self {
470        Self {
471            sloc: Sloc::default(),
472            ploc: Ploc::default(),
473            cloc: Cloc::default(),
474            lloc: Lloc::default(),
475            space_count: 1,
476            blank_min: usize::MAX,
477            blank_max: 0,
478        }
479    }
480}
481
482impl fmt::Display for Stats {
483    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
484        write!(
485            f,
486            "sloc: {}, ploc: {}, lloc: {}, cloc: {}, blank: {}, sloc_average: {}, ploc_average: {}, lloc_average: {}, cloc_average: {}, blank_average: {}, sloc_min: {}, sloc_max: {}, cloc_min: {}, cloc_max: {}, ploc_min: {}, ploc_max: {}, lloc_min: {}, lloc_max: {}, blank_min: {}, blank_max: {}",
487            self.sloc(),
488            self.ploc(),
489            self.lloc(),
490            self.cloc(),
491            self.blank(),
492            self.sloc_average(),
493            self.ploc_average(),
494            self.lloc_average(),
495            self.cloc_average(),
496            self.blank_average(),
497            self.sloc_min(),
498            self.sloc_max(),
499            self.cloc_min(),
500            self.cloc_max(),
501            self.ploc_min(),
502            self.ploc_max(),
503            self.lloc_min(),
504            self.lloc_max(),
505            self.blank_min(),
506            self.blank_max(),
507        )
508    }
509}
510
511impl Stats {
512    /// Test-only constructor that forces a degenerate `cloc`/`sloc`
513    /// pair (here `cloc > sloc`) so callers in other metric modules can
514    /// exercise downstream clamps without depending on the parsing
515    /// pipeline. `cloc <= sloc` always holds for parsed input after
516    /// issue #461, so this state is unreachable through normal use.
517    #[cfg(test)]
518    pub(crate) fn with_cloc_sloc(code_comment_lines: usize, sloc_end_row: usize) -> Self {
519        let mut stats = Stats::default();
520        stats.sloc.start = 0;
521        // `end_row + 1`: the synthetic span models a real one ending
522        // mid-line, so the final row counts and `sloc == sloc_end_row + 1`.
523        stats.sloc.end_line = sloc_end_row.saturating_add(1);
524        // Inject `code_comment_lines` distinct synthetic code-comment
525        // rows. An offset past `sloc_end_row` keeps them disjoint from
526        // any real span row, so `cloc()` (the set's cardinality) equals
527        // the requested count without colliding with sloc attribution.
528        if code_comment_lines > 0 {
529            let synthetic_base = sloc_end_row.saturating_add(1);
530            // Explicit rather than leaning on `insert_range`'s inverted-span
531            // guard: that guard exists to survive a bug, not to serve as a
532            // caller's empty case. The `- 1` is exact under the `> 0` test
533            // above, which is also what makes the inclusive end well-formed.
534            let synthetic_end = synthetic_base
535                .saturating_add(code_comment_lines)
536                .saturating_sub(1);
537            stats
538                .cloc
539                .code_comment_line_starts
540                .insert_range(synthetic_base, synthetic_end);
541        }
542        stats
543    }
544
545    /// Merges a second `Loc` metric suite into the first one
546    pub fn merge(&mut self, other: &Stats) {
547        self.sloc.merge(&other.sloc);
548        self.ploc.merge(&other.ploc);
549        self.cloc.merge(&other.cloc);
550        self.lloc.merge(&other.lloc);
551
552        // Count spaces
553        self.space_count = self.space_count.saturating_add(other.space_count);
554
555        // Fold the child's own min/max so nested spaces propagate (#437).
556        self.blank_min = self.blank_min.min(other.blank_min);
557        self.blank_max = self.blank_max.max(other.blank_max);
558    }
559
560    /// Records an `exclude_tests`-pruned subtree's span so this space's
561    /// `sloc()` excludes those physical lines, matching the
562    /// node-accumulated loc sub-metrics that the pruning already drops
563    /// (issue #722). Called from the walker for the space enclosing each
564    /// skipped subtree.
565    #[inline]
566    pub(crate) fn exclude_test_span(&mut self, start_row: usize, end_line: usize) {
567        self.sloc.exclude_span(start_row, end_line);
568    }
569
570    /// The `Sloc` metric.
571    ///
572    /// Counts the number of lines in a scope
573    #[inline]
574    #[must_use]
575    pub fn sloc(&self) -> u64 {
576        self.sloc.sloc()
577    }
578
579    /// The `Ploc` metric.
580    ///
581    /// Counts the number of instruction lines in a scope
582    #[inline]
583    #[must_use]
584    pub fn ploc(&self) -> u64 {
585        self.ploc.ploc()
586    }
587
588    /// The `Lloc` metric.
589    ///
590    /// Counts the number of statements in a scope
591    #[inline]
592    #[must_use]
593    pub fn lloc(&self) -> u64 {
594        self.lloc.lloc()
595    }
596
597    /// The `Cloc` metric.
598    ///
599    /// Counts the number of comments in a scope
600    #[inline]
601    #[must_use]
602    pub fn cloc(&self) -> u64 {
603        self.cloc.cloc()
604    }
605
606    /// The `Blank` metric.
607    ///
608    /// Counts the number of blank lines in a scope
609    #[inline]
610    #[must_use]
611    pub fn blank(&self) -> u64 {
612        // `sloc - ploc - only_comment_lines` can go negative when a space's
613        // physical and comment line attribution overlaps the span row count
614        // (e.g. single-line bodies). `saturating_sub` clamps at 0 (matching
615        // the prior `.max(0.0)` on the f64 form) so the serialized value is
616        // never negative (#437).
617        self.sloc()
618            .saturating_sub(self.ploc())
619            .saturating_sub(self.cloc.only_comment_line_starts.len() as u64)
620    }
621
622    /// The `Sloc` metric average value.
623    ///
624    /// This value is computed dividing the `Sloc` value for the number of spaces
625    #[inline]
626    #[must_use]
627    pub fn sloc_average(&self) -> f64 {
628        crate::metrics::average(self.sloc() as f64, self.space_count)
629    }
630
631    /// The `Ploc` metric average value.
632    ///
633    /// This value is computed dividing the `Ploc` value for the number of spaces
634    #[inline]
635    #[must_use]
636    pub fn ploc_average(&self) -> f64 {
637        crate::metrics::average(self.ploc() as f64, self.space_count)
638    }
639
640    /// The `Lloc` metric average value.
641    ///
642    /// This value is computed dividing the `Lloc` value for the number of spaces
643    #[inline]
644    #[must_use]
645    pub fn lloc_average(&self) -> f64 {
646        crate::metrics::average(self.lloc() as f64, self.space_count)
647    }
648
649    /// The `Cloc` metric average value.
650    ///
651    /// This value is computed dividing the `Cloc` value for the number of spaces
652    #[inline]
653    #[must_use]
654    pub fn cloc_average(&self) -> f64 {
655        crate::metrics::average(self.cloc() as f64, self.space_count)
656    }
657
658    /// The `Blank` metric average value.
659    ///
660    /// This value is computed dividing the `Blank` value for the number of spaces
661    #[inline]
662    #[must_use]
663    pub fn blank_average(&self) -> f64 {
664        crate::metrics::average(self.blank() as f64, self.space_count)
665    }
666
667    /// The `Sloc` metric minimum value.
668    #[inline]
669    #[must_use]
670    pub fn sloc_min(&self) -> u64 {
671        self.sloc.sloc_min()
672    }
673
674    /// The `Sloc` metric maximum value.
675    #[inline]
676    #[must_use]
677    pub fn sloc_max(&self) -> u64 {
678        self.sloc.sloc_max()
679    }
680
681    /// The `Cloc` metric minimum value.
682    #[inline]
683    #[must_use]
684    pub fn cloc_min(&self) -> u64 {
685        self.cloc.cloc_min()
686    }
687
688    /// The `Cloc` metric maximum value.
689    #[inline]
690    #[must_use]
691    pub fn cloc_max(&self) -> u64 {
692        self.cloc.cloc_max()
693    }
694
695    /// The `Ploc` metric minimum value.
696    #[inline]
697    #[must_use]
698    pub fn ploc_min(&self) -> u64 {
699        self.ploc.ploc_min()
700    }
701
702    /// The `Ploc` metric maximum value.
703    #[inline]
704    #[must_use]
705    pub fn ploc_max(&self) -> u64 {
706        self.ploc.ploc_max()
707    }
708
709    /// The `Lloc` metric minimum value.
710    #[inline]
711    #[must_use]
712    pub fn lloc_min(&self) -> u64 {
713        self.lloc.lloc_min()
714    }
715
716    /// The `Lloc` metric maximum value.
717    #[inline]
718    #[must_use]
719    pub fn lloc_max(&self) -> u64 {
720        self.lloc.lloc_max()
721    }
722
723    /// The `Blank` metric minimum value. See `min_or_zero` for the
724    /// `usize::MAX` sentinel guard.
725    #[inline]
726    #[must_use]
727    pub fn blank_min(&self) -> u64 {
728        min_or_zero(self.blank_min)
729    }
730
731    /// The `Blank` metric maximum value.
732    #[inline]
733    #[must_use]
734    pub fn blank_max(&self) -> u64 {
735        self.blank_max as u64
736    }
737
738    #[inline]
739    pub(crate) fn compute_minmax(&mut self) {
740        self.sloc.compute_minmax();
741        self.ploc.compute_minmax();
742        self.cloc.compute_minmax();
743        self.lloc.compute_minmax();
744
745        // Fold this space's own blank value unconditionally so containers
746        // participate, matching the sibling metrics' convention (#437).
747        // `blank()` returns a `u64` already clamped at 0 by `saturating_sub`,
748        // so the widening `as usize` cast is lossless (64-bit) and cannot
749        // introduce a spurious value here. Bound once: `blank()` popcounts
750        // two word arrays since #1109.
751        let blank = self.blank() as usize;
752        self.blank_min = self.blank_min.min(blank);
753        self.blank_max = self.blank_max.max(blank);
754    }
755
756    pub(crate) fn init_unit_span(&mut self, start: usize, end_line: usize) {
757        self.sloc.start = start;
758        self.sloc.end_line = end_line;
759    }
760}
761
762#[doc(hidden)]
763/// Per-language computation of the lines-of-code metrics.
764pub(crate) trait Loc
765where
766    Self: Checker,
767{
768    /// Walk `node` and update `stats` with this metric for the language
769    /// implementing the trait.
770    /// `ancestors` is the chain the walker descended through: the
771    /// C-family and JVM-family arms read it to tell a declaration in a
772    /// loop header from one in the loop body (#1084).
773    fn compute(node: &Node, ancestors: Ancestors<'_, '_>, stats: &mut Stats, is_func_space: bool);
774}
775
776mod shared;
777pub(crate) use shared::*;
778
779// Real defaults — Loc counts on these "languages" would conflate
780// comments / preproc directives with executable code; treating them
781// as 0 is the documented behaviour. Audited in #188.
782implement_metric_trait!(Loc, PreprocCode, CcommentCode);
783
784mod bash;
785mod c;
786mod cpp;
787mod csharp;
788mod elixir;
789mod go;
790mod groovy;
791mod irules;
792mod java;
793mod javascript;
794mod kotlin;
795mod lua;
796mod mozcpp;
797mod mozjs;
798mod objc;
799mod perl;
800mod php;
801mod python;
802mod ruby;
803mod rust;
804mod tcl;
805mod tsx;
806mod typescript;
807
808#[cfg(test)]
809#[allow(
810    clippy::float_cmp,
811    clippy::cast_precision_loss,
812    clippy::cast_possible_truncation,
813    clippy::cast_sign_loss,
814    clippy::similar_names,
815    clippy::doc_markdown,
816    clippy::needless_raw_string_hashes,
817    clippy::too_many_lines
818)]
819mod tests {
820    use crate::test_support::{check_metrics_only_shim, metrics_verbatim, space_verbatim};
821
822    use super::*;
823
824    check_metrics_only_shim!(check_metrics, Loc);
825
826    /// A `Stats::default()` that never sees an observation must not leak
827    /// the `usize::MAX` sentinel for any of the LOC `_min` accumulators
828    /// (`sloc_min`, `ploc_min`, `lloc_min`, `cloc_min`, `blank_min`).
829    /// The getters collapse the sentinel to `0.0` so JSON never emits
830    /// `1.8446744e19`.
831    #[test]
832    fn loc_empty_file_min_is_zero() {
833        let stats = Stats::default();
834        assert_eq!(stats.sloc_min(), 0);
835        assert_eq!(stats.ploc_min(), 0);
836        assert_eq!(stats.lloc_min(), 0);
837        assert_eq!(stats.cloc_min(), 0);
838        assert_eq!(stats.blank_min(), 0);
839    }
840
841    /// Parses `source` with `PerlParser` and asserts the resulting tree has
842    /// no `ERROR` nodes. Use alongside metric assertions whose expected
843    /// values would happen to match what an error tree produces — a parse
844    /// regression in tree-sitter-perl could otherwise leave such tests
845    /// silently green.
846    #[cfg(test)]
847    fn assert_perl_parses_cleanly(source: &str) {
848        use crate::traits::ParserTrait;
849        // Mirror the trailing-newline normalisation `check_func_space` does
850        // before handing input to the parser, so this helper sees the same
851        // bytes the metric tests do.
852        let path = std::path::PathBuf::from("foo.pl");
853        let mut bytes = source.trim_end_matches('\n').as_bytes().to_vec();
854        bytes.push(b'\n');
855        let parser = PerlParser::new(bytes, &path, None);
856        assert!(
857            !parser.root().has_error(),
858            "tree-sitter-perl returned an error tree for snippet:\n{source}"
859        );
860    }
861
862    #[test]
863    fn python_sloc() {
864        check_metrics::<PythonParser>(
865            "
866
867            a = 42
868
869            ",
870            "foo.py",
871            |metric| {
872                // Spaces: 1
873                insta::assert_json_snapshot!(
874                    metric.loc,
875                    @r#"
876                {
877                  "sloc": 1,
878                  "ploc": 1,
879                  "lloc": 1,
880                  "cloc": 0,
881                  "blank": 0,
882                  "sloc_average": 1.0,
883                  "ploc_average": 1.0,
884                  "lloc_average": 1.0,
885                  "cloc_average": 0.0,
886                  "blank_average": 0.0,
887                  "sloc_min": 1,
888                  "sloc_max": 1,
889                  "cloc_min": 0,
890                  "cloc_max": 0,
891                  "ploc_min": 1,
892                  "ploc_max": 1,
893                  "lloc_min": 1,
894                  "lloc_max": 1,
895                  "blank_min": 0,
896                  "blank_max": 0
897                }
898                "#
899                );
900            },
901        );
902    }
903
904    #[test]
905    fn python_blank() {
906        check_metrics::<PythonParser>(
907            "
908            a = 42
909
910            b = 43
911
912            ",
913            "foo.py",
914            |metric| {
915                // Spaces: 1
916                insta::assert_json_snapshot!(
917                    metric.loc,
918                    @r#"
919                {
920                  "sloc": 3,
921                  "ploc": 2,
922                  "lloc": 2,
923                  "cloc": 0,
924                  "blank": 1,
925                  "sloc_average": 3.0,
926                  "ploc_average": 2.0,
927                  "lloc_average": 2.0,
928                  "cloc_average": 0.0,
929                  "blank_average": 1.0,
930                  "sloc_min": 3,
931                  "sloc_max": 3,
932                  "cloc_min": 0,
933                  "cloc_max": 0,
934                  "ploc_min": 2,
935                  "ploc_max": 2,
936                  "lloc_min": 2,
937                  "lloc_max": 2,
938                  "blank_min": 1,
939                  "blank_max": 1
940                }
941                "#
942                );
943            },
944        );
945    }
946
947    #[test]
948    fn rust_blank() {
949        check_metrics::<RustParser>(
950            "
951
952            let a = 42;
953
954            let b = 43;
955
956            ",
957            "foo.rs",
958            |metric| {
959                // Spaces: 1
960                insta::assert_json_snapshot!(
961                    metric.loc,
962                    @r#"
963                {
964                  "sloc": 3,
965                  "ploc": 2,
966                  "lloc": 2,
967                  "cloc": 0,
968                  "blank": 1,
969                  "sloc_average": 3.0,
970                  "ploc_average": 2.0,
971                  "lloc_average": 2.0,
972                  "cloc_average": 0.0,
973                  "blank_average": 1.0,
974                  "sloc_min": 3,
975                  "sloc_max": 3,
976                  "cloc_min": 0,
977                  "cloc_max": 0,
978                  "ploc_min": 2,
979                  "ploc_max": 2,
980                  "lloc_min": 2,
981                  "lloc_max": 2,
982                  "blank_min": 1,
983                  "blank_max": 1
984                }
985                "#
986                );
987            },
988        );
989
990        check_metrics::<RustParser>("fn func() { /* comment */ }", "foo.rs", |metric| {
991            // Spaces: 2
992            insta::assert_json_snapshot!(
993                metric.loc,
994                @r#"
995            {
996              "sloc": 1,
997              "ploc": 1,
998              "lloc": 0,
999              "cloc": 1,
1000              "blank": 0,
1001              "sloc_average": 0.5,
1002              "ploc_average": 0.5,
1003              "lloc_average": 0.0,
1004              "cloc_average": 0.5,
1005              "blank_average": 0.0,
1006              "sloc_min": 1,
1007              "sloc_max": 1,
1008              "cloc_min": 1,
1009              "cloc_max": 1,
1010              "ploc_min": 1,
1011              "ploc_max": 1,
1012              "lloc_min": 0,
1013              "lloc_max": 0,
1014              "blank_min": 0,
1015              "blank_max": 0
1016            }
1017            "#
1018            );
1019        });
1020    }
1021
1022    #[test]
1023    fn c_blank() {
1024        check_metrics::<CParser>(
1025            "
1026
1027            int a = 42;
1028
1029            int b = 43;
1030
1031            ",
1032            "foo.c",
1033            |metric| {
1034                // Spaces: 1
1035                insta::assert_json_snapshot!(
1036                    metric.loc,
1037                    @r#"
1038                {
1039                  "sloc": 3,
1040                  "ploc": 2,
1041                  "lloc": 2,
1042                  "cloc": 0,
1043                  "blank": 1,
1044                  "sloc_average": 3.0,
1045                  "ploc_average": 2.0,
1046                  "lloc_average": 2.0,
1047                  "cloc_average": 0.0,
1048                  "blank_average": 1.0,
1049                  "sloc_min": 3,
1050                  "sloc_max": 3,
1051                  "cloc_min": 0,
1052                  "cloc_max": 0,
1053                  "ploc_min": 2,
1054                  "ploc_max": 2,
1055                  "lloc_min": 2,
1056                  "lloc_max": 2,
1057                  "blank_min": 1,
1058                  "blank_max": 1
1059                }
1060                "#
1061                );
1062            },
1063        );
1064    }
1065
1066    #[test]
1067    fn python_no_zero_blank() {
1068        // Checks that the blank metric is not equal to 0 when there are some
1069        // comments next to code lines.
1070        check_metrics::<PythonParser>(
1071            "def ConnectToUpdateServer():
1072                 pool = 4
1073
1074                 updateServer = -42
1075                 isConnected = False
1076                 currTry = 0
1077                 numRetries = 10 # Number of IPC connection retries before
1078                                 # giving up.
1079                 numTries = 20 # Number of IPC connection tries before
1080                               # giving up.",
1081            "foo.py",
1082            |metric| {
1083                // Spaces: 2
1084                insta::assert_json_snapshot!(
1085                    metric.loc,
1086                    @r#"
1087                {
1088                  "sloc": 10,
1089                  "ploc": 7,
1090                  "lloc": 6,
1091                  "cloc": 4,
1092                  "blank": 1,
1093                  "sloc_average": 5.0,
1094                  "ploc_average": 3.5,
1095                  "lloc_average": 3.0,
1096                  "cloc_average": 2.0,
1097                  "blank_average": 0.5,
1098                  "sloc_min": 10,
1099                  "sloc_max": 10,
1100                  "cloc_min": 4,
1101                  "cloc_max": 4,
1102                  "ploc_min": 7,
1103                  "ploc_max": 7,
1104                  "lloc_min": 6,
1105                  "lloc_max": 6,
1106                  "blank_min": 1,
1107                  "blank_max": 1
1108                }
1109                "#
1110                );
1111            },
1112        );
1113    }
1114
1115    #[test]
1116    fn python_no_blank() {
1117        // Checks that the blank metric is equal to 0 when there are no blank
1118        // lines and there are comments next to code lines.
1119        check_metrics::<PythonParser>(
1120            "def ConnectToUpdateServer():
1121                 pool = 4
1122                 updateServer = -42
1123                 isConnected = False
1124                 currTry = 0
1125                 numRetries = 10 # Number of IPC connection retries before
1126                                 # giving up.
1127                 numTries = 20 # Number of IPC connection tries before
1128                               # giving up.",
1129            "foo.py",
1130            |metric| {
1131                // Spaces: 2
1132                insta::assert_json_snapshot!(
1133                    metric.loc,
1134                    @r#"
1135                {
1136                  "sloc": 9,
1137                  "ploc": 7,
1138                  "lloc": 6,
1139                  "cloc": 4,
1140                  "blank": 0,
1141                  "sloc_average": 4.5,
1142                  "ploc_average": 3.5,
1143                  "lloc_average": 3.0,
1144                  "cloc_average": 2.0,
1145                  "blank_average": 0.0,
1146                  "sloc_min": 9,
1147                  "sloc_max": 9,
1148                  "cloc_min": 4,
1149                  "cloc_max": 4,
1150                  "ploc_min": 7,
1151                  "ploc_max": 7,
1152                  "lloc_min": 6,
1153                  "lloc_max": 6,
1154                  "blank_min": 0,
1155                  "blank_max": 0
1156                }
1157                "#
1158                );
1159            },
1160        );
1161    }
1162
1163    #[test]
1164    fn python_no_zero_blank_more_comments() {
1165        // Checks that the blank metric is not equal to 0 when there are more
1166        // comments next to code lines compared to the previous tests.
1167        check_metrics::<PythonParser>(
1168            "def ConnectToUpdateServer():
1169                 pool = 4
1170
1171                 updateServer = -42
1172                 isConnected = False
1173                 currTry = 0 # Set this variable to 0
1174                 numRetries = 10 # Number of IPC connection retries before
1175                                 # giving up.
1176                 numTries = 20 # Number of IPC connection tries before
1177                               # giving up.",
1178            "foo.py",
1179            |metric| {
1180                // Spaces: 2
1181                insta::assert_json_snapshot!(
1182                    metric.loc,
1183                    @r#"
1184                {
1185                  "sloc": 10,
1186                  "ploc": 7,
1187                  "lloc": 6,
1188                  "cloc": 5,
1189                  "blank": 1,
1190                  "sloc_average": 5.0,
1191                  "ploc_average": 3.5,
1192                  "lloc_average": 3.0,
1193                  "cloc_average": 2.5,
1194                  "blank_average": 0.5,
1195                  "sloc_min": 10,
1196                  "sloc_max": 10,
1197                  "cloc_min": 5,
1198                  "cloc_max": 5,
1199                  "ploc_min": 7,
1200                  "ploc_max": 7,
1201                  "lloc_min": 6,
1202                  "lloc_max": 6,
1203                  "blank_min": 1,
1204                  "blank_max": 1
1205                }
1206                "#
1207                );
1208            },
1209        );
1210    }
1211
1212    #[test]
1213    fn rust_no_zero_blank() {
1214        // Checks that the blank metric is not equal to 0 when there are some
1215        // comments next to code lines.
1216        check_metrics::<RustParser>(
1217            "fn ConnectToUpdateServer() {
1218              let pool = 0;
1219
1220              let updateServer = -42;
1221              let isConnected = false;
1222              let currTry = 0;
1223              let numRetries = 10;  // Number of IPC connection retries before
1224                                    // giving up.
1225              let numTries = 20;    // Number of IPC connection tries before
1226                                    // giving up.
1227            }",
1228            "foo.rs",
1229            |metric| {
1230                // Spaces: 2
1231                insta::assert_json_snapshot!(
1232                    metric.loc,
1233                    @r#"
1234                {
1235                  "sloc": 11,
1236                  "ploc": 8,
1237                  "lloc": 6,
1238                  "cloc": 4,
1239                  "blank": 1,
1240                  "sloc_average": 5.5,
1241                  "ploc_average": 4.0,
1242                  "lloc_average": 3.0,
1243                  "cloc_average": 2.0,
1244                  "blank_average": 0.5,
1245                  "sloc_min": 11,
1246                  "sloc_max": 11,
1247                  "cloc_min": 4,
1248                  "cloc_max": 4,
1249                  "ploc_min": 8,
1250                  "ploc_max": 8,
1251                  "lloc_min": 6,
1252                  "lloc_max": 6,
1253                  "blank_min": 1,
1254                  "blank_max": 1
1255                }
1256                "#
1257                );
1258            },
1259        );
1260    }
1261
1262    #[test]
1263    fn javascript_no_zero_blank() {
1264        // Checks that the blank metric is not equal to 0 when there are some
1265        // comments next to code lines.
1266        check_metrics::<JavascriptParser>(
1267            "function ConnectToUpdateServer() {
1268              var pool = 0;
1269
1270              var updateServer = -42;
1271              var isConnected = false;
1272              var currTry = 0;
1273              var numRetries = 10;  // Number of IPC connection retries before
1274                                    // giving up.
1275              var numTries = 20;    // Number of IPC connection tries before
1276                                    // giving up.
1277            }",
1278            "foo.js",
1279            |metric| {
1280                // Spaces: 2
1281                insta::assert_json_snapshot!(
1282                    metric.loc,
1283                    @r#"
1284                {
1285                  "sloc": 11,
1286                  "ploc": 8,
1287                  "lloc": 6,
1288                  "cloc": 4,
1289                  "blank": 1,
1290                  "sloc_average": 5.5,
1291                  "ploc_average": 4.0,
1292                  "lloc_average": 3.0,
1293                  "cloc_average": 2.0,
1294                  "blank_average": 0.5,
1295                  "sloc_min": 11,
1296                  "sloc_max": 11,
1297                  "cloc_min": 4,
1298                  "cloc_max": 4,
1299                  "ploc_min": 8,
1300                  "ploc_max": 8,
1301                  "lloc_min": 6,
1302                  "lloc_max": 6,
1303                  "blank_min": 1,
1304                  "blank_max": 1
1305                }
1306                "#
1307                );
1308            },
1309        );
1310    }
1311
1312    #[test]
1313    fn cpp_no_zero_blank() {
1314        // Checks that the blank metric is not equal to 0 when there are some
1315        // comments next to code lines.
1316        check_metrics::<CppParser>(
1317            "void ConnectToUpdateServer() {
1318              int pool;
1319
1320              int updateServer = -42;
1321              bool isConnected = false;
1322              int currTry = 0;
1323              const int numRetries = 10; // Number of IPC connection retries before
1324                                         // giving up.
1325              const int numTries = 20; // Number of IPC connection tries before
1326                                       // giving up.
1327            }",
1328            "foo.cpp",
1329            |metric| {
1330                // Spaces: 2
1331                insta::assert_json_snapshot!(
1332                    metric.loc,
1333                    @r#"
1334                {
1335                  "sloc": 11,
1336                  "ploc": 8,
1337                  "lloc": 6,
1338                  "cloc": 4,
1339                  "blank": 1,
1340                  "sloc_average": 5.5,
1341                  "ploc_average": 4.0,
1342                  "lloc_average": 3.0,
1343                  "cloc_average": 2.0,
1344                  "blank_average": 0.5,
1345                  "sloc_min": 11,
1346                  "sloc_max": 11,
1347                  "cloc_min": 4,
1348                  "cloc_max": 4,
1349                  "ploc_min": 8,
1350                  "ploc_max": 8,
1351                  "lloc_min": 6,
1352                  "lloc_max": 6,
1353                  "blank_min": 1,
1354                  "blank_max": 1
1355                }
1356                "#
1357                );
1358            },
1359        );
1360    }
1361
1362    #[test]
1363    fn cpp_code_line_start_block_blank() {
1364        // Checks that the blank metric is equal to 1 when there are
1365        // block comments starting next to code lines.
1366        check_metrics::<CppParser>(
1367            "void ConnectToUpdateServer() {
1368              int pool;
1369
1370              int updateServer = -42;
1371              bool isConnected = false;
1372              int currTry = 0;
1373              const int numRetries = 10; /* Number of IPC connection retries
1374              before
1375              giving up. */
1376              const int numTries = 20; // Number of IPC connection tries before
1377                                       // giving up.
1378            }",
1379            "foo.cpp",
1380            |metric| {
1381                // Spaces: 2
1382                insta::assert_json_snapshot!(
1383                    metric.loc,
1384                    @r#"
1385                {
1386                  "sloc": 12,
1387                  "ploc": 8,
1388                  "lloc": 6,
1389                  "cloc": 5,
1390                  "blank": 1,
1391                  "sloc_average": 6.0,
1392                  "ploc_average": 4.0,
1393                  "lloc_average": 3.0,
1394                  "cloc_average": 2.5,
1395                  "blank_average": 0.5,
1396                  "sloc_min": 12,
1397                  "sloc_max": 12,
1398                  "cloc_min": 5,
1399                  "cloc_max": 5,
1400                  "ploc_min": 8,
1401                  "ploc_max": 8,
1402                  "lloc_min": 6,
1403                  "lloc_max": 6,
1404                  "blank_min": 1,
1405                  "blank_max": 1
1406                }
1407                "#
1408                );
1409            },
1410        );
1411    }
1412
1413    #[test]
1414    fn cpp_block_comment_blank() {
1415        // Checks that the blank metric is equal to 1 when there are
1416        // block comments on independent lines.
1417        check_metrics::<CppParser>(
1418            "void ConnectToUpdateServer() {
1419              int pool;
1420
1421              int updateServer = -42;
1422              bool isConnected = false;
1423              int currTry = 0;
1424              /* Number of IPC connection retries
1425              before
1426              giving up. */
1427              const int numRetries = 10;
1428              const int numTries = 20; // Number of IPC connection tries before
1429                                       // giving up.
1430            }",
1431            "foo.cpp",
1432            |metric| {
1433                // Spaces: 2
1434                insta::assert_json_snapshot!(
1435                    metric.loc,
1436                    @r#"
1437                {
1438                  "sloc": 13,
1439                  "ploc": 8,
1440                  "lloc": 6,
1441                  "cloc": 5,
1442                  "blank": 1,
1443                  "sloc_average": 6.5,
1444                  "ploc_average": 4.0,
1445                  "lloc_average": 3.0,
1446                  "cloc_average": 2.5,
1447                  "blank_average": 0.5,
1448                  "sloc_min": 13,
1449                  "sloc_max": 13,
1450                  "cloc_min": 5,
1451                  "cloc_max": 5,
1452                  "ploc_min": 8,
1453                  "ploc_max": 8,
1454                  "lloc_min": 6,
1455                  "lloc_max": 6,
1456                  "blank_min": 1,
1457                  "blank_max": 1
1458                }
1459                "#
1460                );
1461            },
1462        );
1463    }
1464
1465    #[test]
1466    fn cpp_code_line_block_one_line_blank() {
1467        // Checks that the blank metric is equal to 1 when there are
1468        // block comments before the same code line.
1469        check_metrics::<CppParser>(
1470            "void ConnectToUpdateServer() {
1471              int pool;
1472
1473              int updateServer = -42;
1474              bool isConnected = false;
1475              int currTry = 0;
1476              /* Number of IPC connection retries before giving up. */ const int numRetries = 10;
1477              const int numTries = 20; // Number of IPC connection tries before
1478                                       // giving up.
1479            }",
1480            "foo.cpp",
1481            |metric| {
1482                // Spaces: 2
1483                insta::assert_json_snapshot!(
1484                    metric.loc,
1485                    @r#"
1486                {
1487                  "sloc": 10,
1488                  "ploc": 8,
1489                  "lloc": 6,
1490                  "cloc": 3,
1491                  "blank": 1,
1492                  "sloc_average": 5.0,
1493                  "ploc_average": 4.0,
1494                  "lloc_average": 3.0,
1495                  "cloc_average": 1.5,
1496                  "blank_average": 0.5,
1497                  "sloc_min": 10,
1498                  "sloc_max": 10,
1499                  "cloc_min": 3,
1500                  "cloc_max": 3,
1501                  "ploc_min": 8,
1502                  "ploc_max": 8,
1503                  "lloc_min": 6,
1504                  "lloc_max": 6,
1505                  "blank_min": 1,
1506                  "blank_max": 1
1507                }
1508                "#
1509                );
1510            },
1511        );
1512    }
1513
1514    #[test]
1515    fn cpp_code_line_end_block_blank() {
1516        // Checks that the blank metric is equal to 1 when there are
1517        // block comments ending next to code lines.
1518        check_metrics::<CppParser>(
1519            "void ConnectToUpdateServer() {
1520              int pool;
1521
1522              int updateServer = -42;
1523              bool isConnected = false;
1524              int currTry = 0;
1525              /* Number of IPC connection retries
1526              before
1527              giving up. */ const int numRetries = 10;
1528              const int numTries = 20; // Number of IPC connection tries before
1529                                       // giving up.
1530            }",
1531            "foo.cpp",
1532            |metric| {
1533                // Spaces: 2
1534                insta::assert_json_snapshot!(
1535                    metric.loc,
1536                    @r#"
1537                {
1538                  "sloc": 12,
1539                  "ploc": 8,
1540                  "lloc": 6,
1541                  "cloc": 5,
1542                  "blank": 1,
1543                  "sloc_average": 6.0,
1544                  "ploc_average": 4.0,
1545                  "lloc_average": 3.0,
1546                  "cloc_average": 2.5,
1547                  "blank_average": 0.5,
1548                  "sloc_min": 12,
1549                  "sloc_max": 12,
1550                  "cloc_min": 5,
1551                  "cloc_max": 5,
1552                  "ploc_min": 8,
1553                  "ploc_max": 8,
1554                  "lloc_min": 6,
1555                  "lloc_max": 6,
1556                  "blank_min": 1,
1557                  "blank_max": 1
1558                }
1559                "#
1560                );
1561            },
1562        );
1563    }
1564
1565    #[test]
1566    fn python_cloc() {
1567        check_metrics::<PythonParser>(
1568            "\"\"\"Block comment
1569            Block comment
1570            \"\"\"
1571            # Line Comment
1572            a = 42 # Line Comment",
1573            "foo.py",
1574            |metric| {
1575                // Spaces: 1
1576                insta::assert_json_snapshot!(
1577                    metric.loc,
1578                    @r#"
1579                {
1580                  "sloc": 5,
1581                  "ploc": 1,
1582                  "lloc": 2,
1583                  "cloc": 5,
1584                  "blank": 0,
1585                  "sloc_average": 5.0,
1586                  "ploc_average": 1.0,
1587                  "lloc_average": 2.0,
1588                  "cloc_average": 5.0,
1589                  "blank_average": 0.0,
1590                  "sloc_min": 5,
1591                  "sloc_max": 5,
1592                  "cloc_min": 5,
1593                  "cloc_max": 5,
1594                  "ploc_min": 1,
1595                  "ploc_max": 1,
1596                  "lloc_min": 2,
1597                  "lloc_max": 2,
1598                  "blank_min": 0,
1599                  "blank_max": 0
1600                }
1601                "#
1602                );
1603            },
1604        );
1605    }
1606
1607    #[test]
1608    fn rust_cloc() {
1609        check_metrics::<RustParser>(
1610            "/*Block comment
1611            Block Comment*/
1612            //Line Comment
1613            /*Block Comment*/ let a = 42; // Line Comment",
1614            "foo.rs",
1615            |metric| {
1616                // Spaces: 1
1617                // expected: cloc = 4 — the 2-line block (lines 1-2) and
1618                // the standalone `//Line Comment` (line 3) give 3
1619                // only-comment lines; line 4 carries a leading block
1620                // comment AND a trailing line comment but is one
1621                // physical code line, so it adds a single code-comment
1622                // line, not two (issue #461). Pre-fix this reported 5,
1623                // violating cloc <= sloc (sloc = 4).
1624                insta::assert_json_snapshot!(
1625                    metric.loc,
1626                    @r#"
1627                {
1628                  "sloc": 4,
1629                  "ploc": 1,
1630                  "lloc": 1,
1631                  "cloc": 4,
1632                  "blank": 0,
1633                  "sloc_average": 4.0,
1634                  "ploc_average": 1.0,
1635                  "lloc_average": 1.0,
1636                  "cloc_average": 4.0,
1637                  "blank_average": 0.0,
1638                  "sloc_min": 4,
1639                  "sloc_max": 4,
1640                  "cloc_min": 4,
1641                  "cloc_max": 4,
1642                  "ploc_min": 1,
1643                  "ploc_max": 1,
1644                  "lloc_min": 1,
1645                  "lloc_max": 1,
1646                  "blank_min": 0,
1647                  "blank_max": 0
1648                }
1649                "#
1650                );
1651            },
1652        );
1653    }
1654
1655    #[test]
1656    fn c_cloc() {
1657        check_metrics::<CParser>(
1658            "/*Block comment
1659            Block Comment*/
1660            //Line Comment
1661            /*Block Comment*/ int a = 42; // Line Comment",
1662            "foo.c",
1663            |metric| {
1664                // Spaces: 1
1665                // expected: cloc = 4 — see `rust_cloc`; line 4's leading
1666                // block comment and trailing line comment share one
1667                // physical code line and add a single code-comment line
1668                // (issue #461). Pre-fix reported 5 (cloc > sloc = 4).
1669                insta::assert_json_snapshot!(
1670                    metric.loc,
1671                    @r#"
1672                {
1673                  "sloc": 4,
1674                  "ploc": 1,
1675                  "lloc": 1,
1676                  "cloc": 4,
1677                  "blank": 0,
1678                  "sloc_average": 4.0,
1679                  "ploc_average": 1.0,
1680                  "lloc_average": 1.0,
1681                  "cloc_average": 4.0,
1682                  "blank_average": 0.0,
1683                  "sloc_min": 4,
1684                  "sloc_max": 4,
1685                  "cloc_min": 4,
1686                  "cloc_max": 4,
1687                  "ploc_min": 1,
1688                  "ploc_max": 1,
1689                  "lloc_min": 1,
1690                  "lloc_max": 1,
1691                  "blank_min": 0,
1692                  "blank_max": 0
1693                }
1694                "#
1695                );
1696            },
1697        );
1698    }
1699
1700    #[test]
1701    fn python_lloc() {
1702        check_metrics::<PythonParser>(
1703            "for x in range(0,42):
1704                if x % 2 == 0:
1705                    print(x)",
1706            "foo.py",
1707            |metric| {
1708                // Spaces: 1
1709                insta::assert_json_snapshot!(
1710                    metric.loc,
1711                    @r#"
1712                {
1713                  "sloc": 3,
1714                  "ploc": 3,
1715                  "lloc": 3,
1716                  "cloc": 0,
1717                  "blank": 0,
1718                  "sloc_average": 3.0,
1719                  "ploc_average": 3.0,
1720                  "lloc_average": 3.0,
1721                  "cloc_average": 0.0,
1722                  "blank_average": 0.0,
1723                  "sloc_min": 3,
1724                  "sloc_max": 3,
1725                  "cloc_min": 0,
1726                  "cloc_max": 0,
1727                  "ploc_min": 3,
1728                  "ploc_max": 3,
1729                  "lloc_min": 3,
1730                  "lloc_max": 3,
1731                  "blank_min": 0,
1732                  "blank_max": 0
1733                }
1734                "#
1735                );
1736            },
1737        );
1738    }
1739
1740    #[test]
1741    fn rust_lloc() {
1742        check_metrics::<RustParser>(
1743            "for x in 0..42 {
1744                if x % 2 == 0 {
1745                    println!(\"{}\", x);
1746                }
1747             }",
1748            "foo.rs",
1749            |metric| {
1750                // Spaces: 1
1751                insta::assert_json_snapshot!(
1752                    metric.loc,
1753                    @r#"
1754                {
1755                  "sloc": 5,
1756                  "ploc": 5,
1757                  "lloc": 3,
1758                  "cloc": 0,
1759                  "blank": 0,
1760                  "sloc_average": 5.0,
1761                  "ploc_average": 5.0,
1762                  "lloc_average": 3.0,
1763                  "cloc_average": 0.0,
1764                  "blank_average": 0.0,
1765                  "sloc_min": 5,
1766                  "sloc_max": 5,
1767                  "cloc_min": 0,
1768                  "cloc_max": 0,
1769                  "ploc_min": 5,
1770                  "ploc_max": 5,
1771                  "lloc_min": 3,
1772                  "lloc_max": 3,
1773                  "blank_min": 0,
1774                  "blank_max": 0
1775                }
1776                "#
1777                );
1778            },
1779        );
1780
1781        // LLOC returns three because there is an empty Rust statement
1782        check_metrics::<RustParser>(
1783            "let a = 42;
1784             if true {
1785                42
1786             } else {
1787                43
1788             };",
1789            "foo.rs",
1790            |metric| {
1791                // Spaces: 1
1792                insta::assert_json_snapshot!(
1793                    metric.loc,
1794                    @r#"
1795                {
1796                  "sloc": 6,
1797                  "ploc": 6,
1798                  "lloc": 3,
1799                  "cloc": 0,
1800                  "blank": 0,
1801                  "sloc_average": 6.0,
1802                  "ploc_average": 6.0,
1803                  "lloc_average": 3.0,
1804                  "cloc_average": 0.0,
1805                  "blank_average": 0.0,
1806                  "sloc_min": 6,
1807                  "sloc_max": 6,
1808                  "cloc_min": 0,
1809                  "cloc_max": 0,
1810                  "ploc_min": 6,
1811                  "ploc_max": 6,
1812                  "lloc_min": 3,
1813                  "lloc_max": 3,
1814                  "blank_min": 0,
1815                  "blank_max": 0
1816                }
1817                "#
1818                );
1819            },
1820        );
1821    }
1822
1823    #[test]
1824    fn c_lloc() {
1825        check_metrics::<CParser>(
1826            "for (;;)
1827                break;",
1828            "foo.c",
1829            |metric| {
1830                // Spaces: 1
1831                insta::assert_json_snapshot!(
1832                    metric.loc,
1833                    @r#"
1834                {
1835                  "sloc": 2,
1836                  "ploc": 2,
1837                  "lloc": 2,
1838                  "cloc": 0,
1839                  "blank": 0,
1840                  "sloc_average": 2.0,
1841                  "ploc_average": 2.0,
1842                  "lloc_average": 2.0,
1843                  "cloc_average": 0.0,
1844                  "blank_average": 0.0,
1845                  "sloc_min": 2,
1846                  "sloc_max": 2,
1847                  "cloc_min": 0,
1848                  "cloc_max": 0,
1849                  "ploc_min": 2,
1850                  "ploc_max": 2,
1851                  "lloc_min": 2,
1852                  "lloc_max": 2,
1853                  "blank_min": 0,
1854                  "blank_max": 0
1855                }
1856                "#
1857                );
1858            },
1859        );
1860    }
1861
1862    #[test]
1863    fn cpp_lloc() {
1864        check_metrics::<CppParser>(
1865            "nsTArray<xpcGCCallback> callbacks(extraGCCallbacks.Clone());
1866             for (uint32_t i = 0; i < callbacks.Length(); ++i) {
1867                 callbacks[i](status);
1868             }",
1869            "foo.cpp",
1870            |metric| {
1871                // Spaces: 1
1872                // lloc: nsTArray, for, callbacks
1873                insta::assert_json_snapshot!(
1874                    metric.loc,
1875                    @r#"
1876                {
1877                  "sloc": 4,
1878                  "ploc": 4,
1879                  "lloc": 3,
1880                  "cloc": 0,
1881                  "blank": 0,
1882                  "sloc_average": 4.0,
1883                  "ploc_average": 4.0,
1884                  "lloc_average": 3.0,
1885                  "cloc_average": 0.0,
1886                  "blank_average": 0.0,
1887                  "sloc_min": 4,
1888                  "sloc_max": 4,
1889                  "cloc_min": 0,
1890                  "cloc_max": 0,
1891                  "ploc_min": 4,
1892                  "ploc_max": 4,
1893                  "lloc_min": 3,
1894                  "lloc_max": 3,
1895                  "blank_min": 0,
1896                  "blank_max": 0
1897                }
1898                "#
1899                );
1900            },
1901        );
1902    }
1903
1904    #[test]
1905    fn cpp_return_lloc() {
1906        check_metrics::<CppParser>(
1907            "uint8_t* pixel_data = frame.GetFrameDataAtPos(DesktopVector(x, y));
1908             return RgbaColor(pixel_data) == blank_pixel_;",
1909            "foo.cpp",
1910            |metric| {
1911                // Spaces: 1
1912                // lloc: pixel_data, return
1913                insta::assert_json_snapshot!(
1914                    metric.loc,
1915                    @r#"
1916                {
1917                  "sloc": 2,
1918                  "ploc": 2,
1919                  "lloc": 2,
1920                  "cloc": 0,
1921                  "blank": 0,
1922                  "sloc_average": 2.0,
1923                  "ploc_average": 2.0,
1924                  "lloc_average": 2.0,
1925                  "cloc_average": 0.0,
1926                  "blank_average": 0.0,
1927                  "sloc_min": 2,
1928                  "sloc_max": 2,
1929                  "cloc_min": 0,
1930                  "cloc_max": 0,
1931                  "ploc_min": 2,
1932                  "ploc_max": 2,
1933                  "lloc_min": 2,
1934                  "lloc_max": 2,
1935                  "blank_min": 0,
1936                  "blank_max": 0
1937                }
1938                "#
1939                );
1940            },
1941        );
1942    }
1943
1944    #[test]
1945    fn cpp_for_lloc() {
1946        check_metrics::<CppParser>(
1947            "for (; start != end; ++start) {
1948                 const unsigned char idx = *start;
1949                 if (idx > 127 || !kValidTokenMap[idx]) return false;
1950             }",
1951            "foo.cpp",
1952            |metric| {
1953                // Spaces: 1
1954                // lloc: for, idx, if, return
1955                insta::assert_json_snapshot!(
1956                    metric.loc,
1957                    @r#"
1958                {
1959                  "sloc": 4,
1960                  "ploc": 4,
1961                  "lloc": 4,
1962                  "cloc": 0,
1963                  "blank": 0,
1964                  "sloc_average": 4.0,
1965                  "ploc_average": 4.0,
1966                  "lloc_average": 4.0,
1967                  "cloc_average": 0.0,
1968                  "blank_average": 0.0,
1969                  "sloc_min": 4,
1970                  "sloc_max": 4,
1971                  "cloc_min": 0,
1972                  "cloc_max": 0,
1973                  "ploc_min": 4,
1974                  "ploc_max": 4,
1975                  "lloc_min": 4,
1976                  "lloc_max": 4,
1977                  "blank_min": 0,
1978                  "blank_max": 0
1979                }
1980                "#
1981                );
1982            },
1983        );
1984    }
1985
1986    #[test]
1987    fn cpp_while_lloc() {
1988        check_metrics::<CppParser>(
1989            "while (sHeapAtoms) {
1990                 HttpHeapAtom* next = sHeapAtoms->next;
1991                 free(sHeapAtoms);
1992            }",
1993            "foo.cpp",
1994            |metric| {
1995                // Spaces: 1
1996                // lloc: while, next, free
1997                insta::assert_json_snapshot!(
1998                    metric.loc,
1999                    @r#"
2000                {
2001                  "sloc": 4,
2002                  "ploc": 4,
2003                  "lloc": 3,
2004                  "cloc": 0,
2005                  "blank": 0,
2006                  "sloc_average": 4.0,
2007                  "ploc_average": 4.0,
2008                  "lloc_average": 3.0,
2009                  "cloc_average": 0.0,
2010                  "blank_average": 0.0,
2011                  "sloc_min": 4,
2012                  "sloc_max": 4,
2013                  "cloc_min": 0,
2014                  "cloc_max": 0,
2015                  "ploc_min": 4,
2016                  "ploc_max": 4,
2017                  "lloc_min": 3,
2018                  "lloc_max": 3,
2019                  "blank_min": 0,
2020                  "blank_max": 0
2021                }
2022                "#
2023                );
2024            },
2025        );
2026    }
2027
2028    #[test]
2029    fn python_string_on_new_line() {
2030        // More lines of the same instruction were counted as blank lines
2031        check_metrics::<PythonParser>(
2032            "capabilities[\"goog:chromeOptions\"][\"androidPackage\"] = \\
2033                \"org.chromium.weblayer.shell\"",
2034            "foo.py",
2035            |metric| {
2036                // Spaces: 1
2037                insta::assert_json_snapshot!(
2038                    metric.loc,
2039                    @r#"
2040                {
2041                  "sloc": 2,
2042                  "ploc": 2,
2043                  "lloc": 1,
2044                  "cloc": 0,
2045                  "blank": 0,
2046                  "sloc_average": 2.0,
2047                  "ploc_average": 2.0,
2048                  "lloc_average": 1.0,
2049                  "cloc_average": 0.0,
2050                  "blank_average": 0.0,
2051                  "sloc_min": 2,
2052                  "sloc_max": 2,
2053                  "cloc_min": 0,
2054                  "cloc_max": 0,
2055                  "ploc_min": 2,
2056                  "ploc_max": 2,
2057                  "lloc_min": 1,
2058                  "lloc_max": 1,
2059                  "blank_min": 0,
2060                  "blank_max": 0
2061                }
2062                "#
2063                );
2064            },
2065        );
2066    }
2067
2068    #[test]
2069    fn python_multiline_string_assignment_ploc() {
2070        // Regression test for issue #415: interior rows of a multi-line,
2071        // non-docstring string literal were mis-counted as blank lines.
2072        check_metrics::<PythonParser>(
2073            "QUERY = \"\"\"
2074SELECT id, name
2075FROM users
2076WHERE active = 1
2077ORDER BY name
2078\"\"\"",
2079            "foo.py",
2080            |metric| {
2081                // Spaces: 1. Six physical lines, all code, no blanks.
2082                assert_eq!(metric.loc.sloc(), 6);
2083                assert_eq!(metric.loc.ploc(), 6);
2084                assert_eq!(metric.loc.cloc(), 0);
2085                assert_eq!(metric.loc.blank(), 0);
2086                insta::assert_json_snapshot!(
2087                    metric.loc,
2088                    @r#"
2089                {
2090                  "sloc": 6,
2091                  "ploc": 6,
2092                  "lloc": 1,
2093                  "cloc": 0,
2094                  "blank": 0,
2095                  "sloc_average": 6.0,
2096                  "ploc_average": 6.0,
2097                  "lloc_average": 1.0,
2098                  "cloc_average": 0.0,
2099                  "blank_average": 0.0,
2100                  "sloc_min": 6,
2101                  "sloc_max": 6,
2102                  "cloc_min": 0,
2103                  "cloc_max": 0,
2104                  "ploc_min": 6,
2105                  "ploc_max": 6,
2106                  "lloc_min": 1,
2107                  "lloc_max": 1,
2108                  "blank_min": 0,
2109                  "blank_max": 0
2110                }
2111                "#
2112                );
2113            },
2114        );
2115    }
2116
2117    #[test]
2118    fn python_multiline_string_argument_ploc() {
2119        // Regression test for issue #415: a multi-line string passed as a call
2120        // argument must have all its rows counted as code, not blank.
2121        check_metrics::<PythonParser>(
2122            "print(\"\"\"
2123line one
2124line two
2125\"\"\")",
2126            "foo.py",
2127            |metric| {
2128                // Spaces: 1. Four physical lines, all code, no blanks.
2129                assert_eq!(metric.loc.sloc(), 4);
2130                assert_eq!(metric.loc.ploc(), 4);
2131                assert_eq!(metric.loc.cloc(), 0);
2132                assert_eq!(metric.loc.blank(), 0);
2133            },
2134        );
2135    }
2136
2137    #[test]
2138    fn python_single_line_string_assignment_ploc() {
2139        // Single-line, non-docstring string: behaviour must be unchanged by
2140        // the issue #415 fix (start == end means no extra rows are inserted).
2141        check_metrics::<PythonParser>("QUERY = \"SELECT 1\"", "foo.py", |metric| {
2142            // Spaces: 1.
2143            assert_eq!(metric.loc.sloc(), 1);
2144            assert_eq!(metric.loc.ploc(), 1);
2145            assert_eq!(metric.loc.cloc(), 0);
2146            assert_eq!(metric.loc.blank(), 0);
2147        });
2148    }
2149
2150    #[test]
2151    fn python_multiline_docstring_still_cloc() {
2152        // The fix for issue #415 must leave docstring classification unchanged:
2153        // a bare triple-quoted string statement is still counted as comments.
2154        check_metrics::<PythonParser>(
2155            "def f():
2156    \"\"\"Docstring line one
2157    Docstring line two
2158    \"\"\"
2159    return 1",
2160            "foo.py",
2161            |metric| {
2162                // Spaces: 2 (module + function). The three docstring rows are
2163                // comment lines, not code or blank lines.
2164                assert_eq!(metric.loc.cloc(), 3);
2165                assert_eq!(metric.loc.blank(), 0);
2166            },
2167        );
2168    }
2169
2170    #[test]
2171    fn rust_no_field_expression_lloc() {
2172        check_metrics::<RustParser>(
2173            "struct Foo {
2174                field: usize,
2175             }
2176             let foo = Foo { 42 };
2177             foo.field;",
2178            "foo.rs",
2179            |metric| {
2180                // Spaces: 1
2181                insta::assert_json_snapshot!(
2182                    metric.loc,
2183                    @r#"
2184                {
2185                  "sloc": 5,
2186                  "ploc": 5,
2187                  "lloc": 2,
2188                  "cloc": 0,
2189                  "blank": 0,
2190                  "sloc_average": 5.0,
2191                  "ploc_average": 5.0,
2192                  "lloc_average": 2.0,
2193                  "cloc_average": 0.0,
2194                  "blank_average": 0.0,
2195                  "sloc_min": 5,
2196                  "sloc_max": 5,
2197                  "cloc_min": 0,
2198                  "cloc_max": 0,
2199                  "ploc_min": 5,
2200                  "ploc_max": 5,
2201                  "lloc_min": 2,
2202                  "lloc_max": 2,
2203                  "blank_min": 0,
2204                  "blank_max": 0
2205                }
2206                "#
2207                );
2208            },
2209        );
2210    }
2211
2212    #[test]
2213    fn rust_no_parenthesized_expression_lloc() {
2214        check_metrics::<RustParser>("let a = (42 + 0);", "foo.rs", |metric| {
2215            // Spaces: 1
2216            insta::assert_json_snapshot!(
2217                metric.loc,
2218                @r#"
2219            {
2220              "sloc": 1,
2221              "ploc": 1,
2222              "lloc": 1,
2223              "cloc": 0,
2224              "blank": 0,
2225              "sloc_average": 1.0,
2226              "ploc_average": 1.0,
2227              "lloc_average": 1.0,
2228              "cloc_average": 0.0,
2229              "blank_average": 0.0,
2230              "sloc_min": 1,
2231              "sloc_max": 1,
2232              "cloc_min": 0,
2233              "cloc_max": 0,
2234              "ploc_min": 1,
2235              "ploc_max": 1,
2236              "lloc_min": 1,
2237              "lloc_max": 1,
2238              "blank_min": 0,
2239              "blank_max": 0
2240            }
2241            "#
2242            );
2243        });
2244    }
2245
2246    #[test]
2247    fn rust_no_array_expression_lloc() {
2248        check_metrics::<RustParser>("let a = [0; 42];", "foo.rs", |metric| {
2249            // Spaces: 1
2250            insta::assert_json_snapshot!(
2251                metric.loc,
2252                @r#"
2253            {
2254              "sloc": 1,
2255              "ploc": 1,
2256              "lloc": 1,
2257              "cloc": 0,
2258              "blank": 0,
2259              "sloc_average": 1.0,
2260              "ploc_average": 1.0,
2261              "lloc_average": 1.0,
2262              "cloc_average": 0.0,
2263              "blank_average": 0.0,
2264              "sloc_min": 1,
2265              "sloc_max": 1,
2266              "cloc_min": 0,
2267              "cloc_max": 0,
2268              "ploc_min": 1,
2269              "ploc_max": 1,
2270              "lloc_min": 1,
2271              "lloc_max": 1,
2272              "blank_min": 0,
2273              "blank_max": 0
2274            }
2275            "#
2276            );
2277        });
2278    }
2279
2280    #[test]
2281    fn rust_no_tuple_expression_lloc() {
2282        check_metrics::<RustParser>("let a = (0, 42);", "foo.rs", |metric| {
2283            // Spaces: 1
2284            insta::assert_json_snapshot!(
2285                metric.loc,
2286                @r#"
2287            {
2288              "sloc": 1,
2289              "ploc": 1,
2290              "lloc": 1,
2291              "cloc": 0,
2292              "blank": 0,
2293              "sloc_average": 1.0,
2294              "ploc_average": 1.0,
2295              "lloc_average": 1.0,
2296              "cloc_average": 0.0,
2297              "blank_average": 0.0,
2298              "sloc_min": 1,
2299              "sloc_max": 1,
2300              "cloc_min": 0,
2301              "cloc_max": 0,
2302              "ploc_min": 1,
2303              "ploc_max": 1,
2304              "lloc_min": 1,
2305              "lloc_max": 1,
2306              "blank_min": 0,
2307              "blank_max": 0
2308            }
2309            "#
2310            );
2311        });
2312    }
2313
2314    #[test]
2315    fn rust_no_unit_expression_lloc() {
2316        check_metrics::<RustParser>("let a = ();", "foo.rs", |metric| {
2317            // Spaces: 1
2318            insta::assert_json_snapshot!(
2319                metric.loc,
2320                @r#"
2321            {
2322              "sloc": 1,
2323              "ploc": 1,
2324              "lloc": 1,
2325              "cloc": 0,
2326              "blank": 0,
2327              "sloc_average": 1.0,
2328              "ploc_average": 1.0,
2329              "lloc_average": 1.0,
2330              "cloc_average": 0.0,
2331              "blank_average": 0.0,
2332              "sloc_min": 1,
2333              "sloc_max": 1,
2334              "cloc_min": 0,
2335              "cloc_max": 0,
2336              "ploc_min": 1,
2337              "ploc_max": 1,
2338              "lloc_min": 1,
2339              "lloc_max": 1,
2340              "blank_min": 0,
2341              "blank_max": 0
2342            }
2343            "#
2344            );
2345        });
2346    }
2347
2348    #[test]
2349    fn rust_call_function_lloc() {
2350        check_metrics::<RustParser>(
2351            "let a = foo(); // +1
2352             foo(); // +1
2353             k!(foo()); // +1",
2354            "foo.rs",
2355            |metric| {
2356                // Spaces: 1
2357                insta::assert_json_snapshot!(
2358                    metric.loc,
2359                    @r#"
2360                {
2361                  "sloc": 3,
2362                  "ploc": 3,
2363                  "lloc": 3,
2364                  "cloc": 3,
2365                  "blank": 0,
2366                  "sloc_average": 3.0,
2367                  "ploc_average": 3.0,
2368                  "lloc_average": 3.0,
2369                  "cloc_average": 3.0,
2370                  "blank_average": 0.0,
2371                  "sloc_min": 3,
2372                  "sloc_max": 3,
2373                  "cloc_min": 3,
2374                  "cloc_max": 3,
2375                  "ploc_min": 3,
2376                  "ploc_max": 3,
2377                  "lloc_min": 3,
2378                  "lloc_max": 3,
2379                  "blank_min": 0,
2380                  "blank_max": 0
2381                }
2382                "#
2383                );
2384            },
2385        );
2386    }
2387
2388    #[test]
2389    fn rust_macro_invocation_lloc() {
2390        check_metrics::<RustParser>(
2391            "let a = foo!(); // +1
2392             foo!(); // +1
2393             k(foo!()); // +1",
2394            "foo.rs",
2395            |metric| {
2396                // Spaces: 1
2397                insta::assert_json_snapshot!(
2398                    metric.loc,
2399                    @r#"
2400                {
2401                  "sloc": 3,
2402                  "ploc": 3,
2403                  "lloc": 3,
2404                  "cloc": 3,
2405                  "blank": 0,
2406                  "sloc_average": 3.0,
2407                  "ploc_average": 3.0,
2408                  "lloc_average": 3.0,
2409                  "cloc_average": 3.0,
2410                  "blank_average": 0.0,
2411                  "sloc_min": 3,
2412                  "sloc_max": 3,
2413                  "cloc_min": 3,
2414                  "cloc_max": 3,
2415                  "ploc_min": 3,
2416                  "ploc_max": 3,
2417                  "lloc_min": 3,
2418                  "lloc_max": 3,
2419                  "blank_min": 0,
2420                  "blank_max": 0
2421                }
2422                "#
2423                );
2424            },
2425        );
2426    }
2427
2428    #[test]
2429    fn rust_function_in_loop_lloc() {
2430        check_metrics::<RustParser>(
2431            "for (a, b) in c.iter().enumerate() {} // +1
2432             while (a, b) in c.iter().enumerate() {} // +1
2433             while let Some(a) = c.strip_prefix(\"hi\") {} // +1",
2434            "foo.rs",
2435            |metric| {
2436                // Spaces: 1
2437                insta::assert_json_snapshot!(
2438                    metric.loc,
2439                    @r#"
2440                {
2441                  "sloc": 3,
2442                  "ploc": 3,
2443                  "lloc": 3,
2444                  "cloc": 3,
2445                  "blank": 0,
2446                  "sloc_average": 3.0,
2447                  "ploc_average": 3.0,
2448                  "lloc_average": 3.0,
2449                  "cloc_average": 3.0,
2450                  "blank_average": 0.0,
2451                  "sloc_min": 3,
2452                  "sloc_max": 3,
2453                  "cloc_min": 3,
2454                  "cloc_max": 3,
2455                  "ploc_min": 3,
2456                  "ploc_max": 3,
2457                  "lloc_min": 3,
2458                  "lloc_max": 3,
2459                  "blank_min": 0,
2460                  "blank_max": 0
2461                }
2462                "#
2463                );
2464            },
2465        );
2466    }
2467
2468    #[test]
2469    fn rust_function_in_if_lloc() {
2470        check_metrics::<RustParser>(
2471            "if foo() {} // +1
2472             if let Some(a) = foo() {} // +1",
2473            "foo.rs",
2474            |metric| {
2475                // Spaces: 1
2476                insta::assert_json_snapshot!(
2477                    metric.loc,
2478                    @r#"
2479                {
2480                  "sloc": 2,
2481                  "ploc": 2,
2482                  "lloc": 2,
2483                  "cloc": 2,
2484                  "blank": 0,
2485                  "sloc_average": 2.0,
2486                  "ploc_average": 2.0,
2487                  "lloc_average": 2.0,
2488                  "cloc_average": 2.0,
2489                  "blank_average": 0.0,
2490                  "sloc_min": 2,
2491                  "sloc_max": 2,
2492                  "cloc_min": 2,
2493                  "cloc_max": 2,
2494                  "ploc_min": 2,
2495                  "ploc_max": 2,
2496                  "lloc_min": 2,
2497                  "lloc_max": 2,
2498                  "blank_min": 0,
2499                  "blank_max": 0
2500                }
2501                "#
2502                );
2503            },
2504        );
2505    }
2506
2507    #[test]
2508    fn rust_function_in_return_lloc() {
2509        check_metrics::<RustParser>(
2510            "return foo();
2511             await foo();",
2512            "foo.rs",
2513            |metric| {
2514                // Spaces: 1
2515                insta::assert_json_snapshot!(
2516                    metric.loc,
2517                    @r#"
2518                {
2519                  "sloc": 2,
2520                  "ploc": 2,
2521                  "lloc": 2,
2522                  "cloc": 0,
2523                  "blank": 0,
2524                  "sloc_average": 2.0,
2525                  "ploc_average": 2.0,
2526                  "lloc_average": 2.0,
2527                  "cloc_average": 0.0,
2528                  "blank_average": 0.0,
2529                  "sloc_min": 2,
2530                  "sloc_max": 2,
2531                  "cloc_min": 0,
2532                  "cloc_max": 0,
2533                  "ploc_min": 2,
2534                  "ploc_max": 2,
2535                  "lloc_min": 2,
2536                  "lloc_max": 2,
2537                  "blank_min": 0,
2538                  "blank_max": 0
2539                }
2540                "#
2541                );
2542            },
2543        );
2544    }
2545
2546    #[test]
2547    fn rust_closure_expression_lloc() {
2548        check_metrics::<RustParser>(
2549            "let a = |i: i32| -> i32 { i + 1 }; // +1
2550             a(42); // +1
2551             k(b.iter().map(|n| n.parse.ok().unwrap_or(42))); // +1",
2552            "foo.rs",
2553            |metric| {
2554                // Spaces: 3
2555                insta::assert_json_snapshot!(
2556                    metric.loc,
2557                    @r#"
2558                {
2559                  "sloc": 3,
2560                  "ploc": 3,
2561                  "lloc": 3,
2562                  "cloc": 3,
2563                  "blank": 0,
2564                  "sloc_average": 1.0,
2565                  "ploc_average": 1.0,
2566                  "lloc_average": 1.0,
2567                  "cloc_average": 1.0,
2568                  "blank_average": 0.0,
2569                  "sloc_min": 1,
2570                  "sloc_max": 3,
2571                  "cloc_min": 0,
2572                  "cloc_max": 3,
2573                  "ploc_min": 1,
2574                  "ploc_max": 3,
2575                  "lloc_min": 0,
2576                  "lloc_max": 3,
2577                  "blank_min": 0,
2578                  "blank_max": 0
2579                }
2580                "#
2581                );
2582            },
2583        );
2584    }
2585
2586    #[test]
2587    fn python_general_loc() {
2588        check_metrics::<PythonParser>(
2589            "def func(a,
2590                      b,
2591                      c):
2592                 print(a)
2593                 print(b)
2594                 print(c)",
2595            "foo.py",
2596            |metric| {
2597                // Spaces: 2
2598                insta::assert_json_snapshot!(
2599                    metric.loc,
2600                    @r#"
2601                {
2602                  "sloc": 6,
2603                  "ploc": 6,
2604                  "lloc": 3,
2605                  "cloc": 0,
2606                  "blank": 0,
2607                  "sloc_average": 3.0,
2608                  "ploc_average": 3.0,
2609                  "lloc_average": 1.5,
2610                  "cloc_average": 0.0,
2611                  "blank_average": 0.0,
2612                  "sloc_min": 6,
2613                  "sloc_max": 6,
2614                  "cloc_min": 0,
2615                  "cloc_max": 0,
2616                  "ploc_min": 6,
2617                  "ploc_max": 6,
2618                  "lloc_min": 3,
2619                  "lloc_max": 3,
2620                  "blank_min": 0,
2621                  "blank_max": 0
2622                }
2623                "#
2624                );
2625            },
2626        );
2627    }
2628
2629    #[test]
2630    fn python_real_loc() {
2631        check_metrics::<PythonParser>(
2632            "def web_socket_transfer_data(request):
2633                while True:
2634                    line = request.ws_stream.receive_message()
2635                    if line is None:
2636                        return
2637                    code, reason = line.split(' ', 1)
2638                    if code is None or reason is None:
2639                        return
2640                    request.ws_stream.close_connection(int(code), reason)
2641                    # close_connection() initiates closing handshake. It validates code
2642                    # and reason. If you want to send a broken close frame for a test,
2643                    # following code will be useful.
2644                    # > data = struct.pack('!H', int(code)) + reason.encode('UTF-8')
2645                    # > request.connection.write(stream.create_close_frame(data))
2646                    # > # Suppress to re-respond client responding close frame.
2647                    # > raise Exception(\"customized server initiated closing handshake\")",
2648            "foo.py",
2649            |metric| {
2650                // Spaces: 2
2651                insta::assert_json_snapshot!(
2652                    metric.loc,
2653                    @r#"
2654                {
2655                  "sloc": 16,
2656                  "ploc": 9,
2657                  "lloc": 8,
2658                  "cloc": 7,
2659                  "blank": 0,
2660                  "sloc_average": 8.0,
2661                  "ploc_average": 4.5,
2662                  "lloc_average": 4.0,
2663                  "cloc_average": 3.5,
2664                  "blank_average": 0.0,
2665                  "sloc_min": 16,
2666                  "sloc_max": 16,
2667                  "cloc_min": 7,
2668                  "cloc_max": 7,
2669                  "ploc_min": 9,
2670                  "ploc_max": 9,
2671                  "lloc_min": 8,
2672                  "lloc_max": 8,
2673                  "blank_min": 0,
2674                  "blank_max": 0
2675                }
2676                "#
2677                );
2678            },
2679        );
2680    }
2681
2682    #[test]
2683    fn javascript_real_loc() {
2684        check_metrics::<JavascriptParser>(
2685            "assert.throws(Test262Error, function() {
2686               for (let { poisoned: x = ++initEvalCount } = poisonedProperty; ; ) {
2687                 return;
2688               }
2689             });",
2690            "foo.js",
2691            |metric| {
2692                // Spaces: 2
2693                insta::assert_json_snapshot!(
2694                    metric.loc,
2695                    @r#"
2696                {
2697                  "sloc": 5,
2698                  "ploc": 5,
2699                  "lloc": 4,
2700                  "cloc": 0,
2701                  "blank": 0,
2702                  "sloc_average": 2.5,
2703                  "ploc_average": 2.5,
2704                  "lloc_average": 2.0,
2705                  "cloc_average": 0.0,
2706                  "blank_average": 0.0,
2707                  "sloc_min": 5,
2708                  "sloc_max": 5,
2709                  "cloc_min": 0,
2710                  "cloc_max": 0,
2711                  "ploc_min": 5,
2712                  "ploc_max": 5,
2713                  "lloc_min": 3,
2714                  "lloc_max": 4,
2715                  "blank_min": 0,
2716                  "blank_max": 0
2717                }
2718                "#
2719                );
2720            },
2721        );
2722    }
2723
2724    #[test]
2725    fn mozjs_real_loc() {
2726        check_metrics::<MozjsParser>(
2727            "assert.throws(Test262Error, function() {
2728               for (let { poisoned: x = ++initEvalCount } = poisonedProperty; ; ) {
2729                 return;
2730               }
2731             });",
2732            "foo.js",
2733            |metric| {
2734                // Spaces: 2
2735                insta::assert_json_snapshot!(
2736                    metric.loc,
2737                    @r#"
2738                {
2739                  "sloc": 5,
2740                  "ploc": 5,
2741                  "lloc": 4,
2742                  "cloc": 0,
2743                  "blank": 0,
2744                  "sloc_average": 2.5,
2745                  "ploc_average": 2.5,
2746                  "lloc_average": 2.0,
2747                  "cloc_average": 0.0,
2748                  "blank_average": 0.0,
2749                  "sloc_min": 5,
2750                  "sloc_max": 5,
2751                  "cloc_min": 0,
2752                  "cloc_max": 0,
2753                  "ploc_min": 5,
2754                  "ploc_max": 5,
2755                  "lloc_min": 3,
2756                  "lloc_max": 4,
2757                  "blank_min": 0,
2758                  "blank_max": 0
2759                }
2760                "#
2761                );
2762            },
2763        );
2764    }
2765
2766    #[test]
2767    fn mozjs_blank_and_comment_loc() {
2768        check_metrics::<MozjsParser>(
2769            "// a comment
2770             function f() {
2771
2772                 var x = 1;
2773
2774             }",
2775            "foo.js",
2776            |metric| {
2777                insta::assert_json_snapshot!(
2778                    metric.loc,
2779                    @r#"
2780                {
2781                  "sloc": 6,
2782                  "ploc": 3,
2783                  "lloc": 1,
2784                  "cloc": 1,
2785                  "blank": 2,
2786                  "sloc_average": 3.0,
2787                  "ploc_average": 1.5,
2788                  "lloc_average": 0.5,
2789                  "cloc_average": 0.5,
2790                  "blank_average": 1.0,
2791                  "sloc_min": 5,
2792                  "sloc_max": 6,
2793                  "cloc_min": 0,
2794                  "cloc_max": 1,
2795                  "ploc_min": 3,
2796                  "ploc_max": 3,
2797                  "lloc_min": 1,
2798                  "lloc_max": 1,
2799                  "blank_min": 2,
2800                  "blank_max": 2
2801                }
2802                "#
2803                );
2804            },
2805        );
2806    }
2807
2808    #[test]
2809    fn cpp_namespace_loc() {
2810        check_metrics::<CppParser>(
2811            "namespace mozilla::dom::quota {} // namespace mozilla::dom::quota",
2812            "foo.cpp",
2813            |metric| {
2814                // Spaces: 2
2815                insta::assert_json_snapshot!(
2816                    metric.loc,
2817                    @r#"
2818                {
2819                  "sloc": 1,
2820                  "ploc": 1,
2821                  "lloc": 0,
2822                  "cloc": 1,
2823                  "blank": 0,
2824                  "sloc_average": 0.5,
2825                  "ploc_average": 0.5,
2826                  "lloc_average": 0.0,
2827                  "cloc_average": 0.5,
2828                  "blank_average": 0.0,
2829                  "sloc_min": 1,
2830                  "sloc_max": 1,
2831                  "cloc_min": 0,
2832                  "cloc_max": 1,
2833                  "ploc_min": 1,
2834                  "ploc_max": 1,
2835                  "lloc_min": 0,
2836                  "lloc_max": 0,
2837                  "blank_min": 0,
2838                  "blank_max": 0
2839                }
2840                "#
2841                );
2842            },
2843        );
2844    }
2845
2846    #[test]
2847    fn java_comments() {
2848        check_metrics::<JavaParser>(
2849            "for (int i = 0; i < 100; i++) { \
2850               // Print hello
2851               System.out.println(\"hello\"); \
2852               // Print world
2853               System.out.println(\"hello\"); \
2854             }",
2855            "foo.java",
2856            |metric| {
2857                // Spaces: 1
2858                insta::assert_json_snapshot!(
2859                    metric.loc,
2860                    @r#"
2861                {
2862                  "sloc": 3,
2863                  "ploc": 3,
2864                  "lloc": 3,
2865                  "cloc": 2,
2866                  "blank": 0,
2867                  "sloc_average": 3.0,
2868                  "ploc_average": 3.0,
2869                  "lloc_average": 3.0,
2870                  "cloc_average": 2.0,
2871                  "blank_average": 0.0,
2872                  "sloc_min": 3,
2873                  "sloc_max": 3,
2874                  "cloc_min": 2,
2875                  "cloc_max": 2,
2876                  "ploc_min": 3,
2877                  "ploc_max": 3,
2878                  "lloc_min": 3,
2879                  "lloc_max": 3,
2880                  "blank_min": 0,
2881                  "blank_max": 0
2882                }
2883                "#
2884                );
2885            },
2886        );
2887    }
2888
2889    #[test]
2890    fn java_blank() {
2891        check_metrics::<JavaParser>(
2892            "int x = 1;
2893
2894
2895            int y = 2;",
2896            "foo.java",
2897            |metric| {
2898                // Spaces: 1
2899                insta::assert_json_snapshot!(
2900                    metric.loc,
2901                    @r#"
2902                {
2903                  "sloc": 4,
2904                  "ploc": 2,
2905                  "lloc": 2,
2906                  "cloc": 0,
2907                  "blank": 2,
2908                  "sloc_average": 4.0,
2909                  "ploc_average": 2.0,
2910                  "lloc_average": 2.0,
2911                  "cloc_average": 0.0,
2912                  "blank_average": 2.0,
2913                  "sloc_min": 4,
2914                  "sloc_max": 4,
2915                  "cloc_min": 0,
2916                  "cloc_max": 0,
2917                  "ploc_min": 2,
2918                  "ploc_max": 2,
2919                  "lloc_min": 2,
2920                  "lloc_max": 2,
2921                  "blank_min": 2,
2922                  "blank_max": 2
2923                }
2924                "#
2925                );
2926            },
2927        );
2928    }
2929
2930    #[test]
2931    fn java_sloc() {
2932        check_metrics::<JavaParser>(
2933            "for (int i = 0; i < 100; i++) {
2934               System.out.println(i);
2935             }",
2936            "foo.java",
2937            |metric| {
2938                // Spaces: 1
2939                insta::assert_json_snapshot!(
2940                    metric.loc,
2941                    @r#"
2942                {
2943                  "sloc": 3,
2944                  "ploc": 3,
2945                  "lloc": 2,
2946                  "cloc": 0,
2947                  "blank": 0,
2948                  "sloc_average": 3.0,
2949                  "ploc_average": 3.0,
2950                  "lloc_average": 2.0,
2951                  "cloc_average": 0.0,
2952                  "blank_average": 0.0,
2953                  "sloc_min": 3,
2954                  "sloc_max": 3,
2955                  "cloc_min": 0,
2956                  "cloc_max": 0,
2957                  "ploc_min": 3,
2958                  "ploc_max": 3,
2959                  "lloc_min": 2,
2960                  "lloc_max": 2,
2961                  "blank_min": 0,
2962                  "blank_max": 0
2963                }
2964                "#
2965                );
2966            },
2967        );
2968    }
2969
2970    #[test]
2971    fn java_module_sloc() {
2972        check_metrics::<JavaParser>(
2973            "module helloworld{
2974              exports com.test;
2975            }",
2976            "foo.java",
2977            |metric| {
2978                // Spaces: 1
2979                insta::assert_json_snapshot!(
2980                    metric.loc,
2981                    @r#"
2982                {
2983                  "sloc": 3,
2984                  "ploc": 3,
2985                  "lloc": 0,
2986                  "cloc": 0,
2987                  "blank": 0,
2988                  "sloc_average": 3.0,
2989                  "ploc_average": 3.0,
2990                  "lloc_average": 0.0,
2991                  "cloc_average": 0.0,
2992                  "blank_average": 0.0,
2993                  "sloc_min": 3,
2994                  "sloc_max": 3,
2995                  "cloc_min": 0,
2996                  "cloc_max": 0,
2997                  "ploc_min": 3,
2998                  "ploc_max": 3,
2999                  "lloc_min": 0,
3000                  "lloc_max": 0,
3001                  "blank_min": 0,
3002                  "blank_max": 0
3003                }
3004                "#
3005                );
3006            },
3007        );
3008    }
3009
3010    #[test]
3011    fn java_single_ploc() {
3012        check_metrics::<JavaParser>("int x = 1;", "foo.java", |metric| {
3013            // Spaces: 1
3014            insta::assert_json_snapshot!(
3015                metric.loc,
3016                @r#"
3017            {
3018              "sloc": 1,
3019              "ploc": 1,
3020              "lloc": 1,
3021              "cloc": 0,
3022              "blank": 0,
3023              "sloc_average": 1.0,
3024              "ploc_average": 1.0,
3025              "lloc_average": 1.0,
3026              "cloc_average": 0.0,
3027              "blank_average": 0.0,
3028              "sloc_min": 1,
3029              "sloc_max": 1,
3030              "cloc_min": 0,
3031              "cloc_max": 0,
3032              "ploc_min": 1,
3033              "ploc_max": 1,
3034              "lloc_min": 1,
3035              "lloc_max": 1,
3036              "blank_min": 0,
3037              "blank_max": 0
3038            }
3039            "#
3040            );
3041        });
3042    }
3043
3044    #[test]
3045    fn java_simple_ploc() {
3046        check_metrics::<JavaParser>(
3047            "for (int i = 0; i < 100; i = i++) {
3048               System.out.println(i);
3049             }",
3050            "foo.java",
3051            |metric| {
3052                // Spaces: 1
3053                insta::assert_json_snapshot!(
3054                    metric.loc,
3055                    @r#"
3056                {
3057                  "sloc": 3,
3058                  "ploc": 3,
3059                  "lloc": 2,
3060                  "cloc": 0,
3061                  "blank": 0,
3062                  "sloc_average": 3.0,
3063                  "ploc_average": 3.0,
3064                  "lloc_average": 2.0,
3065                  "cloc_average": 0.0,
3066                  "blank_average": 0.0,
3067                  "sloc_min": 3,
3068                  "sloc_max": 3,
3069                  "cloc_min": 0,
3070                  "cloc_max": 0,
3071                  "ploc_min": 3,
3072                  "ploc_max": 3,
3073                  "lloc_min": 2,
3074                  "lloc_max": 2,
3075                  "blank_min": 0,
3076                  "blank_max": 0
3077                }
3078                "#
3079                );
3080            },
3081        );
3082    }
3083
3084    #[test]
3085    fn java_multi_ploc() {
3086        check_metrics::<JavaParser>(
3087            "int x = 1;
3088            for (int i = 0; i < 100; i++) {
3089               System.out.println(i);
3090             }",
3091            "foo.java",
3092            |metric| {
3093                // Spaces: 1
3094                insta::assert_json_snapshot!(
3095                    metric.loc,
3096                    @r#"
3097                {
3098                  "sloc": 4,
3099                  "ploc": 4,
3100                  "lloc": 3,
3101                  "cloc": 0,
3102                  "blank": 0,
3103                  "sloc_average": 4.0,
3104                  "ploc_average": 4.0,
3105                  "lloc_average": 3.0,
3106                  "cloc_average": 0.0,
3107                  "blank_average": 0.0,
3108                  "sloc_min": 4,
3109                  "sloc_max": 4,
3110                  "cloc_min": 0,
3111                  "cloc_max": 0,
3112                  "ploc_min": 4,
3113                  "ploc_max": 4,
3114                  "lloc_min": 3,
3115                  "lloc_max": 3,
3116                  "blank_min": 0,
3117                  "blank_max": 0
3118                }
3119                "#
3120                );
3121            },
3122        );
3123    }
3124
3125    #[test]
3126    fn java_single_statement_lloc() {
3127        check_metrics::<JavaParser>("int max = 10;", "foo.java", |metric| {
3128            // Spaces: 1
3129            insta::assert_json_snapshot!(
3130                metric.loc,
3131                @r#"
3132            {
3133              "sloc": 1,
3134              "ploc": 1,
3135              "lloc": 1,
3136              "cloc": 0,
3137              "blank": 0,
3138              "sloc_average": 1.0,
3139              "ploc_average": 1.0,
3140              "lloc_average": 1.0,
3141              "cloc_average": 0.0,
3142              "blank_average": 0.0,
3143              "sloc_min": 1,
3144              "sloc_max": 1,
3145              "cloc_min": 0,
3146              "cloc_max": 0,
3147              "ploc_min": 1,
3148              "ploc_max": 1,
3149              "lloc_min": 1,
3150              "lloc_max": 1,
3151              "blank_min": 0,
3152              "blank_max": 0
3153            }
3154            "#
3155            );
3156        });
3157    }
3158
3159    #[test]
3160    fn java_for_lloc() {
3161        check_metrics::<JavaParser>(
3162            "for (int i = 0; i < 100; i++) { // + 1
3163               System.out.println(i); // + 1
3164             }",
3165            "foo.java",
3166            |metric| {
3167                // Spaces: 1
3168                insta::assert_json_snapshot!(
3169                    metric.loc,
3170                    @r#"
3171                {
3172                  "sloc": 3,
3173                  "ploc": 3,
3174                  "lloc": 2,
3175                  "cloc": 2,
3176                  "blank": 0,
3177                  "sloc_average": 3.0,
3178                  "ploc_average": 3.0,
3179                  "lloc_average": 2.0,
3180                  "cloc_average": 2.0,
3181                  "blank_average": 0.0,
3182                  "sloc_min": 3,
3183                  "sloc_max": 3,
3184                  "cloc_min": 2,
3185                  "cloc_max": 2,
3186                  "ploc_min": 3,
3187                  "ploc_max": 3,
3188                  "lloc_min": 2,
3189                  "lloc_max": 2,
3190                  "blank_min": 0,
3191                  "blank_max": 0
3192                }
3193                "#
3194                );
3195            },
3196        );
3197    }
3198
3199    #[test]
3200    fn java_foreach_lloc() {
3201        check_metrics::<JavaParser>(
3202            "
3203            int arr[]={12,13,14,44}; // +1
3204            for (int i:arr) { // +1
3205               System.out.println(i); // +1
3206             }",
3207            "foo.java",
3208            |metric| {
3209                // Spaces: 1
3210                insta::assert_json_snapshot!(
3211                    metric.loc,
3212                    @r#"
3213                {
3214                  "sloc": 4,
3215                  "ploc": 4,
3216                  "lloc": 3,
3217                  "cloc": 3,
3218                  "blank": 0,
3219                  "sloc_average": 4.0,
3220                  "ploc_average": 4.0,
3221                  "lloc_average": 3.0,
3222                  "cloc_average": 3.0,
3223                  "blank_average": 0.0,
3224                  "sloc_min": 4,
3225                  "sloc_max": 4,
3226                  "cloc_min": 3,
3227                  "cloc_max": 3,
3228                  "ploc_min": 4,
3229                  "ploc_max": 4,
3230                  "lloc_min": 3,
3231                  "lloc_max": 3,
3232                  "blank_min": 0,
3233                  "blank_max": 0
3234                }
3235                "#
3236                );
3237            },
3238        );
3239    }
3240
3241    #[test]
3242    fn java_while_lloc() {
3243        check_metrics::<JavaParser>(
3244            "
3245            int i=0; // +1
3246            while(i < 10) { // +1
3247                i++; // +1
3248                System.out.println(i); // +1
3249             }",
3250            "foo.java",
3251            |metric| {
3252                // Spaces: 1
3253                insta::assert_json_snapshot!(
3254                    metric.loc,
3255                    @r#"
3256                {
3257                  "sloc": 5,
3258                  "ploc": 5,
3259                  "lloc": 4,
3260                  "cloc": 4,
3261                  "blank": 0,
3262                  "sloc_average": 5.0,
3263                  "ploc_average": 5.0,
3264                  "lloc_average": 4.0,
3265                  "cloc_average": 4.0,
3266                  "blank_average": 0.0,
3267                  "sloc_min": 5,
3268                  "sloc_max": 5,
3269                  "cloc_min": 4,
3270                  "cloc_max": 4,
3271                  "ploc_min": 5,
3272                  "ploc_max": 5,
3273                  "lloc_min": 4,
3274                  "lloc_max": 4,
3275                  "blank_min": 0,
3276                  "blank_max": 0
3277                }
3278                "#
3279                );
3280            },
3281        );
3282    }
3283
3284    #[test]
3285    fn java_do_while_lloc() {
3286        check_metrics::<JavaParser>(
3287            "
3288            int i=0; // +1
3289            do { // +1
3290                i++; // +1
3291                System.out.println(i); // +1
3292             } while(i < 10)",
3293            "foo.java",
3294            |metric| {
3295                // Spaces: 1
3296                insta::assert_json_snapshot!(
3297                    metric.loc,
3298                    @r#"
3299                {
3300                  "sloc": 5,
3301                  "ploc": 5,
3302                  "lloc": 4,
3303                  "cloc": 4,
3304                  "blank": 0,
3305                  "sloc_average": 5.0,
3306                  "ploc_average": 5.0,
3307                  "lloc_average": 4.0,
3308                  "cloc_average": 4.0,
3309                  "blank_average": 0.0,
3310                  "sloc_min": 5,
3311                  "sloc_max": 5,
3312                  "cloc_min": 4,
3313                  "cloc_max": 4,
3314                  "ploc_min": 5,
3315                  "ploc_max": 5,
3316                  "lloc_min": 4,
3317                  "lloc_max": 4,
3318                  "blank_min": 0,
3319                  "blank_max": 0
3320                }
3321                "#
3322                );
3323            },
3324        );
3325    }
3326
3327    #[test]
3328    fn java_switch_lloc() {
3329        check_metrics::<JavaParser>(
3330            "switch(grade) { // +1
3331                case 'A' :
3332                   System.out.println(\"Pass with distinction\"); // +1
3333                   break; // +1
3334                case 'B' :
3335                case 'C' :
3336                   System.out.println(\"Pass\"); // +1
3337                   break; // +1
3338                case 'D' :
3339                   System.out.println(\"At risk\"); // +1
3340                case 'F' :
3341                   System.out.println(\"Fail\"); // +1
3342                   break; // +1
3343                default :
3344                   System.out.println(\"Invalid grade\"); // +1
3345             }",
3346            "foo.java",
3347            |metric| {
3348                // Spaces: 1
3349                insta::assert_json_snapshot!(
3350                    metric.loc,
3351                    @r#"
3352                {
3353                  "sloc": 16,
3354                  "ploc": 16,
3355                  "lloc": 9,
3356                  "cloc": 9,
3357                  "blank": 0,
3358                  "sloc_average": 16.0,
3359                  "ploc_average": 16.0,
3360                  "lloc_average": 9.0,
3361                  "cloc_average": 9.0,
3362                  "blank_average": 0.0,
3363                  "sloc_min": 16,
3364                  "sloc_max": 16,
3365                  "cloc_min": 9,
3366                  "cloc_max": 9,
3367                  "ploc_min": 16,
3368                  "ploc_max": 16,
3369                  "lloc_min": 9,
3370                  "lloc_max": 9,
3371                  "blank_min": 0,
3372                  "blank_max": 0
3373                }
3374                "#
3375                );
3376            },
3377        );
3378    }
3379
3380    #[test]
3381    fn java_continue_lloc() {
3382        check_metrics::<JavaParser>(
3383            "int max = 10; // +1
3384
3385            for (int i = 0; i < max; i++) { // +1
3386                if(i % 2 == 0) { continue;} + 2
3387                System.out.println(i); // +1
3388             }",
3389            "foo.java",
3390            |metric| {
3391                // Spaces: 1
3392                insta::assert_json_snapshot!(
3393                    metric.loc,
3394                    @r#"
3395                {
3396                  "sloc": 6,
3397                  "ploc": 5,
3398                  "lloc": 5,
3399                  "cloc": 3,
3400                  "blank": 1,
3401                  "sloc_average": 6.0,
3402                  "ploc_average": 5.0,
3403                  "lloc_average": 5.0,
3404                  "cloc_average": 3.0,
3405                  "blank_average": 1.0,
3406                  "sloc_min": 6,
3407                  "sloc_max": 6,
3408                  "cloc_min": 3,
3409                  "cloc_max": 3,
3410                  "ploc_min": 5,
3411                  "ploc_max": 5,
3412                  "lloc_min": 5,
3413                  "lloc_max": 5,
3414                  "blank_min": 1,
3415                  "blank_max": 1
3416                }
3417                "#
3418                );
3419            },
3420        );
3421    }
3422
3423    #[test]
3424    fn java_try_lloc() {
3425        check_metrics::<JavaParser>(
3426            "try { // +1
3427                int[] myNumbers = {1, 2, 3}; // +1
3428                System.out.println(myNumbers[10]); // +1
3429              } catch (Exception e) {
3430                System.out.println(e.getMessage()); // +1
3431                throw e; // +1
3432              }",
3433            "foo.java",
3434            |metric| {
3435                // Spaces: 1
3436                insta::assert_json_snapshot!(
3437                    metric.loc,
3438                    @r#"
3439                {
3440                  "sloc": 7,
3441                  "ploc": 7,
3442                  "lloc": 5,
3443                  "cloc": 5,
3444                  "blank": 0,
3445                  "sloc_average": 7.0,
3446                  "ploc_average": 7.0,
3447                  "lloc_average": 5.0,
3448                  "cloc_average": 5.0,
3449                  "blank_average": 0.0,
3450                  "sloc_min": 7,
3451                  "sloc_max": 7,
3452                  "cloc_min": 5,
3453                  "cloc_max": 5,
3454                  "ploc_min": 7,
3455                  "ploc_max": 7,
3456                  "lloc_min": 5,
3457                  "lloc_max": 5,
3458                  "blank_min": 0,
3459                  "blank_max": 0
3460                }
3461                "#
3462                );
3463            },
3464        );
3465    }
3466
3467    #[test]
3468    fn java_class_loc() {
3469        check_metrics::<JavaParser>(
3470            "
3471            public class Person {
3472              private String name;
3473              public Person(String name){
3474                this.name = name; // +1
3475              }
3476              public String getName() {
3477                return name; // +1
3478              }
3479            }",
3480            "foo.java",
3481            |metric| {
3482                // Spaces: 4
3483                insta::assert_json_snapshot!(
3484                    metric.loc,
3485                    @r#"
3486                {
3487                  "sloc": 9,
3488                  "ploc": 9,
3489                  "lloc": 2,
3490                  "cloc": 2,
3491                  "blank": 0,
3492                  "sloc_average": 2.25,
3493                  "ploc_average": 2.25,
3494                  "lloc_average": 0.5,
3495                  "cloc_average": 0.5,
3496                  "blank_average": 0.0,
3497                  "sloc_min": 3,
3498                  "sloc_max": 9,
3499                  "cloc_min": 1,
3500                  "cloc_max": 2,
3501                  "ploc_min": 3,
3502                  "ploc_max": 9,
3503                  "lloc_min": 1,
3504                  "lloc_max": 2,
3505                  "blank_min": 0,
3506                  "blank_max": 0
3507                }
3508                "#
3509                );
3510            },
3511        );
3512    }
3513
3514    #[test]
3515    fn java_expressions_lloc() {
3516        check_metrics::<JavaParser>(
3517            "int x = 10;                                                            // +1 local var declaration
3518            x=+89;                                                                  // +1 expression statement
3519            int y = x * 2;                                                          // +1 local var declaration
3520            IntFunction double = (n) -> n*2;                                        // +1 local var declaration
3521            int y2 = double(x);                                                     // +1 local var declaration
3522            System.out.println(\"double \" + x + \" = \" + y2);                     // +1 expression statement
3523            String message = (x % 2) == 0 ? \"Evenly done.\" : \"Oddly done.\";     // +1 local var declaration
3524            Object done = (Runnable) () -> { System.out.println(\"Done!\"); };      // +2 local var declaration + expression statement
3525            String s = \"string\";                                                  // +1 local var declaration
3526            boolean isS = (s instanceof String);                                    // +1 local var declaration
3527            done.run();                                                             // +1 expression statement
3528            ",
3529            "foo.java",
3530            |metric| {
3531                // Spaces: 1
3532                insta::assert_json_snapshot!(
3533                    metric.loc,
3534                    @r#"
3535                {
3536                  "sloc": 11,
3537                  "ploc": 11,
3538                  "lloc": 12,
3539                  "cloc": 11,
3540                  "blank": 0,
3541                  "sloc_average": 11.0,
3542                  "ploc_average": 11.0,
3543                  "lloc_average": 12.0,
3544                  "cloc_average": 11.0,
3545                  "blank_average": 0.0,
3546                  "sloc_min": 11,
3547                  "sloc_max": 11,
3548                  "cloc_min": 11,
3549                  "cloc_max": 11,
3550                  "ploc_min": 11,
3551                  "ploc_max": 11,
3552                  "lloc_min": 12,
3553                  "lloc_max": 12,
3554                  "blank_min": 0,
3555                  "blank_max": 0
3556                }
3557                "#
3558                );
3559            },
3560        );
3561    }
3562
3563    #[test]
3564    fn java_statement_inline_loc() {
3565        check_metrics::<JavaParser>(
3566            "for (int i = 0; i < 100; i++) { System.out.println(\"hello\"); }",
3567            "foo.java",
3568            |metric| {
3569                // Spaces: 1
3570                insta::assert_json_snapshot!(
3571                    metric.loc,
3572                    @r#"
3573                {
3574                  "sloc": 1,
3575                  "ploc": 1,
3576                  "lloc": 2,
3577                  "cloc": 0,
3578                  "blank": 0,
3579                  "sloc_average": 1.0,
3580                  "ploc_average": 1.0,
3581                  "lloc_average": 2.0,
3582                  "cloc_average": 0.0,
3583                  "blank_average": 0.0,
3584                  "sloc_min": 1,
3585                  "sloc_max": 1,
3586                  "cloc_min": 0,
3587                  "cloc_max": 0,
3588                  "ploc_min": 1,
3589                  "ploc_max": 1,
3590                  "lloc_min": 2,
3591                  "lloc_max": 2,
3592                  "blank_min": 0,
3593                  "blank_max": 0
3594                }
3595                "#
3596                );
3597            },
3598        );
3599    }
3600
3601    #[test]
3602    fn java_general_loc() {
3603        check_metrics::<JavaParser>(
3604            "int max = 100;
3605
3606            /*
3607              Loop through and print
3608                from: 0
3609                to: max
3610            */
3611            for (int i = 0; i < max; i++) {
3612               // Print the value
3613               System.out.println(i);
3614             }",
3615            "foo.java",
3616            |metric| {
3617                // Spaces: 1
3618                insta::assert_json_snapshot!(
3619                    metric.loc,
3620                    @r#"
3621                {
3622                  "sloc": 11,
3623                  "ploc": 4,
3624                  "lloc": 3,
3625                  "cloc": 6,
3626                  "blank": 1,
3627                  "sloc_average": 11.0,
3628                  "ploc_average": 4.0,
3629                  "lloc_average": 3.0,
3630                  "cloc_average": 6.0,
3631                  "blank_average": 1.0,
3632                  "sloc_min": 11,
3633                  "sloc_max": 11,
3634                  "cloc_min": 6,
3635                  "cloc_max": 6,
3636                  "ploc_min": 4,
3637                  "ploc_max": 4,
3638                  "lloc_min": 3,
3639                  "lloc_max": 3,
3640                  "blank_min": 1,
3641                  "blank_max": 1
3642                }
3643                "#
3644                );
3645            },
3646        );
3647    }
3648
3649    #[test]
3650    fn java_main_class_loc() {
3651        check_metrics::<JavaParser>(
3652            "package com.company;
3653             /**
3654             * The HelloWorldApp class implements an application that
3655             * simply prints \"Hello World!\" to standard output.
3656             */
3657
3658            class HelloWorldApp {
3659              public void main(String[] args) {
3660                String message = args.length == 0 ? \"Hello empty world\" : \"Hello world\"; // +1 lloc : 1 var assignment
3661                System.out.println(message); // Display the string. +1 lloc
3662              }
3663            }",
3664            "foo.java",
3665            |metric| {
3666                // Spaces: 3
3667                insta::assert_json_snapshot!(
3668                    metric.loc,
3669                    @r#"
3670                {
3671                  "sloc": 12,
3672                  "ploc": 7,
3673                  "lloc": 2,
3674                  "cloc": 6,
3675                  "blank": 1,
3676                  "sloc_average": 4.0,
3677                  "ploc_average": 2.3333333333333335,
3678                  "lloc_average": 0.6666666666666666,
3679                  "cloc_average": 2.0,
3680                  "blank_average": 0.3333333333333333,
3681                  "sloc_min": 4,
3682                  "sloc_max": 12,
3683                  "cloc_min": 2,
3684                  "cloc_max": 6,
3685                  "ploc_min": 4,
3686                  "ploc_max": 7,
3687                  "lloc_min": 2,
3688                  "lloc_max": 2,
3689                  "blank_min": 0,
3690                  "blank_max": 1
3691                }
3692                "#
3693                );
3694            },
3695        );
3696    }
3697
3698    #[test]
3699    fn go_general_loc() {
3700        check_metrics::<GoParser>(
3701            "package main
3702
3703            // entrypoint
3704            func main() {
3705                /* loop body */
3706                for i := 0; i < 10; i++ {
3707                    fmt.Println(i)
3708                }
3709            }",
3710            "foo.go",
3711            |metric| {
3712                // Spaces: 2 (unit + main).
3713                // lloc: for_statement (+1), fmt.Println expression (+1).
3714                //       `i := 0` and `i++` inside the for-clause are gated.
3715                // cloc: 2 comments (line + block).
3716                insta::assert_json_snapshot!(
3717                    metric.loc,
3718                    @r#"
3719                {
3720                  "sloc": 9,
3721                  "ploc": 6,
3722                  "lloc": 2,
3723                  "cloc": 2,
3724                  "blank": 1,
3725                  "sloc_average": 4.5,
3726                  "ploc_average": 3.0,
3727                  "lloc_average": 1.0,
3728                  "cloc_average": 1.0,
3729                  "blank_average": 0.5,
3730                  "sloc_min": 6,
3731                  "sloc_max": 9,
3732                  "cloc_min": 1,
3733                  "cloc_max": 2,
3734                  "ploc_min": 5,
3735                  "ploc_max": 6,
3736                  "lloc_min": 2,
3737                  "lloc_max": 2,
3738                  "blank_min": 0,
3739                  "blank_max": 1
3740                }
3741                "#
3742                );
3743            },
3744        );
3745    }
3746
3747    #[test]
3748    fn go_for_clause_does_not_double_count_lloc() {
3749        // Bare `for` body has only a return; the `for_statement` itself is the
3750        // single logical line. Confirms ShortVarDeclaration in a for-clause
3751        // does not add an extra lloc.
3752        check_metrics::<GoParser>(
3753            "package main
3754            func f(n int) int {
3755                for i := 0; i < n; i++ {
3756                    return i
3757                }
3758                return 0
3759            }",
3760            "foo.go",
3761            |metric| {
3762                // Expected lloc: for (+1), return (+1), return (+1) = 3.
3763                // Without the gate, ShortVarDeclaration would add an extra (+1).
3764                assert_eq!(metric.loc.lloc(), 3);
3765            },
3766        );
3767    }
3768
3769    #[test]
3770    fn go_blank() {
3771        check_metrics::<GoParser>(
3772            "package main
3773
3774            func foo() {
3775                x := 1
3776
3777                y := 2
3778            }",
3779            "foo.go",
3780            |metric| {
3781                // Spaces: 2 (unit + foo).
3782                // blank: 2 (lines 2 and 5 are empty).
3783                insta::assert_json_snapshot!(
3784                    metric.loc,
3785                    @r#"
3786                {
3787                  "sloc": 7,
3788                  "ploc": 5,
3789                  "lloc": 2,
3790                  "cloc": 0,
3791                  "blank": 2,
3792                  "sloc_average": 3.5,
3793                  "ploc_average": 2.5,
3794                  "lloc_average": 1.0,
3795                  "cloc_average": 0.0,
3796                  "blank_average": 1.0,
3797                  "sloc_min": 5,
3798                  "sloc_max": 7,
3799                  "cloc_min": 0,
3800                  "cloc_max": 0,
3801                  "ploc_min": 4,
3802                  "ploc_max": 5,
3803                  "lloc_min": 2,
3804                  "lloc_max": 2,
3805                  "blank_min": 1,
3806                  "blank_max": 2
3807                }
3808                "#
3809                );
3810            },
3811        );
3812    }
3813
3814    #[test]
3815    fn go_cloc_line_comments() {
3816        check_metrics::<GoParser>(
3817            "package main
3818
3819            // helper adds two numbers.
3820            // It returns their sum.
3821            func add(a, b int) int {
3822                // compute the result
3823                return a + b
3824            }",
3825            "foo.go",
3826            |metric| {
3827                // Spaces: 2 (unit + add).
3828                // cloc: 3 lines with `//` comments.
3829                insta::assert_json_snapshot!(
3830                    metric.loc,
3831                    @r#"
3832                {
3833                  "sloc": 8,
3834                  "ploc": 4,
3835                  "lloc": 1,
3836                  "cloc": 3,
3837                  "blank": 1,
3838                  "sloc_average": 4.0,
3839                  "ploc_average": 2.0,
3840                  "lloc_average": 0.5,
3841                  "cloc_average": 1.5,
3842                  "blank_average": 0.5,
3843                  "sloc_min": 4,
3844                  "sloc_max": 8,
3845                  "cloc_min": 1,
3846                  "cloc_max": 3,
3847                  "ploc_min": 3,
3848                  "ploc_max": 4,
3849                  "lloc_min": 1,
3850                  "lloc_max": 1,
3851                  "blank_min": 0,
3852                  "blank_max": 1
3853                }
3854                "#
3855                );
3856            },
3857        );
3858    }
3859
3860    #[test]
3861    fn go_cloc_block_comments() {
3862        check_metrics::<GoParser>(
3863            "package main
3864
3865            /* block comment
3866               spanning two lines */
3867            func foo() {
3868                x := 1 /* inline block */
3869            }",
3870            "foo.go",
3871            |metric| {
3872                // Spaces: 2 (unit + foo).
3873                // cloc: 2-line block comment + inline block = 3 comment lines.
3874                insta::assert_json_snapshot!(
3875                    metric.loc,
3876                    @r#"
3877                {
3878                  "sloc": 7,
3879                  "ploc": 4,
3880                  "lloc": 1,
3881                  "cloc": 3,
3882                  "blank": 1,
3883                  "sloc_average": 3.5,
3884                  "ploc_average": 2.0,
3885                  "lloc_average": 0.5,
3886                  "cloc_average": 1.5,
3887                  "blank_average": 0.5,
3888                  "sloc_min": 3,
3889                  "sloc_max": 7,
3890                  "cloc_min": 1,
3891                  "cloc_max": 3,
3892                  "ploc_min": 3,
3893                  "ploc_max": 4,
3894                  "lloc_min": 1,
3895                  "lloc_max": 1,
3896                  "blank_min": 0,
3897                  "blank_max": 1
3898                }
3899                "#
3900                );
3901            },
3902        );
3903    }
3904
3905    #[test]
3906    fn go_lloc_if_for_switch() {
3907        check_metrics::<GoParser>(
3908            "package main
3909
3910            func foo(n int) int {
3911                if n > 0 {
3912                    for i := 0; i < n; i++ {
3913                        switch i {
3914                        }
3915                    }
3916                }
3917                return n
3918            }",
3919            "foo.go",
3920            |metric| {
3921                // Spaces: 2 (unit + foo).
3922                // lloc: if (+1), for (+1), switch (+1), return (+1) = 4.
3923                insta::assert_json_snapshot!(
3924                    metric.loc,
3925                    @r#"
3926                {
3927                  "sloc": 11,
3928                  "ploc": 10,
3929                  "lloc": 4,
3930                  "cloc": 0,
3931                  "blank": 1,
3932                  "sloc_average": 5.5,
3933                  "ploc_average": 5.0,
3934                  "lloc_average": 2.0,
3935                  "cloc_average": 0.0,
3936                  "blank_average": 0.5,
3937                  "sloc_min": 9,
3938                  "sloc_max": 11,
3939                  "cloc_min": 0,
3940                  "cloc_max": 0,
3941                  "ploc_min": 9,
3942                  "ploc_max": 10,
3943                  "lloc_min": 4,
3944                  "lloc_max": 4,
3945                  "blank_min": 0,
3946                  "blank_max": 1
3947                }
3948                "#
3949                );
3950            },
3951        );
3952    }
3953
3954    #[test]
3955    fn go_lloc_go_defer() {
3956        check_metrics::<GoParser>(
3957            "package main
3958
3959            func foo() {
3960                go run()
3961                defer cleanup()
3962            }",
3963            "foo.go",
3964            |metric| {
3965                // Spaces: 2 (unit + foo).
3966                // lloc: go (+1), defer (+1) = 2.
3967                insta::assert_json_snapshot!(
3968                    metric.loc,
3969                    @r#"
3970                {
3971                  "sloc": 6,
3972                  "ploc": 5,
3973                  "lloc": 2,
3974                  "cloc": 0,
3975                  "blank": 1,
3976                  "sloc_average": 3.0,
3977                  "ploc_average": 2.5,
3978                  "lloc_average": 1.0,
3979                  "cloc_average": 0.0,
3980                  "blank_average": 0.5,
3981                  "sloc_min": 4,
3982                  "sloc_max": 6,
3983                  "cloc_min": 0,
3984                  "cloc_max": 0,
3985                  "ploc_min": 4,
3986                  "ploc_max": 5,
3987                  "lloc_min": 2,
3988                  "lloc_max": 2,
3989                  "blank_min": 0,
3990                  "blank_max": 1
3991                }
3992                "#
3993                );
3994            },
3995        );
3996    }
3997
3998    #[test]
3999    fn go_lloc_var_const_declarations() {
4000        check_metrics::<GoParser>(
4001            "package main
4002
4003            func foo() {
4004                var x int
4005                var y = 10
4006                const z = 42
4007                a := 3
4008                a = 4
4009            }",
4010            "foo.go",
4011            |metric| {
4012                // Spaces: 2 (unit + foo).
4013                // lloc: var (+1), var (+1), const (+1),
4014                //       short_var_decl (+1), assignment (+1) = 5.
4015                insta::assert_json_snapshot!(
4016                    metric.loc,
4017                    @r#"
4018                {
4019                  "sloc": 9,
4020                  "ploc": 8,
4021                  "lloc": 5,
4022                  "cloc": 0,
4023                  "blank": 1,
4024                  "sloc_average": 4.5,
4025                  "ploc_average": 4.0,
4026                  "lloc_average": 2.5,
4027                  "cloc_average": 0.0,
4028                  "blank_average": 0.5,
4029                  "sloc_min": 7,
4030                  "sloc_max": 9,
4031                  "cloc_min": 0,
4032                  "cloc_max": 0,
4033                  "ploc_min": 7,
4034                  "ploc_max": 8,
4035                  "lloc_min": 5,
4036                  "lloc_max": 5,
4037                  "blank_min": 0,
4038                  "blank_max": 1
4039                }
4040                "#
4041                );
4042            },
4043        );
4044    }
4045
4046    #[test]
4047    fn go_lloc_select() {
4048        check_metrics::<GoParser>(
4049            "package main
4050
4051            func foo(ch chan int) {
4052                select {
4053                case v := <-ch:
4054                    _ = v
4055                }
4056            }",
4057            "foo.go",
4058            |metric| {
4059                // Spaces: 2 (unit + foo).
4060                // lloc: select (+1), assignment `_ = v` (+1) = 2.
4061                // `case v := <-ch:` is a receive_statement inside a
4062                // communication_case, not a ShortVarDeclaration.
4063                insta::assert_json_snapshot!(
4064                    metric.loc,
4065                    @r#"
4066                {
4067                  "sloc": 8,
4068                  "ploc": 7,
4069                  "lloc": 2,
4070                  "cloc": 0,
4071                  "blank": 1,
4072                  "sloc_average": 4.0,
4073                  "ploc_average": 3.5,
4074                  "lloc_average": 1.0,
4075                  "cloc_average": 0.0,
4076                  "blank_average": 0.5,
4077                  "sloc_min": 6,
4078                  "sloc_max": 8,
4079                  "cloc_min": 0,
4080                  "cloc_max": 0,
4081                  "ploc_min": 6,
4082                  "ploc_max": 7,
4083                  "lloc_min": 2,
4084                  "lloc_max": 2,
4085                  "blank_min": 0,
4086                  "blank_max": 1
4087                }
4088                "#
4089                );
4090            },
4091        );
4092    }
4093
4094    #[test]
4095    fn go_sloc_multiline_function() {
4096        check_metrics::<GoParser>(
4097            "package main
4098
4099            func add(
4100                a int,
4101                b int,
4102            ) int {
4103                return a + b
4104            }",
4105            "foo.go",
4106            |metric| {
4107                // Spaces: 2 (unit + add).
4108                // The multi-line signature should count each line as sloc.
4109                insta::assert_json_snapshot!(
4110                    metric.loc,
4111                    @r#"
4112                {
4113                  "sloc": 8,
4114                  "ploc": 7,
4115                  "lloc": 1,
4116                  "cloc": 0,
4117                  "blank": 1,
4118                  "sloc_average": 4.0,
4119                  "ploc_average": 3.5,
4120                  "lloc_average": 0.5,
4121                  "cloc_average": 0.0,
4122                  "blank_average": 0.5,
4123                  "sloc_min": 6,
4124                  "sloc_max": 8,
4125                  "cloc_min": 0,
4126                  "cloc_max": 0,
4127                  "ploc_min": 6,
4128                  "ploc_max": 7,
4129                  "lloc_min": 1,
4130                  "lloc_max": 1,
4131                  "blank_min": 0,
4132                  "blank_max": 1
4133                }
4134                "#
4135                );
4136            },
4137        );
4138    }
4139
4140    #[test]
4141    fn go_code_comment_same_line() {
4142        check_metrics::<GoParser>(
4143            "package main
4144
4145            func foo() {
4146                x := 1 // initialize x
4147                y := 2 // initialize y
4148            }",
4149            "foo.go",
4150            |metric| {
4151                // Spaces: 2 (unit + foo).
4152                // cloc: 2 (inline comments on code lines).
4153                // blank: 1 (line between package and func).
4154                // The code+comment lines should count for both ploc and cloc.
4155                insta::assert_json_snapshot!(
4156                    metric.loc,
4157                    @r#"
4158                {
4159                  "sloc": 6,
4160                  "ploc": 5,
4161                  "lloc": 2,
4162                  "cloc": 2,
4163                  "blank": 1,
4164                  "sloc_average": 3.0,
4165                  "ploc_average": 2.5,
4166                  "lloc_average": 1.0,
4167                  "cloc_average": 1.0,
4168                  "blank_average": 0.5,
4169                  "sloc_min": 4,
4170                  "sloc_max": 6,
4171                  "cloc_min": 2,
4172                  "cloc_max": 2,
4173                  "ploc_min": 4,
4174                  "ploc_max": 5,
4175                  "lloc_min": 2,
4176                  "lloc_max": 2,
4177                  "blank_min": 0,
4178                  "blank_max": 1
4179                }
4180                "#
4181                );
4182            },
4183        );
4184    }
4185
4186    #[test]
4187    fn perl_grammar_smoke() {
4188        // Pin the contract that tree-sitter-perl 1.1.2 cleanly parses every
4189        // Perl construct exercised by the rest of the `perl_*` test suite.
4190        // If a future grammar bump turns one of these into an error tree,
4191        // the metric assertions might still pass numerically by coincidence;
4192        // this test fails loudly instead.
4193        assert_perl_parses_cleanly(
4194            "use strict;
4195use warnings;
4196
4197# line comment
4198
4199=pod
4200multi-line POD
4201=cut
4202
4203sub factorial {
4204    my ($n) = @_;
4205    return 1 if $n <= 1;
4206    return $n * factorial($n - 1);
4207}
4208
4209my @arr = (1, 2, 3);
4210my %hash = (a => 1, b => 2);
4211my $closure = sub { return $_[0] + 1; };
4212
4213for my $i (1..3) {
4214    if ($i % 2 == 0) {
4215        print \"even\\n\";
4216    } elsif ($i == 1) {
4217        print \"one\\n\";
4218    } else {
4219        print \"odd\\n\";
4220    }
4221}
4222
4223while ($x > 0) {
4224    last if $x == 0;
4225    $x--;
4226}
4227
4228unless ($done) {
4229    next;
4230}
4231
4232my $heredoc = <<END;
4233hello
4234END
4235",
4236        );
4237    }
4238
4239    #[test]
4240    fn perl_blank() {
4241        check_metrics::<PerlParser>(
4242            "
4243
4244my $a = 42;
4245
4246my $b = 43;
4247
4248",
4249            "foo.pl",
4250            |metric| {
4251                insta::assert_json_snapshot!(metric.loc, @r#"
4252                {
4253                  "sloc": 3,
4254                  "ploc": 2,
4255                  "lloc": 2,
4256                  "cloc": 0,
4257                  "blank": 1,
4258                  "sloc_average": 3.0,
4259                  "ploc_average": 2.0,
4260                  "lloc_average": 2.0,
4261                  "cloc_average": 0.0,
4262                  "blank_average": 1.0,
4263                  "sloc_min": 3,
4264                  "sloc_max": 3,
4265                  "cloc_min": 0,
4266                  "cloc_max": 0,
4267                  "ploc_min": 2,
4268                  "ploc_max": 2,
4269                  "lloc_min": 2,
4270                  "lloc_max": 2,
4271                  "blank_min": 1,
4272                  "blank_max": 1
4273                }
4274                "#);
4275            },
4276        );
4277    }
4278
4279    #[test]
4280    fn perl_no_zero_blank() {
4281        // Blank line interleaved with code that carries trailing comments —
4282        // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
4283        check_metrics::<PerlParser>(
4284            "my $a = 1;
4285my $b = 2;
4286
4287my $c = 3; # trailing
4288my $d = 4; # trailing
4289my $e = 5;",
4290            "foo.pl",
4291            |metric| {
4292                assert_eq!(metric.loc.sloc(), 6);
4293                assert_eq!(metric.loc.ploc(), 5);
4294                assert_eq!(metric.loc.cloc(), 2);
4295                assert_eq!(metric.loc.blank(), 1);
4296                insta::assert_json_snapshot!(metric.loc);
4297            },
4298        );
4299    }
4300
4301    #[test]
4302    fn perl_blank_zero_sanity() {
4303        // Sanity check: blank must report 0, never go negative, when the
4304        // input has no blank lines.
4305        check_metrics::<PerlParser>(
4306            "my $a = 1;
4307my $b = 2;",
4308            "foo.pl",
4309            |metric| {
4310                assert_eq!(metric.loc.sloc(), 2);
4311                assert_eq!(metric.loc.ploc(), 2);
4312                assert_eq!(metric.loc.lloc(), 2);
4313                assert_eq!(metric.loc.cloc(), 0);
4314                assert_eq!(metric.loc.blank(), 0);
4315            },
4316        );
4317    }
4318
4319    /// expected: row 0 is comment-only, row 1 is code carrying a trailing
4320    /// comment, row 2 is code — so `ploc 2` and `cloc 2`, with row 1 in
4321    /// both tallies. This pinned `ploc 3` until #1137: the `#` token
4322    /// inside the `comments` node reached the PLOC catch-all, which also
4323    /// reclassified row 0 from comment-only to code-and-comment.
4324    #[test]
4325    fn perl_cloc_line_comments() {
4326        check_metrics::<PerlParser>(
4327            "# top comment
4328my $a = 1; # trailing
4329my $b = 2;",
4330            "foo.pl",
4331            |metric| {
4332                insta::assert_json_snapshot!(metric.loc, @r#"
4333                {
4334                  "sloc": 3,
4335                  "ploc": 2,
4336                  "lloc": 2,
4337                  "cloc": 2,
4338                  "blank": 0,
4339                  "sloc_average": 3.0,
4340                  "ploc_average": 2.0,
4341                  "lloc_average": 2.0,
4342                  "cloc_average": 2.0,
4343                  "blank_average": 0.0,
4344                  "sloc_min": 3,
4345                  "sloc_max": 3,
4346                  "cloc_min": 2,
4347                  "cloc_max": 2,
4348                  "ploc_min": 2,
4349                  "ploc_max": 2,
4350                  "lloc_min": 2,
4351                  "lloc_max": 2,
4352                  "blank_min": 0,
4353                  "blank_max": 0
4354                }
4355                "#);
4356            },
4357        );
4358    }
4359
4360    #[test]
4361    fn perl_cloc_pod_block() {
4362        check_metrics::<PerlParser>(
4363            "my $x = 1;
4364=pod
4365multi-line
4366pod block
4367=cut
4368my $y = 2;",
4369            "foo.pl",
4370            |metric| {
4371                insta::assert_json_snapshot!(metric.loc, @r#"
4372                {
4373                  "sloc": 6,
4374                  "ploc": 2,
4375                  "lloc": 2,
4376                  "cloc": 4,
4377                  "blank": 0,
4378                  "sloc_average": 6.0,
4379                  "ploc_average": 2.0,
4380                  "lloc_average": 2.0,
4381                  "cloc_average": 4.0,
4382                  "blank_average": 0.0,
4383                  "sloc_min": 6,
4384                  "sloc_max": 6,
4385                  "cloc_min": 4,
4386                  "cloc_max": 4,
4387                  "ploc_min": 2,
4388                  "ploc_max": 2,
4389                  "lloc_min": 2,
4390                  "lloc_max": 2,
4391                  "blank_min": 0,
4392                  "blank_max": 0
4393                }
4394                "#);
4395            },
4396        );
4397    }
4398
4399    #[test]
4400    fn perl_lloc_simple_statements() {
4401        check_metrics::<PerlParser>(
4402            "my $a = 1;
4403my $b = 2;
4404my $c = 3;",
4405            "foo.pl",
4406            |metric| {
4407                insta::assert_json_snapshot!(metric.loc, @r#"
4408                {
4409                  "sloc": 3,
4410                  "ploc": 3,
4411                  "lloc": 3,
4412                  "cloc": 0,
4413                  "blank": 0,
4414                  "sloc_average": 3.0,
4415                  "ploc_average": 3.0,
4416                  "lloc_average": 3.0,
4417                  "cloc_average": 0.0,
4418                  "blank_average": 0.0,
4419                  "sloc_min": 3,
4420                  "sloc_max": 3,
4421                  "cloc_min": 0,
4422                  "cloc_max": 0,
4423                  "ploc_min": 3,
4424                  "ploc_max": 3,
4425                  "lloc_min": 3,
4426                  "lloc_max": 3,
4427                  "blank_min": 0,
4428                  "blank_max": 0
4429                }
4430                "#);
4431            },
4432        );
4433    }
4434
4435    #[test]
4436    fn perl_lloc_compound_statements() {
4437        check_metrics::<PerlParser>(
4438            "if ($x) {
4439    print 'a';
4440}
4441while ($n > 0) {
4442    $n--;
4443}",
4444            "foo.pl",
4445            |metric| {
4446                insta::assert_json_snapshot!(metric.loc, @r#"
4447                {
4448                  "sloc": 6,
4449                  "ploc": 6,
4450                  "lloc": 4,
4451                  "cloc": 0,
4452                  "blank": 0,
4453                  "sloc_average": 6.0,
4454                  "ploc_average": 6.0,
4455                  "lloc_average": 4.0,
4456                  "cloc_average": 0.0,
4457                  "blank_average": 0.0,
4458                  "sloc_min": 6,
4459                  "sloc_max": 6,
4460                  "cloc_min": 0,
4461                  "cloc_max": 0,
4462                  "ploc_min": 6,
4463                  "ploc_max": 6,
4464                  "lloc_min": 4,
4465                  "lloc_max": 4,
4466                  "blank_min": 0,
4467                  "blank_max": 0
4468                }
4469                "#);
4470            },
4471        );
4472    }
4473
4474    #[test]
4475    fn perl_lloc_postfix_form_counts_once() {
4476        // `do_thing() if cond;` is one logical line — wrapped in
4477        // single_line_statement; the inner if_simple_statement does not
4478        // add a second LLOC.
4479        check_metrics::<PerlParser>(
4480            "sub f {
4481    return 1 if $_[0];
4482}",
4483            "foo.pl",
4484            |metric| {
4485                assert_eq!(metric.loc.lloc(), 1);
4486            },
4487        );
4488    }
4489
4490    #[test]
4491    fn perl_lloc_use_statement() {
4492        check_metrics::<PerlParser>(
4493            "use strict;
4494use warnings;
4495my $x = 1;",
4496            "foo.pl",
4497            |metric| {
4498                insta::assert_json_snapshot!(metric.loc, @r#"
4499                {
4500                  "sloc": 3,
4501                  "ploc": 3,
4502                  "lloc": 3,
4503                  "cloc": 0,
4504                  "blank": 0,
4505                  "sloc_average": 3.0,
4506                  "ploc_average": 3.0,
4507                  "lloc_average": 3.0,
4508                  "cloc_average": 0.0,
4509                  "blank_average": 0.0,
4510                  "sloc_min": 3,
4511                  "sloc_max": 3,
4512                  "cloc_min": 0,
4513                  "cloc_max": 0,
4514                  "ploc_min": 3,
4515                  "ploc_max": 3,
4516                  "lloc_min": 3,
4517                  "lloc_max": 3,
4518                  "blank_min": 0,
4519                  "blank_max": 0
4520                }
4521                "#);
4522            },
4523        );
4524    }
4525
4526    #[test]
4527    fn perl_lloc_for_loop() {
4528        check_metrics::<PerlParser>(
4529            "for my $i (1..3) {
4530    print $i;
4531}",
4532            "foo.pl",
4533            |metric| {
4534                // `for_statement_2` (+1) and `print …;` SEMI in block (+1) → 2
4535                assert_eq!(metric.loc.lloc(), 2);
4536            },
4537        );
4538    }
4539
4540    #[test]
4541    fn perl_lloc_loop_control_statement() {
4542        check_metrics::<PerlParser>(
4543            "while (1) {
4544    last if $done;
4545}",
4546            "foo.pl",
4547            |metric| {
4548                // while_statement (+1) + loop_control_statement (+1) = 2
4549                assert_eq!(metric.loc.lloc(), 2);
4550            },
4551        );
4552    }
4553
4554    #[test]
4555    fn perl_lloc_no_double_count_inside_single_line_statement() {
4556        // SEMI inside a single_line_statement (postfix form) is a child of
4557        // if_simple_statement, not Block — so it must not add a second LLOC.
4558        check_metrics::<PerlParser>(
4559            "sub f {
4560    print 'a' unless $_[0];
4561}",
4562            "foo.pl",
4563            |metric| {
4564                assert_eq!(metric.loc.lloc(), 1);
4565            },
4566        );
4567    }
4568
4569    #[test]
4570    fn perl_lloc_function_definition_not_counted() {
4571        // `sub f { ... }` itself is a function space, not an LLOC; only its
4572        // body statements count.
4573        check_metrics::<PerlParser>(
4574            "sub f {
4575    my $x = 1;
4576}",
4577            "foo.pl",
4578            |metric| {
4579                assert_eq!(metric.loc.lloc(), 1);
4580            },
4581        );
4582    }
4583
4584    #[test]
4585    fn perl_lloc_anonymous_function() {
4586        // `my $f = sub { return 1; };` — the assignment is one LLOC at the
4587        // top level (the SEMI after `};`); the `return 1;` inside the
4588        // anonymous function block is a second LLOC inside the closure.
4589        check_metrics::<PerlParser>("my $f = sub { return 1; };", "foo.pl", |metric| {
4590            assert_eq!(metric.loc.lloc(), 2);
4591        });
4592    }
4593
4594    #[test]
4595    fn perl_multiline_string_assignment_ploc() {
4596        // Regression test for issue #778: interior rows of a multi-line string
4597        // literal are real code, not blank lines, and must be credited to PLOC
4598        // exactly as Python does (#415). Previously Perl no-op'd its string
4599        // kinds, so row 1 reached neither PLOC nor CLOC and `blank =
4600        // sloc - ploc - cloc` mislabelled it as blank (ploc was 2, blank 1).
4601        // Row 0 holds `my $s = "line1`, row 1 `line2`, row 2 `line3";`.
4602        check_metrics::<PerlParser>(
4603            "my $s = \"line1
4604line2
4605line3\";",
4606            "foo.pl",
4607            |metric| {
4608                // Three physical rows, all code, no blanks — matching Python.
4609                assert_eq!(metric.loc.sloc(), 3);
4610                assert_eq!(metric.loc.ploc(), 3);
4611                assert_eq!(metric.loc.cloc(), 0);
4612                assert_eq!(metric.loc.blank(), 0);
4613            },
4614        );
4615    }
4616
4617    #[test]
4618    fn multiline_string_ploc_consistent_across_languages() {
4619        // Cross-language parity for issue #778: the SAME 3-line string
4620        // assignment must report identical ploc / blank in every language
4621        // that has a multi-line string literal. The canonical value is
4622        // Python's #415 decision: all three rows are code, none are blank.
4623        // `check_metrics` takes a plain `fn(CodeMetrics)`, so the shared
4624        // assertion is a named function rather than a capturing closure and
4625        // must take its argument by value to match that pointer type.
4626        #[allow(clippy::needless_pass_by_value)]
4627        fn assert_three_code_rows(metric: crate::CodeMetrics) {
4628            assert_eq!(metric.loc.sloc(), 3);
4629            assert_eq!(metric.loc.ploc(), 3);
4630            assert_eq!(metric.loc.cloc(), 0);
4631            assert_eq!(metric.loc.blank(), 0);
4632        }
4633        check_metrics::<PythonParser>(
4634            "s = \"\"\"line1\nline2\nline3\"\"\"",
4635            "foo.py",
4636            assert_three_code_rows,
4637        );
4638        check_metrics::<PerlParser>(
4639            "my $s = \"line1\nline2\nline3\";",
4640            "foo.pl",
4641            assert_three_code_rows,
4642        );
4643        check_metrics::<RubyParser>(
4644            "s = \"line1\nline2\nline3\"",
4645            "foo.rb",
4646            assert_three_code_rows,
4647        );
4648        // Go, Kotlin, and Mozilla-C++ reach the same shared
4649        // `add_multiline_string_ploc` helper through their own
4650        // raw-string kinds (`raw_string_literal`,
4651        // `multiline_string_literal`, `raw_string_literal`), and were
4652        // the three call sites of it that no test exercised. Each needs
4653        // its own syntax, so they cannot reuse the quoted form above.
4654        check_metrics::<GoParser>(
4655            "package p\n\nvar s = `line1\nline2\nline3`",
4656            "foo.go",
4657            |metric| {
4658                // Two extra code rows for `package p` and the blank
4659                // between it and the declaration, which Go requires.
4660                assert_eq!(metric.loc.sloc(), 5);
4661                assert_eq!(metric.loc.ploc(), 4);
4662                assert_eq!(metric.loc.cloc(), 0);
4663                assert_eq!(metric.loc.blank(), 1);
4664            },
4665        );
4666        check_metrics::<KotlinParser>(
4667            "val s = \"\"\"line1\nline2\nline3\"\"\"",
4668            "foo.kt",
4669            assert_three_code_rows,
4670        );
4671        check_metrics::<MozcppParser>(
4672            "const char* s = R\"(line1\nline2\nline3)\";",
4673            "foo.cpp",
4674            assert_three_code_rows,
4675        );
4676    }
4677
4678    #[test]
4679    fn perl_lloc_unless_until() {
4680        check_metrics::<PerlParser>(
4681            "unless ($x) {
4682    print 'a';
4683}
4684until ($n == 0) {
4685    $n--;
4686}",
4687            "foo.pl",
4688            |metric| {
4689                // unless_statement (+1) + print SEMI (+1) + until_statement (+1)
4690                // + $n-- SEMI (+1) = 4
4691                assert_eq!(metric.loc.lloc(), 4);
4692            },
4693        );
4694    }
4695
4696    #[test]
4697    fn perl_lloc_heredoc_body_not_counted() {
4698        // Heredoc body content is data, not code: the body lines should not
4699        // contribute LLOC or PLOC.
4700        check_metrics::<PerlParser>(
4701            "my $s = <<END;
4702line1
4703line2
4704END
4705my $x = 1;",
4706            "foo.pl",
4707            |metric| {
4708                // Two top-level statements: the heredoc-using `my $s = …;`
4709                // and `my $x = 1;`.
4710                assert_eq!(metric.loc.lloc(), 2);
4711            },
4712        );
4713        // Independent confirmation that the snippet is a valid heredoc and
4714        // not silently parsed as an error tree (which could otherwise yield
4715        // the same `lloc == 2.0` and mask a grammar regression).
4716        assert_perl_parses_cleanly(
4717            "my $s = <<END;
4718line1
4719line2
4720END
4721my $x = 1;",
4722        );
4723    }
4724
4725    #[test]
4726    fn perl_lloc_package_and_require() {
4727        check_metrics::<PerlParser>(
4728            "package Foo;
4729require 5.010;
4730my $x = 1;",
4731            "foo.pl",
4732            |metric| {
4733                insta::assert_json_snapshot!(metric.loc, @r#"
4734                {
4735                  "sloc": 3,
4736                  "ploc": 3,
4737                  "lloc": 3,
4738                  "cloc": 0,
4739                  "blank": 0,
4740                  "sloc_average": 3.0,
4741                  "ploc_average": 3.0,
4742                  "lloc_average": 3.0,
4743                  "cloc_average": 0.0,
4744                  "blank_average": 0.0,
4745                  "sloc_min": 3,
4746                  "sloc_max": 3,
4747                  "cloc_min": 0,
4748                  "cloc_max": 0,
4749                  "ploc_min": 3,
4750                  "ploc_max": 3,
4751                  "lloc_min": 3,
4752                  "lloc_max": 3,
4753                  "blank_min": 0,
4754                  "blank_max": 0
4755                }
4756                "#);
4757            },
4758        );
4759    }
4760
4761    #[test]
4762    fn lua_blank() {
4763        check_metrics::<LuaParser>(
4764            "local x = 1
4765
4766local y = 2",
4767            "foo.lua",
4768            |metric| {
4769                assert_eq!(metric.loc.sloc(), 3);
4770                assert_eq!(metric.loc.ploc(), 2);
4771                assert_eq!(metric.loc.lloc(), 2);
4772                assert_eq!(metric.loc.cloc(), 0);
4773                assert_eq!(metric.loc.blank(), 1);
4774                insta::assert_json_snapshot!(metric.loc);
4775            },
4776        );
4777    }
4778
4779    #[test]
4780    fn lua_no_zero_blank() {
4781        // Blank line interleaved with code that carries trailing comments —
4782        // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
4783        check_metrics::<LuaParser>(
4784            "local a = 1
4785local b = 2
4786
4787local c = 3 -- trailing
4788local d = 4 -- trailing
4789local e = 5",
4790            "foo.lua",
4791            |metric| {
4792                assert_eq!(metric.loc.sloc(), 6);
4793                assert_eq!(metric.loc.ploc(), 5);
4794                assert_eq!(metric.loc.cloc(), 2);
4795                assert_eq!(metric.loc.blank(), 1);
4796                insta::assert_json_snapshot!(metric.loc);
4797            },
4798        );
4799    }
4800
4801    #[test]
4802    fn lua_blank_zero_sanity() {
4803        // Sanity check: blank must report 0, never go negative, when the
4804        // input has no blank lines.
4805        check_metrics::<LuaParser>(
4806            "local x = 1
4807local y = 2",
4808            "foo.lua",
4809            |metric| {
4810                assert_eq!(metric.loc.sloc(), 2);
4811                assert_eq!(metric.loc.ploc(), 2);
4812                assert_eq!(metric.loc.lloc(), 2);
4813                assert_eq!(metric.loc.cloc(), 0);
4814                assert_eq!(metric.loc.blank(), 0);
4815            },
4816        );
4817    }
4818
4819    #[test]
4820    fn lua_cloc() {
4821        check_metrics::<LuaParser>(
4822            "-- single line comment
4823local x = 1
4824--[[
4825  block comment
4826  second line
4827]]",
4828            "foo.lua",
4829            |metric| {
4830                assert_eq!(metric.loc.sloc(), 6);
4831                assert_eq!(metric.loc.ploc(), 1);
4832                assert_eq!(metric.loc.lloc(), 1);
4833                assert_eq!(metric.loc.cloc(), 5);
4834                assert_eq!(metric.loc.blank(), 0);
4835                insta::assert_json_snapshot!(metric.loc);
4836            },
4837        );
4838    }
4839
4840    #[test]
4841    fn lua_lloc() {
4842        check_metrics::<LuaParser>(
4843            "local function f(x)
4844  if x > 0 then
4845    local y = x + 1
4846    return y
4847  end
4848  return 0
4849end",
4850            "foo.lua",
4851            |metric| {
4852                assert_eq!(metric.loc.sloc(), 7);
4853                assert_eq!(metric.loc.ploc(), 7);
4854                assert_eq!(metric.loc.lloc(), 5);
4855                assert_eq!(metric.loc.cloc(), 0);
4856                assert_eq!(metric.loc.blank(), 0);
4857                insta::assert_json_snapshot!(metric.loc);
4858            },
4859        );
4860    }
4861
4862    #[test]
4863    fn lua_no_string_lloc() {
4864        // Long strings spanning multiple lines must not inflate lloc.
4865        check_metrics::<LuaParser>(
4866            "local s = [[
4867  line one
4868  line two
4869]]",
4870            "foo.lua",
4871            |metric| {
4872                // #778: a multi-line long-bracket string credits every spanned
4873                // row to PLOC (matching Python's #415 decision), so all four
4874                // rows are code and none are blank. It still contributes a
4875                // single lloc — the assignment statement.
4876                assert_eq!(metric.loc.sloc(), 4);
4877                assert_eq!(metric.loc.ploc(), 4);
4878                assert_eq!(metric.loc.lloc(), 1);
4879                assert_eq!(metric.loc.cloc(), 0);
4880                assert_eq!(metric.loc.blank(), 0);
4881                insta::assert_json_snapshot!(metric.loc);
4882            },
4883        );
4884    }
4885
4886    #[test]
4887    fn lua_no_functiondefinition_lloc() {
4888        // Anonymous function definition is an expression, not a statement.
4889        // The containing variable_declaration counts as lloc; FunctionDefinition must not.
4890        check_metrics::<LuaParser>(
4891            "local f = function(x)
4892  return x + 1
4893end",
4894            "foo.lua",
4895            |metric| {
4896                assert_eq!(metric.loc.sloc(), 3);
4897                assert_eq!(metric.loc.ploc(), 3);
4898                assert_eq!(metric.loc.lloc(), 2);
4899                assert_eq!(metric.loc.cloc(), 0);
4900                assert_eq!(metric.loc.blank(), 0);
4901                insta::assert_json_snapshot!(metric.loc);
4902            },
4903        );
4904    }
4905
4906    #[test]
4907    fn lua_no_elseif_lloc() {
4908        // elseif_statement must not add lloc; only if_statement does.
4909        check_metrics::<LuaParser>(
4910            "local function f(x)
4911  if x > 0 then
4912    return 1
4913  elseif x < 0 then
4914    return -1
4915  else
4916    return 0
4917  end
4918end",
4919            "foo.lua",
4920            |metric| {
4921                assert_eq!(metric.loc.sloc(), 9);
4922                assert_eq!(metric.loc.ploc(), 9);
4923                assert_eq!(metric.loc.lloc(), 5);
4924                assert_eq!(metric.loc.cloc(), 0);
4925                assert_eq!(metric.loc.blank(), 0);
4926                insta::assert_json_snapshot!(metric.loc);
4927            },
4928        );
4929    }
4930
4931    #[test]
4932    fn lua_no_else_lloc() {
4933        // else_statement must not add lloc.
4934        check_metrics::<LuaParser>(
4935            "local function f(x)
4936  if x > 0 then
4937    return 1
4938  else
4939    return 0
4940  end
4941end",
4942            "foo.lua",
4943            |metric| {
4944                assert_eq!(metric.loc.sloc(), 7);
4945                assert_eq!(metric.loc.ploc(), 7);
4946                assert_eq!(metric.loc.lloc(), 4);
4947                assert_eq!(metric.loc.cloc(), 0);
4948                assert_eq!(metric.loc.blank(), 0);
4949                insta::assert_json_snapshot!(metric.loc);
4950            },
4951        );
4952    }
4953
4954    #[test]
4955    fn lua_functiondeclaration_lloc() {
4956        // Named function declaration counts as one lloc.
4957        check_metrics::<LuaParser>(
4958            "function f()
4959  return 1
4960end",
4961            "foo.lua",
4962            |metric| {
4963                assert_eq!(metric.loc.sloc(), 3);
4964                assert_eq!(metric.loc.ploc(), 3);
4965                assert_eq!(metric.loc.lloc(), 2);
4966                assert_eq!(metric.loc.cloc(), 0);
4967                assert_eq!(metric.loc.blank(), 0);
4968                insta::assert_json_snapshot!(metric.loc);
4969            },
4970        );
4971    }
4972
4973    #[test]
4974    fn lua_local_function_lloc() {
4975        // local function declaration is also a function_declaration node → one lloc.
4976        check_metrics::<LuaParser>(
4977            "local function g()
4978  return 2
4979end",
4980            "foo.lua",
4981            |metric| {
4982                assert_eq!(metric.loc.sloc(), 3);
4983                assert_eq!(metric.loc.ploc(), 3);
4984                assert_eq!(metric.loc.lloc(), 2);
4985                assert_eq!(metric.loc.cloc(), 0);
4986                assert_eq!(metric.loc.blank(), 0);
4987                insta::assert_json_snapshot!(metric.loc);
4988            },
4989        );
4990    }
4991
4992    #[test]
4993    fn lua_for_numeric_lloc() {
4994        check_metrics::<LuaParser>(
4995            "for i = 1, 10 do
4996  print(i)
4997end",
4998            "foo.lua",
4999            |metric| {
5000                assert_eq!(metric.loc.sloc(), 3);
5001                assert_eq!(metric.loc.ploc(), 3);
5002                assert_eq!(metric.loc.lloc(), 1);
5003                assert_eq!(metric.loc.cloc(), 0);
5004                assert_eq!(metric.loc.blank(), 0);
5005                insta::assert_json_snapshot!(metric.loc);
5006            },
5007        );
5008    }
5009
5010    #[test]
5011    fn lua_for_generic_lloc() {
5012        check_metrics::<LuaParser>(
5013            "for k, v in pairs(t) do
5014  print(k, v)
5015end",
5016            "foo.lua",
5017            |metric| {
5018                assert_eq!(metric.loc.sloc(), 3);
5019                assert_eq!(metric.loc.ploc(), 3);
5020                assert_eq!(metric.loc.lloc(), 1);
5021                assert_eq!(metric.loc.cloc(), 0);
5022                assert_eq!(metric.loc.blank(), 0);
5023                insta::assert_json_snapshot!(metric.loc);
5024            },
5025        );
5026    }
5027
5028    #[test]
5029    fn lua_repeat_lloc() {
5030        check_metrics::<LuaParser>(
5031            "local i = 0
5032repeat
5033  i = i + 1
5034until i >= 10",
5035            "foo.lua",
5036            |metric| {
5037                assert_eq!(metric.loc.sloc(), 4);
5038                assert_eq!(metric.loc.ploc(), 4);
5039                assert_eq!(metric.loc.lloc(), 3);
5040                assert_eq!(metric.loc.cloc(), 0);
5041                assert_eq!(metric.loc.blank(), 0);
5042                insta::assert_json_snapshot!(metric.loc);
5043            },
5044        );
5045    }
5046
5047    #[test]
5048    fn lua_local_decl_lloc() {
5049        check_metrics::<LuaParser>(
5050            "local x = 1
5051local y, z = 2, 3",
5052            "foo.lua",
5053            |metric| {
5054                assert_eq!(metric.loc.sloc(), 2);
5055                assert_eq!(metric.loc.ploc(), 2);
5056                assert_eq!(metric.loc.lloc(), 2);
5057                assert_eq!(metric.loc.cloc(), 0);
5058                assert_eq!(metric.loc.blank(), 0);
5059                insta::assert_json_snapshot!(metric.loc);
5060            },
5061        );
5062    }
5063
5064    #[test]
5065    fn lua_function_call_lloc() {
5066        // Standalone function calls have no expression_statement wrapper in Lua.
5067        // They fall to the `_` branch → counted as ploc, not lloc.
5068        check_metrics::<LuaParser>(
5069            "print(\"hello\")
5070local x = 1",
5071            "foo.lua",
5072            |metric| {
5073                assert_eq!(metric.loc.sloc(), 2);
5074                assert_eq!(metric.loc.ploc(), 2);
5075                assert_eq!(metric.loc.lloc(), 1);
5076                assert_eq!(metric.loc.cloc(), 0);
5077                assert_eq!(metric.loc.blank(), 0);
5078                insta::assert_json_snapshot!(metric.loc);
5079            },
5080        );
5081    }
5082
5083    #[test]
5084    fn lua_toplevel_assignment_lloc() {
5085        // Bare `x = 1` at chunk level: parent is Chunk, not VariableDeclaration,
5086        // so the parent-guard correctly counts it as 1 lloc.
5087        check_metrics::<LuaParser>(
5088            "x = 1
5089y, z = 2, 3",
5090            "foo.lua",
5091            |metric| {
5092                assert_eq!(metric.loc.sloc(), 2);
5093                assert_eq!(metric.loc.ploc(), 2);
5094                assert_eq!(metric.loc.lloc(), 2);
5095                assert_eq!(metric.loc.cloc(), 0);
5096                assert_eq!(metric.loc.blank(), 0);
5097                insta::assert_json_snapshot!(metric.loc);
5098            },
5099        );
5100    }
5101
5102    #[test]
5103    fn tsx_basic_loc() {
5104        check_metrics::<TsxParser>(
5105            "// A simple utility function
5106            function add(a: number, b: number): number {
5107                /* multi-line
5108                   comment */
5109                return a + b;
5110            }
5111
5112            const greet = (name: string) => {
5113                return `Hello, ${name}`;
5114            };",
5115            "foo.tsx",
5116            |metric| {
5117                insta::assert_json_snapshot!(
5118                    metric.loc,
5119                    @r#"
5120                {
5121                  "sloc": 10,
5122                  "ploc": 6,
5123                  "lloc": 3,
5124                  "cloc": 3,
5125                  "blank": 1,
5126                  "sloc_average": 3.3333333333333335,
5127                  "ploc_average": 2.0,
5128                  "lloc_average": 1.0,
5129                  "cloc_average": 1.0,
5130                  "blank_average": 0.3333333333333333,
5131                  "sloc_min": 3,
5132                  "sloc_max": 10,
5133                  "cloc_min": 0,
5134                  "cloc_max": 3,
5135                  "ploc_min": 3,
5136                  "ploc_max": 6,
5137                  "lloc_min": 1,
5138                  "lloc_max": 3,
5139                  "blank_min": 0,
5140                  "blank_max": 1
5141                }
5142                "#
5143                );
5144            },
5145        );
5146    }
5147
5148    #[test]
5149    fn typescript_basic_loc() {
5150        check_metrics::<TypescriptParser>(
5151            "// Line comment
5152            /* Block
5153               comment */
5154            function greet(name: string): string {
5155                return `Hello, ${name}`;
5156            }
5157
5158            const add = (a: number, b: number): number => a + b;",
5159            "foo.ts",
5160            |metric| {
5161                insta::assert_json_snapshot!(
5162                    metric.loc,
5163                    @r#"
5164                {
5165                  "sloc": 8,
5166                  "ploc": 4,
5167                  "lloc": 2,
5168                  "cloc": 3,
5169                  "blank": 1,
5170                  "sloc_average": 2.6666666666666665,
5171                  "ploc_average": 1.3333333333333333,
5172                  "lloc_average": 0.6666666666666666,
5173                  "cloc_average": 1.0,
5174                  "blank_average": 0.3333333333333333,
5175                  "sloc_min": 1,
5176                  "sloc_max": 8,
5177                  "cloc_min": 0,
5178                  "cloc_max": 3,
5179                  "ploc_min": 1,
5180                  "ploc_max": 4,
5181                  "lloc_min": 0,
5182                  "lloc_max": 2,
5183                  "blank_min": 0,
5184                  "blank_max": 1
5185                }
5186                "#
5187                );
5188            },
5189        );
5190    }
5191
5192    #[test]
5193    fn csharp_comments() {
5194        check_metrics::<CsharpParser>(
5195            "for (int i = 0; i < 100; i++) {
5196               // Print hello
5197               System.Console.WriteLine(\"hello\");
5198               /// XML doc comment
5199               System.Console.WriteLine(\"hello\");
5200             }",
5201            "foo.cs",
5202            |metric| {
5203                assert_eq!(metric.loc.sloc(), 6);
5204                assert_eq!(metric.loc.ploc(), 4);
5205                assert_eq!(metric.loc.lloc(), 3);
5206                assert_eq!(metric.loc.cloc(), 2);
5207                assert_eq!(metric.loc.blank(), 0);
5208                insta::assert_json_snapshot!(metric.loc);
5209            },
5210        );
5211    }
5212
5213    #[test]
5214    fn csharp_blank() {
5215        check_metrics::<CsharpParser>(
5216            "int x = 1;
5217
5218
5219            int y = 2;",
5220            "foo.cs",
5221            |metric| {
5222                assert_eq!(metric.loc.sloc(), 4);
5223                assert_eq!(metric.loc.ploc(), 2);
5224                assert_eq!(metric.loc.lloc(), 2);
5225                assert_eq!(metric.loc.cloc(), 0);
5226                assert_eq!(metric.loc.blank(), 2);
5227                insta::assert_json_snapshot!(metric.loc);
5228            },
5229        );
5230    }
5231
5232    #[test]
5233    fn csharp_sloc() {
5234        check_metrics::<CsharpParser>(
5235            "for (int i = 0; i < 100; i++) {
5236               System.Console.WriteLine(i);
5237             }",
5238            "foo.cs",
5239            |metric| {
5240                assert_eq!(metric.loc.sloc(), 3);
5241                assert_eq!(metric.loc.ploc(), 3);
5242                assert_eq!(metric.loc.lloc(), 2);
5243                assert_eq!(metric.loc.cloc(), 0);
5244                assert_eq!(metric.loc.blank(), 0);
5245                insta::assert_json_snapshot!(metric.loc);
5246            },
5247        );
5248    }
5249
5250    #[test]
5251    fn csharp_module_sloc() {
5252        check_metrics::<CsharpParser>(
5253            "namespace HelloWorld {
5254              class Program { }
5255            }",
5256            "foo.cs",
5257            |metric| {
5258                assert_eq!(metric.loc.sloc(), 3);
5259                assert_eq!(metric.loc.ploc(), 3);
5260                assert_eq!(metric.loc.lloc(), 0);
5261                assert_eq!(metric.loc.cloc(), 0);
5262                assert_eq!(metric.loc.blank(), 0);
5263                insta::assert_json_snapshot!(metric.loc);
5264            },
5265        );
5266    }
5267
5268    #[test]
5269    fn csharp_single_ploc() {
5270        check_metrics::<CsharpParser>("int x = 1;", "foo.cs", |metric| {
5271            assert_eq!(metric.loc.sloc(), 1);
5272            assert_eq!(metric.loc.ploc(), 1);
5273            assert_eq!(metric.loc.lloc(), 1);
5274            assert_eq!(metric.loc.cloc(), 0);
5275            assert_eq!(metric.loc.blank(), 0);
5276            insta::assert_json_snapshot!(metric.loc);
5277        });
5278    }
5279
5280    #[test]
5281    fn csharp_simple_ploc() {
5282        check_metrics::<CsharpParser>(
5283            "for (int i = 0; i < 100; i++) {
5284               System.Console.WriteLine(i);
5285             }",
5286            "foo.cs",
5287            |metric| {
5288                assert_eq!(metric.loc.sloc(), 3);
5289                assert_eq!(metric.loc.ploc(), 3);
5290                assert_eq!(metric.loc.lloc(), 2);
5291                assert_eq!(metric.loc.cloc(), 0);
5292                assert_eq!(metric.loc.blank(), 0);
5293                insta::assert_json_snapshot!(metric.loc);
5294            },
5295        );
5296    }
5297
5298    #[test]
5299    fn csharp_multi_ploc() {
5300        check_metrics::<CsharpParser>(
5301            "int x = 1;
5302            for (int i = 0; i < 100; i++) {
5303               System.Console.WriteLine(i);
5304             }",
5305            "foo.cs",
5306            |metric| {
5307                assert_eq!(metric.loc.sloc(), 4);
5308                assert_eq!(metric.loc.ploc(), 4);
5309                assert_eq!(metric.loc.lloc(), 3);
5310                assert_eq!(metric.loc.cloc(), 0);
5311                assert_eq!(metric.loc.blank(), 0);
5312                insta::assert_json_snapshot!(metric.loc);
5313            },
5314        );
5315    }
5316
5317    #[test]
5318    fn csharp_single_statement_lloc() {
5319        check_metrics::<CsharpParser>("int max = 10;", "foo.cs", |metric| {
5320            assert_eq!(metric.loc.sloc(), 1);
5321            assert_eq!(metric.loc.ploc(), 1);
5322            assert_eq!(metric.loc.lloc(), 1);
5323            assert_eq!(metric.loc.cloc(), 0);
5324            assert_eq!(metric.loc.blank(), 0);
5325            insta::assert_json_snapshot!(metric.loc);
5326        });
5327    }
5328
5329    #[test]
5330    fn csharp_for_lloc() {
5331        check_metrics::<CsharpParser>(
5332            "for (int i = 0; i < 10; i++) {
5333                System.Console.WriteLine(i);
5334            }",
5335            "foo.cs",
5336            |metric| {
5337                assert_eq!(metric.loc.sloc(), 3);
5338                assert_eq!(metric.loc.ploc(), 3);
5339                assert_eq!(metric.loc.lloc(), 2);
5340                assert_eq!(metric.loc.cloc(), 0);
5341                assert_eq!(metric.loc.blank(), 0);
5342                insta::assert_json_snapshot!(metric.loc);
5343            },
5344        );
5345    }
5346
5347    #[test]
5348    fn csharp_foreach_lloc() {
5349        check_metrics::<CsharpParser>(
5350            "foreach (var item in items) {
5351                System.Console.WriteLine(item);
5352            }",
5353            "foo.cs",
5354            |metric| {
5355                assert_eq!(metric.loc.sloc(), 3);
5356                assert_eq!(metric.loc.ploc(), 3);
5357                assert_eq!(metric.loc.lloc(), 2);
5358                assert_eq!(metric.loc.cloc(), 0);
5359                assert_eq!(metric.loc.blank(), 0);
5360                insta::assert_json_snapshot!(metric.loc);
5361            },
5362        );
5363    }
5364
5365    #[test]
5366    fn csharp_while_lloc() {
5367        check_metrics::<CsharpParser>(
5368            "int i = 0;
5369            while (i < 10) {
5370                i++;
5371            }",
5372            "foo.cs",
5373            |metric| {
5374                assert_eq!(metric.loc.sloc(), 4);
5375                assert_eq!(metric.loc.ploc(), 4);
5376                assert_eq!(metric.loc.lloc(), 3);
5377                assert_eq!(metric.loc.cloc(), 0);
5378                assert_eq!(metric.loc.blank(), 0);
5379                insta::assert_json_snapshot!(metric.loc);
5380            },
5381        );
5382    }
5383
5384    #[test]
5385    fn csharp_do_while_lloc() {
5386        check_metrics::<CsharpParser>(
5387            "int i = 0;
5388            do {
5389                i++;
5390            } while (i < 10);",
5391            "foo.cs",
5392            |metric| {
5393                assert_eq!(metric.loc.sloc(), 4);
5394                assert_eq!(metric.loc.ploc(), 4);
5395                assert_eq!(metric.loc.lloc(), 3);
5396                assert_eq!(metric.loc.cloc(), 0);
5397                assert_eq!(metric.loc.blank(), 0);
5398                insta::assert_json_snapshot!(metric.loc);
5399            },
5400        );
5401    }
5402
5403    #[test]
5404    fn csharp_switch_lloc() {
5405        check_metrics::<CsharpParser>(
5406            "switch (x) {
5407                case 1: System.Console.WriteLine(1); break;
5408                case 2: System.Console.WriteLine(2); break;
5409                default: System.Console.WriteLine(0); break;
5410            }
5411            string s = x switch { 1 => \"one\", _ => \"other\" };",
5412            "foo.cs",
5413            |metric| {
5414                assert_eq!(metric.loc.sloc(), 6);
5415                assert_eq!(metric.loc.ploc(), 6);
5416                assert_eq!(metric.loc.lloc(), 8);
5417                assert_eq!(metric.loc.cloc(), 0);
5418                assert_eq!(metric.loc.blank(), 0);
5419                insta::assert_json_snapshot!(metric.loc);
5420            },
5421        );
5422    }
5423
5424    #[test]
5425    fn csharp_continue_lloc() {
5426        check_metrics::<CsharpParser>(
5427            "for (int i = 0; i < 10; i++) {
5428                if (i == 5) continue;
5429                System.Console.WriteLine(i);
5430            }",
5431            "foo.cs",
5432            |metric| {
5433                assert_eq!(metric.loc.sloc(), 4);
5434                assert_eq!(metric.loc.ploc(), 4);
5435                assert_eq!(metric.loc.lloc(), 4);
5436                assert_eq!(metric.loc.cloc(), 0);
5437                assert_eq!(metric.loc.blank(), 0);
5438                insta::assert_json_snapshot!(metric.loc);
5439            },
5440        );
5441    }
5442
5443    #[test]
5444    fn csharp_try_lloc() {
5445        check_metrics::<CsharpParser>(
5446            "try {
5447                System.Console.WriteLine(\"try\");
5448            } catch (System.Exception e) {
5449                throw new System.Exception(\"caught\");
5450            } finally {
5451                System.Console.WriteLine(\"done\");
5452            }",
5453            "foo.cs",
5454            |metric| {
5455                assert_eq!(metric.loc.sloc(), 7);
5456                assert_eq!(metric.loc.ploc(), 7);
5457                assert_eq!(metric.loc.lloc(), 4);
5458                assert_eq!(metric.loc.cloc(), 0);
5459                assert_eq!(metric.loc.blank(), 0);
5460                insta::assert_json_snapshot!(metric.loc);
5461            },
5462        );
5463    }
5464
5465    #[test]
5466    fn csharp_class_loc() {
5467        check_metrics::<CsharpParser>(
5468            "class A {
5469                int x;
5470                public void M() {
5471                    System.Console.WriteLine(x);
5472                }
5473            }",
5474            "foo.cs",
5475            |metric| {
5476                assert_eq!(metric.loc.sloc(), 6);
5477                assert_eq!(metric.loc.ploc(), 6);
5478                assert_eq!(metric.loc.lloc(), 1);
5479                assert_eq!(metric.loc.cloc(), 0);
5480                assert_eq!(metric.loc.blank(), 0);
5481                insta::assert_json_snapshot!(metric.loc);
5482            },
5483        );
5484    }
5485
5486    #[test]
5487    fn csharp_expressions_lloc() {
5488        check_metrics::<CsharpParser>(
5489            "int a = 1;
5490            int b = 2;
5491            int c = a + b;
5492            System.Console.WriteLine(c);",
5493            "foo.cs",
5494            |metric| {
5495                assert_eq!(metric.loc.sloc(), 4);
5496                assert_eq!(metric.loc.ploc(), 4);
5497                assert_eq!(metric.loc.lloc(), 4);
5498                assert_eq!(metric.loc.cloc(), 0);
5499                assert_eq!(metric.loc.blank(), 0);
5500                insta::assert_json_snapshot!(metric.loc);
5501            },
5502        );
5503    }
5504
5505    #[test]
5506    fn csharp_statement_inline_loc() {
5507        check_metrics::<CsharpParser>(
5508            "if (x > 0) System.Console.WriteLine(x);",
5509            "foo.cs",
5510            |metric| {
5511                assert_eq!(metric.loc.sloc(), 1);
5512                assert_eq!(metric.loc.ploc(), 1);
5513                assert_eq!(metric.loc.lloc(), 2);
5514                assert_eq!(metric.loc.cloc(), 0);
5515                assert_eq!(metric.loc.blank(), 0);
5516                insta::assert_json_snapshot!(metric.loc);
5517            },
5518        );
5519    }
5520
5521    #[test]
5522    fn csharp_general_loc() {
5523        check_metrics::<CsharpParser>(
5524            "using System;
5525            namespace Demo {
5526                class A {
5527                    public void M() {
5528                        Console.WriteLine(\"hi\");
5529                    }
5530                }
5531                class B {
5532                    public int N() { return 0; }
5533                }
5534            }",
5535            "foo.cs",
5536            |metric| {
5537                assert_eq!(metric.loc.sloc(), 11);
5538                assert_eq!(metric.loc.ploc(), 11);
5539                assert_eq!(metric.loc.lloc(), 2);
5540                assert_eq!(metric.loc.cloc(), 0);
5541                assert_eq!(metric.loc.blank(), 0);
5542                insta::assert_json_snapshot!(metric.loc);
5543            },
5544        );
5545    }
5546
5547    #[test]
5548    fn csharp_using_lloc() {
5549        // EC11 — `using_directive` does not bump LLOC; `using_statement`
5550        // (block form) and the C# 8 simple-using local-declaration
5551        // (`using var x = ...;`) both do, the latter via the standard
5552        // `LocalDeclarationStatement` path.
5553        check_metrics::<CsharpParser>(
5554            "using System;
5555            using System.IO;
5556            class A {
5557                public void M() {
5558                    using (var s = File.OpenRead(\"x\")) {
5559                        Console.WriteLine(s);
5560                    }
5561                    using var t = File.OpenRead(\"y\");
5562                    Console.WriteLine(t);
5563                }
5564            }",
5565            "foo.cs",
5566            |metric| {
5567                assert_eq!(metric.loc.sloc(), 11);
5568                assert_eq!(metric.loc.ploc(), 11);
5569                assert_eq!(metric.loc.lloc(), 4);
5570                assert_eq!(metric.loc.cloc(), 0);
5571                assert_eq!(metric.loc.blank(), 0);
5572                insta::assert_json_snapshot!(metric.loc);
5573            },
5574        );
5575    }
5576
5577    #[test]
5578    fn kotlin_loc_basic() {
5579        check_metrics::<KotlinParser>(
5580            "// A simple function
5581            fun greet(name: String): String {
5582                val greeting = \"Hello, \" + name
5583                if (name.isEmpty()) {
5584                    return \"Hello, World!\"
5585                }
5586                return greeting
5587            }",
5588            "foo.kt",
5589            |metric| {
5590                insta::assert_json_snapshot!(
5591                    metric.loc,
5592                    @r#"
5593                {
5594                  "sloc": 8,
5595                  "ploc": 7,
5596                  "lloc": 4,
5597                  "cloc": 1,
5598                  "blank": 0,
5599                  "sloc_average": 4.0,
5600                  "ploc_average": 3.5,
5601                  "lloc_average": 2.0,
5602                  "cloc_average": 0.5,
5603                  "blank_average": 0.0,
5604                  "sloc_min": 7,
5605                  "sloc_max": 8,
5606                  "cloc_min": 0,
5607                  "cloc_max": 1,
5608                  "ploc_min": 7,
5609                  "ploc_max": 7,
5610                  "lloc_min": 4,
5611                  "lloc_max": 4,
5612                  "blank_min": 0,
5613                  "blank_max": 0
5614                }
5615                "#
5616                );
5617            },
5618        );
5619    }
5620
5621    #[test]
5622    fn kotlin_loc_bare_expression() {
5623        check_metrics::<KotlinParser>(
5624            "fun main() {
5625                val x = 42
5626                println(x)
5627                listOf(1, 2, 3).forEach { println(it) }
5628            }",
5629            "foo.kt",
5630            |metric| {
5631                // lloc should count: val x = 42 (PropertyDeclaration, +1)
5632                // + println(x) (CallExpression, parent=Block, +1)
5633                // + listOf(1, 2, 3).forEach { ... } (CallExpression, parent=Block, +1) = 3
5634                insta::assert_json_snapshot!(
5635                    metric.loc,
5636                    @r#"
5637                {
5638                  "sloc": 5,
5639                  "ploc": 5,
5640                  "lloc": 3,
5641                  "cloc": 0,
5642                  "blank": 0,
5643                  "sloc_average": 2.5,
5644                  "ploc_average": 2.5,
5645                  "lloc_average": 1.5,
5646                  "cloc_average": 0.0,
5647                  "blank_average": 0.0,
5648                  "sloc_min": 5,
5649                  "sloc_max": 5,
5650                  "cloc_min": 0,
5651                  "cloc_max": 0,
5652                  "ploc_min": 5,
5653                  "ploc_max": 5,
5654                  "lloc_min": 3,
5655                  "lloc_max": 3,
5656                  "blank_min": 0,
5657                  "blank_max": 0
5658                }
5659                "#
5660                );
5661            },
5662        );
5663    }
5664
5665    #[test]
5666    fn bash_loc() {
5667        check_metrics::<BashParser>(
5668            "#!/bin/bash
5669# This is a comment
5670f() {
5671    echo 'hello'
5672}
5673
5674# Another comment
5675f",
5676            "foo.sh",
5677            |metric| {
5678                assert_eq!(metric.loc.sloc(), 8);
5679                assert_eq!(metric.loc.ploc(), 4);
5680                assert_eq!(metric.loc.lloc(), 3);
5681                assert_eq!(metric.loc.cloc(), 3);
5682                assert_eq!(metric.loc.blank(), 1);
5683                insta::assert_json_snapshot!(metric.loc);
5684            },
5685        );
5686    }
5687
5688    // CRLF regression tests: metrics must be identical regardless of line ending style.
5689    // These also serve as canaries for tree-sitter row-counting behaviour with \r bytes.
5690
5691    #[test]
5692    fn python_cloc_crlf_matches_lf() {
5693        check_metrics::<PythonParser>("# comment\nx = 1", "foo.py", |m| {
5694            assert_eq!(m.loc.cloc(), 1);
5695            assert_eq!(m.loc.ploc(), 1);
5696            assert_eq!(m.loc.sloc(), 2);
5697            assert_eq!(m.loc.blank(), 0);
5698        });
5699        check_metrics::<PythonParser>("# comment\r\nx = 1", "foo.py", |m| {
5700            assert_eq!(m.loc.cloc(), 1);
5701            assert_eq!(m.loc.ploc(), 1);
5702            assert_eq!(m.loc.sloc(), 2);
5703            assert_eq!(m.loc.blank(), 0);
5704        });
5705        // Lone-CR (old Mac line endings) is the true canary: without CR normalisation,
5706        // tree-sitter 0.26.8 only advances its row counter on \n, collapsing all content
5707        // onto row 0 and producing wrong sloc/cloc metrics.
5708        check_metrics::<PythonParser>("# comment\rx = 1", "foo.py", |m| {
5709            assert_eq!(m.loc.cloc(), 1);
5710            assert_eq!(m.loc.ploc(), 1);
5711            assert_eq!(m.loc.sloc(), 2);
5712            assert_eq!(m.loc.blank(), 0);
5713        });
5714    }
5715
5716    #[test]
5717    fn python_blank_crlf_matches_lf() {
5718        check_metrics::<PythonParser>("# comment\n\nx = 1", "foo.py", |m| {
5719            assert_eq!(m.loc.blank(), 1);
5720        });
5721        check_metrics::<PythonParser>("# comment\r\n\r\nx = 1", "foo.py", |m| {
5722            assert_eq!(m.loc.blank(), 1);
5723        });
5724        // Lone-CR: without normalisation the blank \r line stays on row 0 and is not counted.
5725        check_metrics::<PythonParser>("# comment\r\rx = 1", "foo.py", |m| {
5726            assert_eq!(m.loc.blank(), 1);
5727        });
5728    }
5729
5730    #[test]
5731    fn rust_cloc_crlf_matches_lf() {
5732        check_metrics::<RustParser>(
5733            "fn f() {\n    // comment\n    let x = 1;\n}",
5734            "foo.rs",
5735            |m| {
5736                assert_eq!(m.loc.cloc(), 1);
5737                assert_eq!(m.loc.sloc(), 4);
5738            },
5739        );
5740        check_metrics::<RustParser>(
5741            "fn f() {\r\n    // comment\r\n    let x = 1;\r\n}",
5742            "foo.rs",
5743            |m| {
5744                assert_eq!(m.loc.cloc(), 1);
5745                assert_eq!(m.loc.sloc(), 4);
5746            },
5747        );
5748        // Lone-CR: without normalisation, tree-sitter 0.26.8 only advances its row counter on
5749        // \n, so all content collapses onto row 0 and sloc becomes 1 instead of 4.
5750        check_metrics::<RustParser>(
5751            "fn f() {\r    // comment\r    let x = 1;\r}",
5752            "foo.rs",
5753            |m| {
5754                assert_eq!(m.loc.cloc(), 1);
5755                assert_eq!(m.loc.sloc(), 4);
5756            },
5757        );
5758    }
5759
5760    #[test]
5761    fn tcl_blank() {
5762        check_metrics::<TclParser>("set x 1\n\nset y 2", "foo.tcl", |metric| {
5763            assert_eq!(metric.loc.sloc(), 3);
5764            assert_eq!(metric.loc.ploc(), 2);
5765            assert_eq!(metric.loc.lloc(), 2);
5766            assert_eq!(metric.loc.cloc(), 0);
5767            assert_eq!(metric.loc.blank(), 1);
5768            insta::assert_json_snapshot!(metric.loc);
5769        });
5770    }
5771
5772    #[test]
5773    fn tcl_no_zero_blank() {
5774        // Blank line interleaved with code that carries trailing comments —
5775        // ensures the `blank = sloc - (ploc ∪ cloc lines)` union math holds
5776        // when code and comment lines coincide.
5777        check_metrics::<TclParser>(
5778            "set a 1\nset b 2\n\nset c 3 ;# trailing\nset d 4 ;# trailing\nset e 5",
5779            "foo.tcl",
5780            |metric| {
5781                assert_eq!(metric.loc.sloc(), 6);
5782                assert_eq!(metric.loc.ploc(), 5);
5783                assert_eq!(metric.loc.cloc(), 2);
5784                assert_eq!(metric.loc.blank(), 1);
5785            },
5786        );
5787    }
5788
5789    /// expected: row 0 is comment-only, row 1 is the sole code row — so
5790    /// `cloc 1 + ploc 1 == sloc 2`. This test pinned `ploc == 2` until
5791    /// #1135: the `LF` token terminating the comment row landed in the
5792    /// `_` catch-all and inserted that row into PLOC, which also drove
5793    /// `cloc + ploc` past `sloc`.
5794    #[test]
5795    fn tcl_cloc() {
5796        check_metrics::<TclParser>("# This is a comment\nset x 1", "foo.tcl", |metric| {
5797            assert_eq!(metric.loc.sloc(), 2);
5798            assert_eq!(metric.loc.ploc(), 1);
5799            assert_eq!(metric.loc.lloc(), 1);
5800            assert_eq!(metric.loc.cloc(), 1);
5801            assert_eq!(metric.loc.blank(), 0);
5802            insta::assert_json_snapshot!(metric.loc);
5803        });
5804    }
5805
5806    #[test]
5807    fn tcl_lloc() {
5808        check_metrics::<TclParser>(
5809            "proc f {x} {
5810    while {$x > 0} {
5811        if {$x > 10} {
5812            set x [expr {$x - 1}]
5813        }
5814    }
5815}",
5816            "foo.tcl",
5817            |metric| {
5818                assert_eq!(metric.loc.sloc(), 7);
5819                assert_eq!(metric.loc.ploc(), 7);
5820                assert_eq!(metric.loc.lloc(), 4);
5821                assert_eq!(metric.loc.cloc(), 0);
5822                assert_eq!(metric.loc.blank(), 0);
5823                insta::assert_json_snapshot!(metric.loc);
5824            },
5825        );
5826    }
5827
5828    #[test]
5829    fn tcl_no_command_substitution_lloc() {
5830        // `string toupper` inside [...] is a sub-expression; only `puts` is top-level.
5831        check_metrics::<TclParser>("puts [string toupper x]", "foo.tcl", |metric| {
5832            assert_eq!(metric.loc.sloc(), 1);
5833            assert_eq!(metric.loc.ploc(), 1);
5834            assert_eq!(metric.loc.lloc(), 1);
5835            assert_eq!(metric.loc.cloc(), 0);
5836            assert_eq!(metric.loc.blank(), 0);
5837            insta::assert_json_snapshot!(metric.loc);
5838        });
5839    }
5840
5841    #[test]
5842    fn tcl_procedure_lloc() {
5843        check_metrics::<TclParser>("proc foo {} {\n    puts hello\n}", "foo.tcl", |metric| {
5844            assert_eq!(metric.loc.sloc(), 3);
5845            assert_eq!(metric.loc.ploc(), 3);
5846            assert_eq!(metric.loc.lloc(), 2);
5847            assert_eq!(metric.loc.cloc(), 0);
5848            assert_eq!(metric.loc.blank(), 0);
5849            insta::assert_json_snapshot!(metric.loc);
5850        });
5851    }
5852
5853    #[test]
5854    fn tcl_if_lloc() {
5855        check_metrics::<TclParser>("if {1} {\n    puts hello\n}", "foo.tcl", |metric| {
5856            assert_eq!(metric.loc.sloc(), 3);
5857            assert_eq!(metric.loc.ploc(), 3);
5858            assert_eq!(metric.loc.lloc(), 2);
5859            assert_eq!(metric.loc.cloc(), 0);
5860            assert_eq!(metric.loc.blank(), 0);
5861            insta::assert_json_snapshot!(metric.loc);
5862        });
5863    }
5864
5865    #[test]
5866    fn tcl_elseif_lloc() {
5867        // if=1 lloc, elseif=1 lloc, else adds 0 lloc
5868        check_metrics::<TclParser>(
5869            "if {$x > 10} {
5870    puts big
5871} elseif {$x > 5} {
5872    puts medium
5873} else {
5874    puts small
5875}",
5876            "foo.tcl",
5877            |metric| {
5878                assert_eq!(metric.loc.sloc(), 7);
5879                assert_eq!(metric.loc.ploc(), 7);
5880                assert_eq!(metric.loc.lloc(), 5);
5881                assert_eq!(metric.loc.cloc(), 0);
5882                assert_eq!(metric.loc.blank(), 0);
5883                insta::assert_json_snapshot!(metric.loc);
5884            },
5885        );
5886    }
5887
5888    #[test]
5889    fn tcl_while_lloc() {
5890        check_metrics::<TclParser>(
5891            "while {$x > 0} {\n    set x [expr {$x - 1}]\n}",
5892            "foo.tcl",
5893            |metric| {
5894                assert_eq!(metric.loc.sloc(), 3);
5895                assert_eq!(metric.loc.ploc(), 3);
5896                assert_eq!(metric.loc.lloc(), 2);
5897                assert_eq!(metric.loc.cloc(), 0);
5898                assert_eq!(metric.loc.blank(), 0);
5899                insta::assert_json_snapshot!(metric.loc);
5900            },
5901        );
5902    }
5903
5904    #[test]
5905    fn tcl_foreach_lloc() {
5906        check_metrics::<TclParser>(
5907            "foreach item {a b c} {\n    puts $item\n}",
5908            "foo.tcl",
5909            |metric| {
5910                assert_eq!(metric.loc.sloc(), 3);
5911                assert_eq!(metric.loc.ploc(), 3);
5912                assert_eq!(metric.loc.lloc(), 2);
5913                assert_eq!(metric.loc.cloc(), 0);
5914                assert_eq!(metric.loc.blank(), 0);
5915                insta::assert_json_snapshot!(metric.loc);
5916            },
5917        );
5918    }
5919
5920    #[test]
5921    fn tcl_set_lloc() {
5922        check_metrics::<TclParser>("set x 42", "foo.tcl", |metric| {
5923            assert_eq!(metric.loc.sloc(), 1);
5924            assert_eq!(metric.loc.ploc(), 1);
5925            assert_eq!(metric.loc.lloc(), 1);
5926            assert_eq!(metric.loc.cloc(), 0);
5927            assert_eq!(metric.loc.blank(), 0);
5928            insta::assert_json_snapshot!(metric.loc);
5929        });
5930    }
5931
5932    #[test]
5933    fn tcl_global_lloc() {
5934        check_metrics::<TclParser>("global x", "foo.tcl", |metric| {
5935            assert_eq!(metric.loc.sloc(), 1);
5936            assert_eq!(metric.loc.ploc(), 1);
5937            assert_eq!(metric.loc.lloc(), 1);
5938            assert_eq!(metric.loc.cloc(), 0);
5939            assert_eq!(metric.loc.blank(), 0);
5940            insta::assert_json_snapshot!(metric.loc);
5941        });
5942    }
5943
5944    #[test]
5945    fn tcl_try_catch_lloc() {
5946        // try=1 lloc; catch command=1 lloc; commands inside bodies count separately
5947        check_metrics::<TclParser>(
5948            "catch {
5949    set x 1
5950} result
5951try {
5952    set y 2
5953} on error {msg} {
5954    puts $msg
5955}",
5956            "foo.tcl",
5957            |metric| {
5958                assert_eq!(metric.loc.sloc(), 8);
5959                assert_eq!(metric.loc.ploc(), 8);
5960                assert_eq!(metric.loc.lloc(), 5);
5961                assert_eq!(metric.loc.cloc(), 0);
5962                assert_eq!(metric.loc.blank(), 0);
5963                insta::assert_json_snapshot!(metric.loc);
5964            },
5965        );
5966    }
5967
5968    #[test]
5969    fn tcl_namespace_lloc() {
5970        check_metrics::<TclParser>(
5971            "namespace eval myns {\n    set x 1\n}",
5972            "foo.tcl",
5973            |metric| {
5974                assert_eq!(metric.loc.sloc(), 3);
5975                assert_eq!(metric.loc.ploc(), 3);
5976                assert_eq!(metric.loc.lloc(), 2);
5977                assert_eq!(metric.loc.cloc(), 0);
5978                assert_eq!(metric.loc.blank(), 0);
5979                insta::assert_json_snapshot!(metric.loc);
5980            },
5981        );
5982    }
5983
5984    #[test]
5985    fn tcl_regexp_lloc() {
5986        check_metrics::<TclParser>("regexp {^[0-9]+$} $x", "foo.tcl", |metric| {
5987            assert_eq!(metric.loc.sloc(), 1);
5988            assert_eq!(metric.loc.ploc(), 1);
5989            assert_eq!(metric.loc.lloc(), 1);
5990            assert_eq!(metric.loc.cloc(), 0);
5991            assert_eq!(metric.loc.blank(), 0);
5992            insta::assert_json_snapshot!(metric.loc);
5993        });
5994    }
5995
5996    #[test]
5997    fn tcl_expr_cmd_lloc() {
5998        check_metrics::<TclParser>("expr {1 + 2}", "foo.tcl", |metric| {
5999            assert_eq!(metric.loc.sloc(), 1);
6000            assert_eq!(metric.loc.ploc(), 1);
6001            assert_eq!(metric.loc.lloc(), 1);
6002            assert_eq!(metric.loc.cloc(), 0);
6003            assert_eq!(metric.loc.blank(), 0);
6004            insta::assert_json_snapshot!(metric.loc);
6005        });
6006    }
6007
6008    #[test]
6009    fn tcl_no_expr_cmd_substitution_lloc() {
6010        // `expr` inside [...] is a sub-expression, not a statement; only `set` counts.
6011        check_metrics::<TclParser>("set x [expr {1 + 2}]", "foo.tcl", |metric| {
6012            assert_eq!(metric.loc.sloc(), 1);
6013            assert_eq!(metric.loc.ploc(), 1);
6014            assert_eq!(metric.loc.lloc(), 1);
6015            assert_eq!(metric.loc.cloc(), 0);
6016            assert_eq!(metric.loc.blank(), 0);
6017            insta::assert_json_snapshot!(metric.loc);
6018        });
6019    }
6020
6021    #[test]
6022    fn tcl_nested_commands_lloc() {
6023        // Commands inside proc body are recursively parsed; verify each counts.
6024        check_metrics::<TclParser>(
6025            "proc f {x} {
6026    set y [expr {$x * 2}]
6027    puts $y
6028}",
6029            "foo.tcl",
6030            |metric| {
6031                assert_eq!(metric.loc.sloc(), 4);
6032                assert_eq!(metric.loc.ploc(), 4);
6033                assert_eq!(metric.loc.lloc(), 3);
6034                assert_eq!(metric.loc.cloc(), 0);
6035                assert_eq!(metric.loc.blank(), 0);
6036                insta::assert_json_snapshot!(metric.loc);
6037            },
6038        );
6039    }
6040
6041    #[test]
6042    fn tcl_command_lloc() {
6043        check_metrics::<TclParser>("puts hello", "foo.tcl", |metric| {
6044            assert_eq!(metric.loc.sloc(), 1);
6045            assert_eq!(metric.loc.ploc(), 1);
6046            assert_eq!(metric.loc.lloc(), 1);
6047            assert_eq!(metric.loc.cloc(), 0);
6048            assert_eq!(metric.loc.blank(), 0);
6049            insta::assert_json_snapshot!(metric.loc);
6050        });
6051    }
6052
6053    #[test]
6054    fn tcl_no_else_lloc() {
6055        // `else` block does not add a logical line.
6056        check_metrics::<TclParser>(
6057            "if {1} {\n    puts yes\n} else {\n    puts no\n}",
6058            "foo.tcl",
6059            |metric| {
6060                assert_eq!(metric.loc.sloc(), 5);
6061                assert_eq!(metric.loc.ploc(), 5);
6062                assert_eq!(metric.loc.lloc(), 3);
6063                assert_eq!(metric.loc.cloc(), 0);
6064                assert_eq!(metric.loc.blank(), 0);
6065                insta::assert_json_snapshot!(metric.loc);
6066            },
6067        );
6068    }
6069
6070    #[test]
6071    fn tcl_no_finally_lloc() {
6072        // `finally` block, like `else`, does not add a logical line.
6073        // proc(1) + try(1) + puts_hi(1) + puts_done(1) + finally(0) = 4.
6074        check_metrics::<TclParser>(
6075            "proc f {} {\n    try {\n        puts hi\n    } finally {\n        puts done\n    }\n}",
6076            "foo.tcl",
6077            |metric| {
6078                assert_eq!(
6079                    metric.loc.lloc(),
6080                    4,
6081                    "finally adds 0 lloc; would be 5 if finally counted"
6082                );
6083            },
6084        );
6085    }
6086
6087    #[test]
6088    fn tcl_multiline_block() {
6089        check_metrics::<TclParser>(
6090            "proc f {x} {
6091    set a 1
6092
6093    set b 2
6094    return [expr {$a + $b}]
6095}",
6096            "foo.tcl",
6097            |metric| {
6098                assert_eq!(metric.loc.sloc(), 6);
6099                assert_eq!(metric.loc.ploc(), 5);
6100                assert_eq!(metric.loc.lloc(), 4);
6101                assert_eq!(metric.loc.cloc(), 0);
6102                assert_eq!(metric.loc.blank(), 1);
6103                insta::assert_json_snapshot!(metric.loc);
6104            },
6105        );
6106    }
6107
6108    #[test]
6109    fn tcl_no_string_lloc() {
6110        // Multi-line double-quoted strings must not inflate lloc — only the
6111        // surrounding command should count. Mirrors lua_no_string_lloc and
6112        // elixir_no_string_content_lloc; pins the heredoc-shaped invariant
6113        // for Tcl quoted_word bodies.
6114        check_metrics::<TclParser>(
6115            "set s \"line one\nline two\nline three\"",
6116            "foo.tcl",
6117            |metric| {
6118                assert_eq!(metric.loc.sloc(), 3);
6119                assert_eq!(metric.loc.ploc(), 2);
6120                assert_eq!(metric.loc.lloc(), 1);
6121                assert_eq!(metric.loc.cloc(), 0);
6122                assert_eq!(metric.loc.blank(), 1);
6123                insta::assert_json_snapshot!(metric.loc);
6124            },
6125        );
6126    }
6127
6128    #[test]
6129    fn javascript_blank() {
6130        check_metrics::<JavascriptParser>(
6131            "// header comment
6132        function f() {
6133
6134            var x = 1;
6135
6136            var y = 2;
6137        }",
6138            "foo.js",
6139            |metric| {
6140                assert_eq!(metric.loc.sloc(), 7);
6141                assert_eq!(metric.loc.ploc(), 4);
6142                assert_eq!(metric.loc.lloc(), 2);
6143                assert_eq!(metric.loc.cloc(), 1);
6144                assert_eq!(metric.loc.blank(), 2);
6145                insta::assert_json_snapshot!(metric.loc);
6146            },
6147        );
6148    }
6149
6150    #[test]
6151    fn javascript_cloc() {
6152        check_metrics::<JavascriptParser>(
6153            "// line comment
6154        /* block
6155           comment */
6156        function f() {
6157            return 1; // inline
6158        }",
6159            "foo.js",
6160            |metric| {
6161                assert_eq!(metric.loc.sloc(), 6);
6162                assert_eq!(metric.loc.ploc(), 3);
6163                assert_eq!(metric.loc.lloc(), 1);
6164                assert_eq!(metric.loc.cloc(), 4);
6165                assert_eq!(metric.loc.blank(), 0);
6166                insta::assert_json_snapshot!(metric.loc);
6167            },
6168        );
6169    }
6170
6171    #[test]
6172    fn javascript_cloc_html_comment() {
6173        // The Annex-B `<!-- -->` `html_comment` must count as CLOC, not
6174        // fall to the `_` arm and inflate PLOC (#697). Pre-fix this
6175        // fixture measured cloc 0 / ploc 4.
6176        check_metrics::<JavascriptParser>(
6177            "<!-- header comment -->
6178function f() {
6179  return 1;
6180}",
6181            "foo.js",
6182            |metric| {
6183                assert_eq!(metric.loc.sloc(), 4);
6184                assert_eq!(metric.loc.ploc(), 3);
6185                assert_eq!(metric.loc.lloc(), 1);
6186                assert_eq!(metric.loc.cloc(), 1);
6187                assert_eq!(metric.loc.blank(), 0);
6188            },
6189        );
6190    }
6191
6192    #[test]
6193    fn mozjs_blank() {
6194        check_metrics::<MozjsParser>(
6195            "function f() {
6196
6197            var x = 1;
6198
6199        }",
6200            "foo.js",
6201            |metric| {
6202                assert_eq!(metric.loc.sloc(), 5);
6203                assert_eq!(metric.loc.ploc(), 3);
6204                assert_eq!(metric.loc.lloc(), 1);
6205                assert_eq!(metric.loc.cloc(), 0);
6206                assert_eq!(metric.loc.blank(), 2);
6207                insta::assert_json_snapshot!(metric.loc);
6208            },
6209        );
6210    }
6211
6212    #[test]
6213    fn mozjs_cloc() {
6214        check_metrics::<MozjsParser>(
6215            "// header
6216        /* block comment */
6217        function f() {
6218            return 42;
6219        }",
6220            "foo.js",
6221            |metric| {
6222                assert_eq!(metric.loc.sloc(), 5);
6223                assert_eq!(metric.loc.ploc(), 3);
6224                assert_eq!(metric.loc.lloc(), 1);
6225                assert_eq!(metric.loc.cloc(), 2);
6226                assert_eq!(metric.loc.blank(), 0);
6227                insta::assert_json_snapshot!(metric.loc);
6228            },
6229        );
6230    }
6231
6232    #[test]
6233    fn mozjs_no_zero_blank() {
6234        // Blank line interleaved with code that carries trailing comments —
6235        // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
6236        check_metrics::<MozjsParser>(
6237            "function f() {
6238  var a = 1;
6239
6240  var b = 2; // trailing
6241  var c = 3; // trailing
6242}",
6243            "foo.js",
6244            |metric| {
6245                assert_eq!(metric.loc.sloc(), 6);
6246                assert_eq!(metric.loc.ploc(), 5);
6247                assert_eq!(metric.loc.cloc(), 2);
6248                assert_eq!(metric.loc.blank(), 1);
6249                insta::assert_json_snapshot!(metric.loc);
6250            },
6251        );
6252    }
6253
6254    #[test]
6255    fn mozjs_arrow_function_loc() {
6256        check_metrics::<MozjsParser>(
6257            "const add = (a, b) => a + b;
6258        const greet = name => {
6259            return 'Hello ' + name;
6260        };",
6261            "foo.js",
6262            |metric| {
6263                assert_eq!(metric.loc.sloc(), 4);
6264                assert_eq!(metric.loc.ploc(), 4);
6265                assert_eq!(metric.loc.lloc(), 3);
6266                assert_eq!(metric.loc.cloc(), 0);
6267                assert_eq!(metric.loc.blank(), 0);
6268                insta::assert_json_snapshot!(metric.loc);
6269            },
6270        );
6271    }
6272
6273    #[test]
6274    fn mozjs_multiple_functions_loc() {
6275        check_metrics::<MozjsParser>(
6276            "function f() {
6277            return 1;
6278        }
6279        function g() {
6280            return 2;
6281        }",
6282            "foo.js",
6283            |metric| {
6284                assert_eq!(metric.loc.sloc(), 6);
6285                assert_eq!(metric.loc.ploc(), 6);
6286                assert_eq!(metric.loc.lloc(), 2);
6287                assert_eq!(metric.loc.cloc(), 0);
6288                assert_eq!(metric.loc.blank(), 0);
6289                insta::assert_json_snapshot!(metric.loc);
6290            },
6291        );
6292    }
6293
6294    #[test]
6295    fn mozjs_nested_function_loc() {
6296        check_metrics::<MozjsParser>(
6297            "function outer() {
6298            function inner() {
6299                return 1;
6300            }
6301            return inner();
6302        }",
6303            "foo.js",
6304            |metric| {
6305                assert_eq!(metric.loc.sloc(), 6);
6306                assert_eq!(metric.loc.ploc(), 6);
6307                assert_eq!(metric.loc.lloc(), 2);
6308                assert_eq!(metric.loc.cloc(), 0);
6309                assert_eq!(metric.loc.blank(), 0);
6310                insta::assert_json_snapshot!(metric.loc);
6311            },
6312        );
6313    }
6314
6315    #[test]
6316    fn mozjs_if_lloc() {
6317        check_metrics::<MozjsParser>(
6318            "function f(x) {
6319            if (x > 0) {
6320                return 1;
6321            } else {
6322                return -1;
6323            }
6324        }",
6325            "foo.js",
6326            |metric| {
6327                assert_eq!(metric.loc.sloc(), 7);
6328                assert_eq!(metric.loc.ploc(), 7);
6329                // lloc = 3: the `if` statement plus the two `return`
6330                // statements. The three `{ … }` brace blocks (function
6331                // body, `if` consequent, `else` alternative) are syntactic
6332                // groupings, not logical statements, and contribute 0 —
6333                // matching C/Rust/Java for the equivalent code (#777).
6334                // Pre-#777 this asserted 6 (every StatementBlock counted).
6335                assert_eq!(metric.loc.lloc(), 3);
6336                assert_eq!(metric.loc.cloc(), 0);
6337                assert_eq!(metric.loc.blank(), 0);
6338                insta::assert_json_snapshot!(metric.loc);
6339            },
6340        );
6341    }
6342
6343    // Cross-language parity (#777): the same if/else function body yields
6344    // identical lloc across the JS family and the C-family / Rust baselines.
6345    // Removing `StatementBlock` from the JS-family lloc arms restored this
6346    // invariant — every brace block now contributes 0 lloc, as it always
6347    // had elsewhere. Pre-#777 the JS variants reported lloc 6 (three brace
6348    // blocks over-counted) against C's and Rust's 3.
6349    #[test]
6350    fn js_family_if_lloc_matches_c_and_rust() {
6351        const JS_SRC: &str = "function f(x) {
6352            if (x > 0) {
6353                return 1;
6354            } else {
6355                return -1;
6356            }
6357        }";
6358        const C_SRC: &str = "int f(int x) {
6359            if (x > 0) {
6360                return 1;
6361            } else {
6362                return -1;
6363            }
6364        }";
6365        const RUST_SRC: &str = "fn f(x: i32) -> i32 {
6366            if x > 0 {
6367                return 1;
6368            } else {
6369                return -1;
6370            }
6371        }";
6372
6373        // The logical-statement count is grammar-independent: one `if`
6374        // plus two `return`s, regardless of brace style or language.
6375        const EXPECTED_LLOC: usize = 3;
6376
6377        check_metrics::<CppParser>(C_SRC, "f.c", |m| {
6378            assert_eq!(m.loc.lloc() as usize, EXPECTED_LLOC);
6379        });
6380        check_metrics::<RustParser>(RUST_SRC, "f.rs", |m| {
6381            assert_eq!(m.loc.lloc() as usize, EXPECTED_LLOC);
6382        });
6383        check_metrics::<MozjsParser>(JS_SRC, "f.js", |m| {
6384            assert_eq!(m.loc.lloc() as usize, EXPECTED_LLOC);
6385        });
6386        check_metrics::<JavascriptParser>(JS_SRC, "f.js", |m| {
6387            assert_eq!(m.loc.lloc() as usize, EXPECTED_LLOC);
6388        });
6389        check_metrics::<TypescriptParser>(JS_SRC, "f.ts", |m| {
6390            assert_eq!(m.loc.lloc() as usize, EXPECTED_LLOC);
6391        });
6392        check_metrics::<TsxParser>(JS_SRC, "f.tsx", |m| {
6393            assert_eq!(m.loc.lloc() as usize, EXPECTED_LLOC);
6394        });
6395    }
6396
6397    #[test]
6398    fn mozjs_for_lloc() {
6399        check_metrics::<MozjsParser>(
6400            "function f(n) {
6401            var s = 0;
6402            for (var i = 0; i < n; i++) {
6403                s += i;
6404            }
6405            return s;
6406        }",
6407            "foo.js",
6408            |metric| {
6409                assert_eq!(metric.loc.sloc(), 7);
6410                assert_eq!(metric.loc.ploc(), 7);
6411                assert_eq!(metric.loc.lloc(), 4);
6412                assert_eq!(metric.loc.cloc(), 0);
6413                assert_eq!(metric.loc.blank(), 0);
6414                insta::assert_json_snapshot!(metric.loc);
6415            },
6416        );
6417    }
6418
6419    #[test]
6420    fn bash_blank() {
6421        check_metrics::<BashParser>(
6422            "#!/bin/bash
6423
6424        f() {
6425
6426            echo hello
6427
6428        }",
6429            "foo.sh",
6430            |metric| {
6431                assert_eq!(metric.loc.sloc(), 7);
6432                assert_eq!(metric.loc.ploc(), 3);
6433                assert_eq!(metric.loc.lloc(), 2);
6434                assert_eq!(metric.loc.cloc(), 1);
6435                assert_eq!(metric.loc.blank(), 3);
6436                insta::assert_json_snapshot!(metric.loc);
6437            },
6438        );
6439    }
6440
6441    #[test]
6442    fn bash_cloc() {
6443        check_metrics::<BashParser>(
6444            "# header comment
6445        f() {
6446            # body comment
6447            echo hello
6448        }",
6449            "foo.sh",
6450            |metric| {
6451                assert_eq!(metric.loc.sloc(), 5);
6452                assert_eq!(metric.loc.ploc(), 3);
6453                assert_eq!(metric.loc.lloc(), 2);
6454                assert_eq!(metric.loc.cloc(), 2);
6455                assert_eq!(metric.loc.blank(), 0);
6456                insta::assert_json_snapshot!(metric.loc);
6457            },
6458        );
6459    }
6460
6461    #[test]
6462    fn bash_no_zero_blank() {
6463        // Blank line interleaved with code that carries trailing comments —
6464        // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
6465        check_metrics::<BashParser>(
6466            "f() {
6467  echo a
6468
6469  echo b # trailing
6470  echo c # trailing
6471}",
6472            "foo.sh",
6473            |metric| {
6474                assert_eq!(metric.loc.sloc(), 6);
6475                assert_eq!(metric.loc.ploc(), 5);
6476                assert_eq!(metric.loc.cloc(), 2);
6477                assert_eq!(metric.loc.blank(), 1);
6478                insta::assert_json_snapshot!(metric.loc);
6479            },
6480        );
6481    }
6482
6483    #[test]
6484    fn bash_comment_before_code_line_reclassified() {
6485        // Regression for #547: a standalone `#` comment sitting on a line
6486        // that the grammar *also* anchors a zero-width code leaf to (the
6487        // empty `word` tree-sitter-bash emits inside a `$(...)` command
6488        // substitution that contains only a comment) must reclassify that
6489        // row from comment-only to code-comment. The Bash `Loc` leaf arm
6490        // previously omitted `check_comment_ends_on_code_line` (unlike
6491        // Elixir and every other impl), so the row was credited to BOTH
6492        // `ploc` and the comment-only set and `blank` was undercounted by
6493        // one.
6494        //
6495        // Source rows: 0 `echo a`, 1 blank, 2 `echo "$(`, 3 `  # c`,
6496        // 4 `)"`. expected: sloc=5 (every physical row),
6497        // ploc=4 (rows 0/2/3/4 — row 3 carries the phantom code leaf),
6498        // lloc=3, cloc=1 (row 3, now a code-comment line, not comment-only),
6499        // blank=1 (row 1). Without the fix `blank` collapses to 0 because
6500        // row 3 is double-counted. Verified fail-on-revert per
6501        // .claude/rules/testing.md.
6502        check_metrics::<BashParser>("echo a\n\necho \"$(\n  # c\n)\"\n", "foo.sh", |metric| {
6503            assert_eq!(metric.loc.sloc(), 5);
6504            assert_eq!(metric.loc.ploc(), 4);
6505            assert_eq!(metric.loc.lloc(), 3);
6506            assert_eq!(metric.loc.cloc(), 1);
6507            assert_eq!(metric.loc.blank(), 1);
6508            insta::assert_json_snapshot!(
6509                metric.loc,
6510                @r#"
6511                {
6512                  "sloc": 5,
6513                  "ploc": 4,
6514                  "lloc": 3,
6515                  "cloc": 1,
6516                  "blank": 1,
6517                  "sloc_average": 5.0,
6518                  "ploc_average": 4.0,
6519                  "lloc_average": 3.0,
6520                  "cloc_average": 1.0,
6521                  "blank_average": 1.0,
6522                  "sloc_min": 5,
6523                  "sloc_max": 5,
6524                  "cloc_min": 1,
6525                  "cloc_max": 1,
6526                  "ploc_min": 4,
6527                  "ploc_max": 4,
6528                  "lloc_min": 3,
6529                  "lloc_max": 3,
6530                  "blank_min": 1,
6531                  "blank_max": 1
6532                }
6533                "#
6534            );
6535        });
6536    }
6537
6538    #[test]
6539    fn bash_if_lloc() {
6540        check_metrics::<BashParser>(
6541            "f() {
6542            if [ $1 -gt 0 ]; then
6543                echo positive
6544            else
6545                echo negative
6546            fi
6547        }",
6548            "foo.sh",
6549            |metric| {
6550                assert_eq!(metric.loc.sloc(), 7);
6551                assert_eq!(metric.loc.ploc(), 7);
6552                assert_eq!(metric.loc.lloc(), 4);
6553                assert_eq!(metric.loc.cloc(), 0);
6554                assert_eq!(metric.loc.blank(), 0);
6555                insta::assert_json_snapshot!(metric.loc);
6556            },
6557        );
6558    }
6559
6560    #[test]
6561    fn bash_for_lloc() {
6562        check_metrics::<BashParser>(
6563            "f() {
6564            for i in 1 2 3; do
6565                echo $i
6566            done
6567        }",
6568            "foo.sh",
6569            |metric| {
6570                assert_eq!(metric.loc.sloc(), 5);
6571                assert_eq!(metric.loc.ploc(), 5);
6572                assert_eq!(metric.loc.lloc(), 3);
6573                assert_eq!(metric.loc.cloc(), 0);
6574                assert_eq!(metric.loc.blank(), 0);
6575                insta::assert_json_snapshot!(metric.loc);
6576            },
6577        );
6578    }
6579
6580    #[test]
6581    fn bash_while_lloc() {
6582        check_metrics::<BashParser>(
6583            "f() {
6584            local n=5
6585            while [ $n -gt 0 ]; do
6586                echo $n
6587                n=$((n - 1))
6588            done
6589        }",
6590            "foo.sh",
6591            |metric| {
6592                assert_eq!(metric.loc.sloc(), 7);
6593                assert_eq!(metric.loc.ploc(), 7);
6594                assert_eq!(metric.loc.lloc(), 4);
6595                assert_eq!(metric.loc.cloc(), 0);
6596                assert_eq!(metric.loc.blank(), 0);
6597                insta::assert_json_snapshot!(metric.loc);
6598            },
6599        );
6600    }
6601
6602    #[test]
6603    fn bash_case_lloc() {
6604        check_metrics::<BashParser>(
6605            "f() {
6606            case $1 in
6607                start) echo starting ;;
6608                stop)  echo stopping ;;
6609                *)     echo unknown  ;;
6610            esac
6611        }",
6612            "foo.sh",
6613            |metric| {
6614                assert_eq!(metric.loc.sloc(), 7);
6615                assert_eq!(metric.loc.ploc(), 7);
6616                assert_eq!(metric.loc.lloc(), 5);
6617                assert_eq!(metric.loc.cloc(), 0);
6618                assert_eq!(metric.loc.blank(), 0);
6619                insta::assert_json_snapshot!(metric.loc);
6620            },
6621        );
6622    }
6623
6624    #[test]
6625    fn bash_multiple_functions_loc() {
6626        check_metrics::<BashParser>(
6627            "f() {
6628            echo hello
6629        }
6630        g() {
6631            echo world
6632        }",
6633            "foo.sh",
6634            |metric| {
6635                assert_eq!(metric.loc.sloc(), 6);
6636                assert_eq!(metric.loc.ploc(), 6);
6637                assert_eq!(metric.loc.lloc(), 4);
6638                assert_eq!(metric.loc.cloc(), 0);
6639                assert_eq!(metric.loc.blank(), 0);
6640                insta::assert_json_snapshot!(metric.loc);
6641            },
6642        );
6643    }
6644
6645    #[test]
6646    fn bash_nested_function_loc() {
6647        check_metrics::<BashParser>(
6648            "outer() {
6649            inner() {
6650                echo inner
6651            }
6652            inner
6653            echo outer
6654        }",
6655            "foo.sh",
6656            |metric| {
6657                assert_eq!(metric.loc.sloc(), 7);
6658                assert_eq!(metric.loc.ploc(), 7);
6659                assert_eq!(metric.loc.lloc(), 5);
6660                assert_eq!(metric.loc.cloc(), 0);
6661                assert_eq!(metric.loc.blank(), 0);
6662                insta::assert_json_snapshot!(metric.loc);
6663            },
6664        );
6665    }
6666
6667    #[test]
6668    fn bash_heredoc_loc() {
6669        check_metrics::<BashParser>(
6670            "f() {
6671            cat <<EOF
6672line1
6673line2
6674EOF
6675        }",
6676            "foo.sh",
6677            |metric| {
6678                assert_eq!(metric.loc.sloc(), 6);
6679                assert_eq!(metric.loc.ploc(), 5);
6680                assert_eq!(metric.loc.lloc(), 2);
6681                assert_eq!(metric.loc.cloc(), 0);
6682                assert_eq!(metric.loc.blank(), 1);
6683                insta::assert_json_snapshot!(metric.loc);
6684            },
6685        );
6686    }
6687
6688    #[test]
6689    fn kotlin_loc_blank() {
6690        check_metrics::<KotlinParser>(
6691            "fun f(): Int {
6692
6693            val x = 1
6694
6695            return x
6696        }",
6697            "foo.kt",
6698            |metric| {
6699                assert_eq!(metric.loc.sloc(), 6);
6700                assert_eq!(metric.loc.ploc(), 4);
6701                assert_eq!(metric.loc.lloc(), 2);
6702                assert_eq!(metric.loc.cloc(), 0);
6703                assert_eq!(metric.loc.blank(), 2);
6704                insta::assert_json_snapshot!(metric.loc);
6705            },
6706        );
6707    }
6708
6709    #[test]
6710    fn kotlin_loc_cloc() {
6711        check_metrics::<KotlinParser>(
6712            "// header comment
6713        /* block
6714           comment */
6715        fun f(): Int {
6716            return 42 // inline
6717        }",
6718            "foo.kt",
6719            |metric| {
6720                assert_eq!(metric.loc.sloc(), 6);
6721                assert_eq!(metric.loc.ploc(), 3);
6722                assert_eq!(metric.loc.lloc(), 1);
6723                assert_eq!(metric.loc.cloc(), 4);
6724                assert_eq!(metric.loc.blank(), 0);
6725                insta::assert_json_snapshot!(metric.loc);
6726            },
6727        );
6728    }
6729
6730    #[test]
6731    fn kotlin_loc_no_zero_blank() {
6732        // Checks that the blank metric is not equal to 0 when there are some
6733        // comments next to code lines. Mirrors rust_no_zero_blank.
6734        check_metrics::<KotlinParser>(
6735            "fun connectToUpdateServer() {
6736              val pool = 0
6737
6738              val updateServer = -42
6739              val isConnected = false
6740              val currTry = 0
6741              val numRetries = 10  // Number of IPC connection retries before
6742                                    // giving up.
6743              val numTries = 20    // Number of IPC connection tries before
6744                                    // giving up.
6745            }",
6746            "foo.kt",
6747            |metric| {
6748                // Anchor the headline integer values; in particular
6749                // `blank() > 0` is the contract this test's name advertises.
6750                assert_eq!(metric.loc.sloc(), 11);
6751                assert_eq!(metric.loc.ploc(), 8);
6752                assert_eq!(metric.loc.cloc(), 4);
6753                assert_eq!(metric.loc.blank(), 1);
6754                insta::assert_json_snapshot!(
6755                    metric.loc,
6756                    @r#"
6757                {
6758                  "sloc": 11,
6759                  "ploc": 8,
6760                  "lloc": 6,
6761                  "cloc": 4,
6762                  "blank": 1,
6763                  "sloc_average": 5.5,
6764                  "ploc_average": 4.0,
6765                  "lloc_average": 3.0,
6766                  "cloc_average": 2.0,
6767                  "blank_average": 0.5,
6768                  "sloc_min": 11,
6769                  "sloc_max": 11,
6770                  "cloc_min": 4,
6771                  "cloc_max": 4,
6772                  "ploc_min": 8,
6773                  "ploc_max": 8,
6774                  "lloc_min": 6,
6775                  "lloc_max": 6,
6776                  "blank_min": 1,
6777                  "blank_max": 1
6778                }
6779                "#
6780                );
6781            },
6782        );
6783    }
6784
6785    #[test]
6786    fn kotlin_loc_blank_zero_sanity() {
6787        // Sanity: when the source has no blank lines, blank() must be 0.
6788        // Preserves the no-blank coverage previously held by
6789        // kotlin_loc_no_zero_blank before it was rewritten to assert the
6790        // positive case its name advertises.
6791        check_metrics::<KotlinParser>(
6792            "fun f(): Int {
6793            val x = 1 // x
6794            val y = 2 // y
6795            return x + y
6796        }",
6797            "foo.kt",
6798            |metric| {
6799                assert_eq!(metric.loc.sloc(), 5);
6800                assert_eq!(metric.loc.ploc(), 5);
6801                assert_eq!(metric.loc.lloc(), 3);
6802                assert_eq!(metric.loc.cloc(), 2);
6803                assert_eq!(metric.loc.blank(), 0);
6804            },
6805        );
6806    }
6807
6808    #[test]
6809    fn kotlin_loc_if_lloc() {
6810        check_metrics::<KotlinParser>(
6811            "fun classify(n: Int): String {
6812            if (n > 0) {
6813                return \"positive\"
6814            } else if (n < 0) {
6815                return \"negative\"
6816            }
6817            return \"zero\"
6818        }",
6819            "foo.kt",
6820            |metric| {
6821                assert_eq!(metric.loc.sloc(), 8);
6822                assert_eq!(metric.loc.ploc(), 8);
6823                assert_eq!(metric.loc.lloc(), 5);
6824                assert_eq!(metric.loc.cloc(), 0);
6825                assert_eq!(metric.loc.blank(), 0);
6826                insta::assert_json_snapshot!(metric.loc);
6827            },
6828        );
6829    }
6830
6831    #[test]
6832    fn kotlin_loc_for_lloc() {
6833        check_metrics::<KotlinParser>(
6834            "fun sum(n: Int): Int {
6835            var s = 0
6836            for (i in 1..n) {
6837                s += i
6838            }
6839            return s
6840        }",
6841            "foo.kt",
6842            |metric| {
6843                assert_eq!(metric.loc.sloc(), 7);
6844                assert_eq!(metric.loc.ploc(), 7);
6845                assert_eq!(metric.loc.lloc(), 4);
6846                assert_eq!(metric.loc.cloc(), 0);
6847                assert_eq!(metric.loc.blank(), 0);
6848                insta::assert_json_snapshot!(metric.loc);
6849            },
6850        );
6851    }
6852
6853    #[test]
6854    fn kotlin_loc_when_lloc() {
6855        check_metrics::<KotlinParser>(
6856            "fun describe(x: Int): String {
6857            return when (x) {
6858                1 -> \"one\"
6859                2 -> \"two\"
6860                else -> \"other\"
6861            }
6862        }",
6863            "foo.kt",
6864            |metric| {
6865                assert_eq!(metric.loc.sloc(), 7);
6866                assert_eq!(metric.loc.ploc(), 7);
6867                assert_eq!(metric.loc.lloc(), 2);
6868                assert_eq!(metric.loc.cloc(), 0);
6869                assert_eq!(metric.loc.blank(), 0);
6870                insta::assert_json_snapshot!(metric.loc);
6871            },
6872        );
6873    }
6874
6875    #[test]
6876    fn kotlin_loc_lambda_lloc() {
6877        check_metrics::<KotlinParser>(
6878            "fun f(list: List<Int>): List<Int> {
6879            return list.filter { it > 0 }
6880                       .map { it * 2 }
6881        }",
6882            "foo.kt",
6883            |metric| {
6884                assert_eq!(metric.loc.sloc(), 4);
6885                assert_eq!(metric.loc.ploc(), 4);
6886                assert_eq!(metric.loc.lloc(), 1);
6887                assert_eq!(metric.loc.cloc(), 0);
6888                assert_eq!(metric.loc.blank(), 0);
6889                insta::assert_json_snapshot!(metric.loc);
6890            },
6891        );
6892    }
6893
6894    #[test]
6895    fn kotlin_loc_class_loc() {
6896        check_metrics::<KotlinParser>(
6897            "class Counter {
6898            private var count = 0
6899            fun increment() { count++ }
6900            fun get(): Int = count
6901        }",
6902            "foo.kt",
6903            |metric| {
6904                assert_eq!(metric.loc.sloc(), 5);
6905                assert_eq!(metric.loc.ploc(), 5);
6906                assert_eq!(metric.loc.lloc(), 1);
6907                assert_eq!(metric.loc.cloc(), 0);
6908                assert_eq!(metric.loc.blank(), 0);
6909                insta::assert_json_snapshot!(metric.loc);
6910            },
6911        );
6912    }
6913
6914    #[test]
6915    fn kotlin_loc_multiple_functions_loc() {
6916        check_metrics::<KotlinParser>(
6917            "fun f(): Int {
6918            return 1
6919        }
6920        fun g(): Int {
6921            return 2
6922        }",
6923            "foo.kt",
6924            |metric| {
6925                assert_eq!(metric.loc.sloc(), 6);
6926                assert_eq!(metric.loc.ploc(), 6);
6927                assert_eq!(metric.loc.lloc(), 2);
6928                assert_eq!(metric.loc.cloc(), 0);
6929                assert_eq!(metric.loc.blank(), 0);
6930                insta::assert_json_snapshot!(metric.loc);
6931            },
6932        );
6933    }
6934
6935    #[test]
6936    fn kotlin_loc_while_lloc() {
6937        check_metrics::<KotlinParser>(
6938            "fun countdown(n: Int) {
6939            var i = n
6940            while (i > 0) {
6941                println(i)
6942                i--
6943            }
6944        }",
6945            "foo.kt",
6946            |metric| {
6947                assert_eq!(metric.loc.sloc(), 7);
6948                assert_eq!(metric.loc.ploc(), 7);
6949                assert_eq!(metric.loc.lloc(), 3);
6950                assert_eq!(metric.loc.cloc(), 0);
6951                assert_eq!(metric.loc.blank(), 0);
6952                insta::assert_json_snapshot!(metric.loc);
6953            },
6954        );
6955    }
6956
6957    #[test]
6958    fn typescript_blank() {
6959        check_metrics::<TypescriptParser>(
6960            "function f(): void {
6961
6962            const x = 1;
6963
6964        }",
6965            "foo.ts",
6966            |metric| {
6967                assert_eq!(metric.loc.sloc(), 5);
6968                assert_eq!(metric.loc.ploc(), 3);
6969                assert_eq!(metric.loc.lloc(), 1);
6970                assert_eq!(metric.loc.cloc(), 0);
6971                assert_eq!(metric.loc.blank(), 2);
6972                insta::assert_json_snapshot!(metric.loc);
6973            },
6974        );
6975    }
6976
6977    #[test]
6978    fn typescript_cloc() {
6979        check_metrics::<TypescriptParser>(
6980            "// header
6981        /* block
6982           comment */
6983        function f(): number {
6984            return 42; // inline
6985        }",
6986            "foo.ts",
6987            |metric| {
6988                assert_eq!(metric.loc.sloc(), 6);
6989                assert_eq!(metric.loc.ploc(), 3);
6990                assert_eq!(metric.loc.lloc(), 1);
6991                assert_eq!(metric.loc.cloc(), 4);
6992                assert_eq!(metric.loc.blank(), 0);
6993                insta::assert_json_snapshot!(metric.loc);
6994            },
6995        );
6996    }
6997
6998    #[test]
6999    fn typescript_no_zero_blank() {
7000        // Blank line interleaved with code that carries trailing comments —
7001        // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
7002        check_metrics::<TypescriptParser>(
7003            "function f(): void {
7004  const a = 1;
7005
7006  const b = 2; // trailing
7007  const c = 3; // trailing
7008}",
7009            "foo.ts",
7010            |metric| {
7011                assert_eq!(metric.loc.sloc(), 6);
7012                assert_eq!(metric.loc.ploc(), 5);
7013                assert_eq!(metric.loc.cloc(), 2);
7014                assert_eq!(metric.loc.blank(), 1);
7015                insta::assert_json_snapshot!(metric.loc);
7016            },
7017        );
7018    }
7019
7020    #[test]
7021    fn typescript_if_lloc() {
7022        check_metrics::<TypescriptParser>(
7023            "function classify(n: number): string {
7024            if (n > 0) {
7025                return 'positive';
7026            } else {
7027                return 'non-positive';
7028            }
7029        }",
7030            "foo.ts",
7031            |metric| {
7032                assert_eq!(metric.loc.sloc(), 7);
7033                assert_eq!(metric.loc.ploc(), 7);
7034                assert_eq!(metric.loc.lloc(), 3);
7035                assert_eq!(metric.loc.cloc(), 0);
7036                assert_eq!(metric.loc.blank(), 0);
7037                insta::assert_json_snapshot!(metric.loc);
7038            },
7039        );
7040    }
7041
7042    #[test]
7043    fn typescript_for_lloc() {
7044        check_metrics::<TypescriptParser>(
7045            "function sum(n: number): number {
7046            let s = 0;
7047            for (let i = 0; i < n; i++) {
7048                s += i;
7049            }
7050            return s;
7051        }",
7052            "foo.ts",
7053            |metric| {
7054                assert_eq!(metric.loc.sloc(), 7);
7055                assert_eq!(metric.loc.ploc(), 7);
7056                assert_eq!(metric.loc.lloc(), 4);
7057                assert_eq!(metric.loc.cloc(), 0);
7058                assert_eq!(metric.loc.blank(), 0);
7059                insta::assert_json_snapshot!(metric.loc);
7060            },
7061        );
7062    }
7063
7064    #[test]
7065    fn typescript_while_lloc() {
7066        check_metrics::<TypescriptParser>(
7067            "function countdown(n: number): void {
7068            let i = n;
7069            while (i > 0) {
7070                console.log(i);
7071                i--;
7072            }
7073        }",
7074            "foo.ts",
7075            |metric| {
7076                assert_eq!(metric.loc.sloc(), 7);
7077                assert_eq!(metric.loc.ploc(), 7);
7078                assert_eq!(metric.loc.lloc(), 4);
7079                assert_eq!(metric.loc.cloc(), 0);
7080                assert_eq!(metric.loc.blank(), 0);
7081                insta::assert_json_snapshot!(metric.loc);
7082            },
7083        );
7084    }
7085
7086    #[test]
7087    fn typescript_switch_lloc() {
7088        check_metrics::<TypescriptParser>(
7089            "function describe(x: number): string {
7090            switch (x) {
7091                case 1: return 'one';
7092                case 2: return 'two';
7093                default: return 'other';
7094            }
7095        }",
7096            "foo.ts",
7097            |metric| {
7098                assert_eq!(metric.loc.sloc(), 7);
7099                assert_eq!(metric.loc.ploc(), 7);
7100                assert_eq!(metric.loc.lloc(), 4);
7101                assert_eq!(metric.loc.cloc(), 0);
7102                assert_eq!(metric.loc.blank(), 0);
7103                insta::assert_json_snapshot!(metric.loc);
7104            },
7105        );
7106    }
7107
7108    #[test]
7109    fn typescript_class_loc() {
7110        check_metrics::<TypescriptParser>(
7111            "class Counter {
7112            private count: number = 0;
7113            increment(): void { this.count++; }
7114            get(): number { return this.count; }
7115        }",
7116            "foo.ts",
7117            |metric| {
7118                assert_eq!(metric.loc.sloc(), 5);
7119                assert_eq!(metric.loc.ploc(), 5);
7120                assert_eq!(metric.loc.lloc(), 2);
7121                assert_eq!(metric.loc.cloc(), 0);
7122                assert_eq!(metric.loc.blank(), 0);
7123                insta::assert_json_snapshot!(metric.loc);
7124            },
7125        );
7126    }
7127
7128    #[test]
7129    fn typescript_arrow_function_loc() {
7130        check_metrics::<TypescriptParser>(
7131            "const add = (a: number, b: number): number => a + b;
7132        const greet = (name: string): string => {
7133            return `Hello, ${name}`;
7134        };",
7135            "foo.ts",
7136            |metric| {
7137                assert_eq!(metric.loc.sloc(), 4);
7138                assert_eq!(metric.loc.ploc(), 4);
7139                assert_eq!(metric.loc.lloc(), 3);
7140                assert_eq!(metric.loc.cloc(), 0);
7141                assert_eq!(metric.loc.blank(), 0);
7142                insta::assert_json_snapshot!(metric.loc);
7143            },
7144        );
7145    }
7146
7147    #[test]
7148    fn typescript_interface_loc() {
7149        check_metrics::<TypescriptParser>(
7150            "interface Shape {
7151            area(): number;
7152            perimeter(): number;
7153        }
7154        function describe(s: Shape): string {
7155            return `area=${s.area()}`;
7156        }",
7157            "foo.ts",
7158            |metric| {
7159                assert_eq!(metric.loc.sloc(), 7);
7160                assert_eq!(metric.loc.ploc(), 7);
7161                assert_eq!(metric.loc.lloc(), 1);
7162                assert_eq!(metric.loc.cloc(), 0);
7163                assert_eq!(metric.loc.blank(), 0);
7164                insta::assert_json_snapshot!(metric.loc);
7165            },
7166        );
7167    }
7168
7169    #[test]
7170    fn typescript_multiple_functions_loc() {
7171        check_metrics::<TypescriptParser>(
7172            "function f(): number {
7173            return 1;
7174        }
7175        function g(): number {
7176            return 2;
7177        }
7178        function h(): number {
7179            return 3;
7180        }",
7181            "foo.ts",
7182            |metric| {
7183                assert_eq!(metric.loc.sloc(), 9);
7184                assert_eq!(metric.loc.ploc(), 9);
7185                assert_eq!(metric.loc.lloc(), 3);
7186                assert_eq!(metric.loc.cloc(), 0);
7187                assert_eq!(metric.loc.blank(), 0);
7188                insta::assert_json_snapshot!(metric.loc);
7189            },
7190        );
7191    }
7192
7193    #[test]
7194    fn typescript_try_catch_lloc() {
7195        check_metrics::<TypescriptParser>(
7196            "function safe(x: number): number {
7197            try {
7198                return 1 / x;
7199            } catch (e) {
7200                return 0;
7201            }
7202        }",
7203            "foo.ts",
7204            |metric| {
7205                assert_eq!(metric.loc.sloc(), 7);
7206                assert_eq!(metric.loc.ploc(), 7);
7207                assert_eq!(metric.loc.lloc(), 3);
7208                assert_eq!(metric.loc.cloc(), 0);
7209                assert_eq!(metric.loc.blank(), 0);
7210                insta::assert_json_snapshot!(metric.loc);
7211            },
7212        );
7213    }
7214
7215    #[test]
7216    fn typescript_nested_functions_loc() {
7217        check_metrics::<TypescriptParser>(
7218            "function outer(x: number): number {
7219            function inner(y: number): number {
7220                return y * 2;
7221            }
7222            return inner(x) + 1;
7223        }",
7224            "foo.ts",
7225            |metric| {
7226                assert_eq!(metric.loc.sloc(), 6);
7227                assert_eq!(metric.loc.ploc(), 6);
7228                assert_eq!(metric.loc.lloc(), 2);
7229                assert_eq!(metric.loc.cloc(), 0);
7230                assert_eq!(metric.loc.blank(), 0);
7231                insta::assert_json_snapshot!(metric.loc);
7232            },
7233        );
7234    }
7235
7236    #[test]
7237    fn typescript_generic_function_loc() {
7238        check_metrics::<TypescriptParser>(
7239            "function identity<T>(value: T): T {
7240            return value;
7241        }
7242        function first<T>(arr: T[]): T | undefined {
7243            return arr[0];
7244        }",
7245            "foo.ts",
7246            |metric| {
7247                assert_eq!(metric.loc.sloc(), 6);
7248                assert_eq!(metric.loc.ploc(), 6);
7249                assert_eq!(metric.loc.lloc(), 2);
7250                assert_eq!(metric.loc.cloc(), 0);
7251                assert_eq!(metric.loc.blank(), 0);
7252                insta::assert_json_snapshot!(metric.loc);
7253            },
7254        );
7255    }
7256
7257    #[test]
7258    fn tsx_blank() {
7259        check_metrics::<TsxParser>(
7260            "function f(): void {
7261
7262            const x = 1;
7263
7264        }",
7265            "foo.tsx",
7266            |metric| {
7267                assert_eq!(metric.loc.sloc(), 5);
7268                assert_eq!(metric.loc.ploc(), 3);
7269                assert_eq!(metric.loc.lloc(), 1);
7270                assert_eq!(metric.loc.cloc(), 0);
7271                assert_eq!(metric.loc.blank(), 2);
7272                insta::assert_json_snapshot!(metric.loc);
7273            },
7274        );
7275    }
7276
7277    #[test]
7278    fn tsx_cloc() {
7279        check_metrics::<TsxParser>(
7280            "// header
7281        /* block
7282           comment */
7283        function f(): number {
7284            return 42; // inline
7285        }",
7286            "foo.tsx",
7287            |metric| {
7288                assert_eq!(metric.loc.sloc(), 6);
7289                assert_eq!(metric.loc.ploc(), 3);
7290                assert_eq!(metric.loc.lloc(), 1);
7291                assert_eq!(metric.loc.cloc(), 4);
7292                assert_eq!(metric.loc.blank(), 0);
7293                insta::assert_json_snapshot!(metric.loc);
7294            },
7295        );
7296    }
7297
7298    #[test]
7299    fn tsx_no_zero_blank() {
7300        // Blank line interleaved with code that carries trailing comments —
7301        // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
7302        check_metrics::<TsxParser>(
7303            "function f(): void {
7304  const a = 1;
7305
7306  const b = 2; // trailing
7307  const c = 3; // trailing
7308}",
7309            "foo.tsx",
7310            |metric| {
7311                assert_eq!(metric.loc.sloc(), 6);
7312                assert_eq!(metric.loc.ploc(), 5);
7313                assert_eq!(metric.loc.cloc(), 2);
7314                assert_eq!(metric.loc.blank(), 1);
7315                insta::assert_json_snapshot!(metric.loc);
7316            },
7317        );
7318    }
7319
7320    #[test]
7321    fn tsx_if_lloc() {
7322        check_metrics::<TsxParser>(
7323            "function classify(n: number): string {
7324            if (n > 0) {
7325                return 'positive';
7326            } else {
7327                return 'non-positive';
7328            }
7329        }",
7330            "foo.tsx",
7331            |metric| {
7332                assert_eq!(metric.loc.sloc(), 7);
7333                assert_eq!(metric.loc.ploc(), 7);
7334                assert_eq!(metric.loc.lloc(), 3);
7335                assert_eq!(metric.loc.cloc(), 0);
7336                assert_eq!(metric.loc.blank(), 0);
7337                insta::assert_json_snapshot!(metric.loc);
7338            },
7339        );
7340    }
7341
7342    #[test]
7343    fn tsx_for_lloc() {
7344        check_metrics::<TsxParser>(
7345            "function sum(n: number): number {
7346            let s = 0;
7347            for (let i = 0; i < n; i++) {
7348                s += i;
7349            }
7350            return s;
7351        }",
7352            "foo.tsx",
7353            |metric| {
7354                assert_eq!(metric.loc.sloc(), 7);
7355                assert_eq!(metric.loc.ploc(), 7);
7356                assert_eq!(metric.loc.lloc(), 4);
7357                assert_eq!(metric.loc.cloc(), 0);
7358                assert_eq!(metric.loc.blank(), 0);
7359                insta::assert_json_snapshot!(metric.loc);
7360            },
7361        );
7362    }
7363
7364    #[test]
7365    fn tsx_while_lloc() {
7366        check_metrics::<TsxParser>(
7367            "function countdown(n: number): void {
7368            let i = n;
7369            while (i > 0) {
7370                console.log(i);
7371                i--;
7372            }
7373        }",
7374            "foo.tsx",
7375            |metric| {
7376                assert_eq!(metric.loc.sloc(), 7);
7377                assert_eq!(metric.loc.ploc(), 7);
7378                assert_eq!(metric.loc.lloc(), 4);
7379                assert_eq!(metric.loc.cloc(), 0);
7380                assert_eq!(metric.loc.blank(), 0);
7381                insta::assert_json_snapshot!(metric.loc);
7382            },
7383        );
7384    }
7385
7386    #[test]
7387    fn tsx_switch_lloc() {
7388        check_metrics::<TsxParser>(
7389            "function describe(x: number): string {
7390            switch (x) {
7391                case 1: return 'one';
7392                case 2: return 'two';
7393                default: return 'other';
7394            }
7395        }",
7396            "foo.tsx",
7397            |metric| {
7398                assert_eq!(metric.loc.sloc(), 7);
7399                assert_eq!(metric.loc.ploc(), 7);
7400                assert_eq!(metric.loc.lloc(), 4);
7401                assert_eq!(metric.loc.cloc(), 0);
7402                assert_eq!(metric.loc.blank(), 0);
7403                insta::assert_json_snapshot!(metric.loc);
7404            },
7405        );
7406    }
7407
7408    #[test]
7409    fn tsx_class_loc() {
7410        check_metrics::<TsxParser>(
7411            "class Counter {
7412            private count: number = 0;
7413            increment(): void { this.count++; }
7414            get(): number { return this.count; }
7415        }",
7416            "foo.tsx",
7417            |metric| {
7418                assert_eq!(metric.loc.sloc(), 5);
7419                assert_eq!(metric.loc.ploc(), 5);
7420                assert_eq!(metric.loc.lloc(), 2);
7421                assert_eq!(metric.loc.cloc(), 0);
7422                assert_eq!(metric.loc.blank(), 0);
7423                insta::assert_json_snapshot!(metric.loc);
7424            },
7425        );
7426    }
7427
7428    #[test]
7429    fn tsx_arrow_function_loc() {
7430        check_metrics::<TsxParser>(
7431            "const add = (a: number, b: number): number => a + b;
7432        const greet = (name: string): string => {
7433            return `Hello, ${name}`;
7434        };",
7435            "foo.tsx",
7436            |metric| {
7437                assert_eq!(metric.loc.sloc(), 4);
7438                assert_eq!(metric.loc.ploc(), 4);
7439                assert_eq!(metric.loc.lloc(), 3);
7440                assert_eq!(metric.loc.cloc(), 0);
7441                assert_eq!(metric.loc.blank(), 0);
7442                insta::assert_json_snapshot!(metric.loc);
7443            },
7444        );
7445    }
7446
7447    #[test]
7448    fn tsx_multiple_functions_loc() {
7449        check_metrics::<TsxParser>(
7450            "function f(): number {
7451            return 1;
7452        }
7453        function g(): number {
7454            return 2;
7455        }
7456        function h(): number {
7457            return 3;
7458        }",
7459            "foo.tsx",
7460            |metric| {
7461                assert_eq!(metric.loc.sloc(), 9);
7462                assert_eq!(metric.loc.ploc(), 9);
7463                assert_eq!(metric.loc.lloc(), 3);
7464                assert_eq!(metric.loc.cloc(), 0);
7465                assert_eq!(metric.loc.blank(), 0);
7466                insta::assert_json_snapshot!(metric.loc);
7467            },
7468        );
7469    }
7470
7471    #[test]
7472    fn tsx_try_catch_lloc() {
7473        check_metrics::<TsxParser>(
7474            "function safe(x: number): number {
7475            try {
7476                return 1 / x;
7477            } catch (e) {
7478                return 0;
7479            }
7480        }",
7481            "foo.tsx",
7482            |metric| {
7483                assert_eq!(metric.loc.sloc(), 7);
7484                assert_eq!(metric.loc.ploc(), 7);
7485                assert_eq!(metric.loc.lloc(), 3);
7486                assert_eq!(metric.loc.cloc(), 0);
7487                assert_eq!(metric.loc.blank(), 0);
7488                insta::assert_json_snapshot!(metric.loc);
7489            },
7490        );
7491    }
7492
7493    #[test]
7494    fn tsx_nested_functions_loc() {
7495        check_metrics::<TsxParser>(
7496            "function outer(x: number): number {
7497            function inner(y: number): number {
7498                return y * 2;
7499            }
7500            return inner(x) + 1;
7501        }",
7502            "foo.tsx",
7503            |metric| {
7504                assert_eq!(metric.loc.sloc(), 6);
7505                assert_eq!(metric.loc.ploc(), 6);
7506                assert_eq!(metric.loc.lloc(), 2);
7507                assert_eq!(metric.loc.cloc(), 0);
7508                assert_eq!(metric.loc.blank(), 0);
7509                insta::assert_json_snapshot!(metric.loc);
7510            },
7511        );
7512    }
7513
7514    #[test]
7515    fn tsx_interface_loc() {
7516        check_metrics::<TsxParser>(
7517            "interface Shape {
7518            area(): number;
7519            perimeter(): number;
7520        }
7521        function describe(s: Shape): string {
7522            return `area=${s.area()}`;
7523        }",
7524            "foo.tsx",
7525            |metric| {
7526                assert_eq!(metric.loc.sloc(), 7);
7527                assert_eq!(metric.loc.ploc(), 7);
7528                assert_eq!(metric.loc.lloc(), 1);
7529                assert_eq!(metric.loc.cloc(), 0);
7530                assert_eq!(metric.loc.blank(), 0);
7531                insta::assert_json_snapshot!(metric.loc);
7532            },
7533        );
7534    }
7535
7536    #[test]
7537    fn tsx_generic_function_loc() {
7538        check_metrics::<TsxParser>(
7539            "function identity<T>(value: T): T {
7540            return value;
7541        }
7542        function first<T>(arr: T[]): T | undefined {
7543            return arr[0];
7544        }",
7545            "foo.tsx",
7546            |metric| {
7547                assert_eq!(metric.loc.sloc(), 6);
7548                assert_eq!(metric.loc.ploc(), 6);
7549                assert_eq!(metric.loc.lloc(), 2);
7550                assert_eq!(metric.loc.cloc(), 0);
7551                assert_eq!(metric.loc.blank(), 0);
7552                insta::assert_json_snapshot!(metric.loc);
7553            },
7554        );
7555    }
7556
7557    #[test]
7558    fn php_blank() {
7559        check_metrics::<PhpParser>(
7560            "<?php
7561
7562$a = 1;
7563
7564$b = 2;
7565
7566",
7567            "foo.php",
7568            |metric| {
7569                assert_eq!(metric.loc.sloc(), 5);
7570                assert_eq!(metric.loc.ploc(), 3);
7571                assert_eq!(metric.loc.lloc(), 2);
7572                assert_eq!(metric.loc.cloc(), 0);
7573                assert_eq!(metric.loc.blank(), 2);
7574                insta::assert_json_snapshot!(metric.loc);
7575            },
7576        );
7577    }
7578
7579    #[test]
7580    fn php_no_zero_blank() {
7581        // Blank line interleaved with code that carries trailing comments —
7582        // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
7583        check_metrics::<PhpParser>(
7584            "<?php
7585$a = 1;
7586
7587$b = 2; // trailing
7588$c = 3; // trailing
7589",
7590            "foo.php",
7591            |metric| {
7592                assert_eq!(metric.loc.sloc(), 5);
7593                assert_eq!(metric.loc.ploc(), 4);
7594                assert_eq!(metric.loc.cloc(), 2);
7595                assert_eq!(metric.loc.blank(), 1);
7596                insta::assert_json_snapshot!(metric.loc);
7597            },
7598        );
7599    }
7600
7601    #[test]
7602    fn php_cloc_double_slash() {
7603        check_metrics::<PhpParser>(
7604            "<?php
7605// first
7606// second
7607$a = 1; // trailing",
7608            "foo.php",
7609            |metric| {
7610                assert_eq!(metric.loc.sloc(), 4);
7611                assert_eq!(metric.loc.ploc(), 2);
7612                assert_eq!(metric.loc.lloc(), 1);
7613                assert_eq!(metric.loc.cloc(), 3);
7614                assert_eq!(metric.loc.blank(), 0);
7615                insta::assert_json_snapshot!(metric.loc);
7616            },
7617        );
7618    }
7619
7620    #[test]
7621    fn php_cloc_hash() {
7622        check_metrics::<PhpParser>(
7623            "<?php
7624# first
7625# second
7626$a = 1;",
7627            "foo.php",
7628            |metric| {
7629                assert_eq!(metric.loc.sloc(), 4);
7630                assert_eq!(metric.loc.ploc(), 2);
7631                assert_eq!(metric.loc.lloc(), 1);
7632                assert_eq!(metric.loc.cloc(), 2);
7633                assert_eq!(metric.loc.blank(), 0);
7634                insta::assert_json_snapshot!(metric.loc);
7635            },
7636        );
7637    }
7638
7639    #[test]
7640    fn php_cloc_block() {
7641        check_metrics::<PhpParser>(
7642            "<?php
7643/*
7644 * block
7645 * comment
7646 */
7647$a = 1;",
7648            "foo.php",
7649            |metric| {
7650                assert_eq!(metric.loc.sloc(), 6);
7651                assert_eq!(metric.loc.ploc(), 2);
7652                assert_eq!(metric.loc.lloc(), 1);
7653                assert_eq!(metric.loc.cloc(), 4);
7654                assert_eq!(metric.loc.blank(), 0);
7655                insta::assert_json_snapshot!(metric.loc);
7656            },
7657        );
7658    }
7659
7660    #[test]
7661    fn php_lloc() {
7662        // Three statements: assignment, if (with body), echo.
7663        check_metrics::<PhpParser>(
7664            "<?php
7665$a = 1;
7666if ($a > 0) {
7667    echo $a;
7668}",
7669            "foo.php",
7670            |metric| {
7671                assert_eq!(metric.loc.sloc(), 5);
7672                assert_eq!(metric.loc.ploc(), 5);
7673                assert_eq!(metric.loc.lloc(), 3);
7674                assert_eq!(metric.loc.cloc(), 0);
7675                assert_eq!(metric.loc.blank(), 0);
7676                insta::assert_json_snapshot!(metric.loc);
7677            },
7678        );
7679    }
7680
7681    #[test]
7682    fn php_no_parenthesized_expression_lloc() {
7683        // Parenthesized expression should not add an extra LLOC over the
7684        // surrounding expression_statement.
7685        check_metrics::<PhpParser>(
7686            "<?php
7687$a = (1 + 2);",
7688            "foo.php",
7689            |metric| {
7690                assert_eq!(metric.loc.sloc(), 2);
7691                assert_eq!(metric.loc.ploc(), 2);
7692                assert_eq!(metric.loc.lloc(), 1);
7693                assert_eq!(metric.loc.cloc(), 0);
7694                assert_eq!(metric.loc.blank(), 0);
7695                insta::assert_json_snapshot!(metric.loc);
7696            },
7697        );
7698    }
7699
7700    #[test]
7701    fn php_no_compound_statement_lloc() {
7702        // Block wrappers (`{ … }`) are not LLOC themselves.
7703        check_metrics::<PhpParser>(
7704            "<?php
7705function f(): void {
7706    $a = 1;
7707}",
7708            "foo.php",
7709            |metric| {
7710                assert_eq!(metric.loc.sloc(), 4);
7711                assert_eq!(metric.loc.ploc(), 4);
7712                assert_eq!(metric.loc.lloc(), 1);
7713                assert_eq!(metric.loc.cloc(), 0);
7714                assert_eq!(metric.loc.blank(), 0);
7715                insta::assert_json_snapshot!(metric.loc);
7716            },
7717        );
7718    }
7719
7720    #[test]
7721    fn php_no_colon_block_lloc() {
7722        // Alternative syntax (`if: … endif;`) uses ColonBlock instead of
7723        // CompoundStatement; it is also not LLOC.
7724        check_metrics::<PhpParser>(
7725            "<?php
7726if (true):
7727    $a = 1;
7728endif;",
7729            "foo.php",
7730            |metric| {
7731                assert_eq!(metric.loc.sloc(), 4);
7732                assert_eq!(metric.loc.ploc(), 4);
7733                assert_eq!(metric.loc.lloc(), 2);
7734                assert_eq!(metric.loc.cloc(), 0);
7735                assert_eq!(metric.loc.blank(), 0);
7736                insta::assert_json_snapshot!(metric.loc);
7737            },
7738        );
7739    }
7740
7741    #[test]
7742    fn php_no_else_clause_lloc() {
7743        // ElseClause and ElseIfClause are sub-parts of IfStatement.
7744        check_metrics::<PhpParser>(
7745            "<?php
7746if ($x) {
7747    $a = 1;
7748} elseif ($y) {
7749    $a = 2;
7750} else {
7751    $a = 3;
7752}",
7753            "foo.php",
7754            |metric| {
7755                assert_eq!(metric.loc.sloc(), 8);
7756                assert_eq!(metric.loc.ploc(), 8);
7757                assert_eq!(metric.loc.lloc(), 4);
7758                assert_eq!(metric.loc.cloc(), 0);
7759                assert_eq!(metric.loc.blank(), 0);
7760                insta::assert_json_snapshot!(metric.loc);
7761            },
7762        );
7763    }
7764
7765    #[test]
7766    fn php_no_case_statement_lloc() {
7767        // CaseStatement / DefaultStatement are switch arms, not separate
7768        // statements.
7769        check_metrics::<PhpParser>(
7770            "<?php
7771switch ($x) {
7772    case 1:
7773        $a = 1;
7774        break;
7775    case 2:
7776        $a = 2;
7777        break;
7778    default:
7779        $a = 0;
7780}",
7781            "foo.php",
7782            |metric| {
7783                assert_eq!(metric.loc.sloc(), 11);
7784                assert_eq!(metric.loc.ploc(), 11);
7785                assert_eq!(metric.loc.lloc(), 6);
7786                assert_eq!(metric.loc.cloc(), 0);
7787                assert_eq!(metric.loc.blank(), 0);
7788                insta::assert_json_snapshot!(metric.loc);
7789            },
7790        );
7791    }
7792
7793    #[test]
7794    fn php_no_match_arm_lloc() {
7795        // MatchConditionalExpression / MatchDefaultExpression are arms;
7796        // only the surrounding expression_statement counts.
7797        check_metrics::<PhpParser>(
7798            "<?php
7799$a = match ($x) {
7800    1 => 'one',
7801    2 => 'two',
7802    default => 'other',
7803};",
7804            "foo.php",
7805            |metric| {
7806                assert_eq!(metric.loc.sloc(), 6);
7807                assert_eq!(metric.loc.ploc(), 6);
7808                assert_eq!(metric.loc.lloc(), 1);
7809                assert_eq!(metric.loc.cloc(), 0);
7810                assert_eq!(metric.loc.blank(), 0);
7811                insta::assert_json_snapshot!(metric.loc);
7812            },
7813        );
7814    }
7815
7816    #[test]
7817    fn php_no_throw_in_expression_lloc() {
7818        // PHP 8 `throw` as expression: only the surrounding statement
7819        // counts (the `??` in this example), not the throw_expression.
7820        check_metrics::<PhpParser>(
7821            "<?php
7822$x = $y ?? throw new \\Exception('nope');",
7823            "foo.php",
7824            |metric| {
7825                assert_eq!(metric.loc.sloc(), 2);
7826                assert_eq!(metric.loc.ploc(), 2);
7827                assert_eq!(metric.loc.lloc(), 1);
7828                assert_eq!(metric.loc.cloc(), 0);
7829                assert_eq!(metric.loc.blank(), 0);
7830                insta::assert_json_snapshot!(metric.loc);
7831            },
7832        );
7833    }
7834
7835    #[test]
7836    fn php_no_closure_in_assignment_lloc() {
7837        // Anonymous function as RHS does not add an LLOC; only the
7838        // expression_statement counts. The closure body's statements are
7839        // counted in its own FuncSpace.
7840        check_metrics::<PhpParser>(
7841            "<?php
7842$f = function (): int {
7843    return 42;
7844};",
7845            "foo.php",
7846            |metric| {
7847                assert_eq!(metric.loc.sloc(), 4);
7848                assert_eq!(metric.loc.ploc(), 4);
7849                assert_eq!(metric.loc.lloc(), 2);
7850                assert_eq!(metric.loc.cloc(), 0);
7851                assert_eq!(metric.loc.blank(), 0);
7852                insta::assert_json_snapshot!(metric.loc);
7853            },
7854        );
7855    }
7856
7857    #[test]
7858    fn php_for_lloc() {
7859        // The for_statement contributes 1 LLOC; init/cond/update are NOT
7860        // separate statements in PHP's grammar.
7861        check_metrics::<PhpParser>(
7862            "<?php
7863for ($i = 0; $i < 10; $i++) {
7864    echo $i;
7865}",
7866            "foo.php",
7867            |metric| {
7868                assert_eq!(metric.loc.sloc(), 4);
7869                assert_eq!(metric.loc.ploc(), 4);
7870                assert_eq!(metric.loc.lloc(), 2);
7871                assert_eq!(metric.loc.cloc(), 0);
7872                assert_eq!(metric.loc.blank(), 0);
7873                insta::assert_json_snapshot!(metric.loc);
7874            },
7875        );
7876    }
7877
7878    #[test]
7879    fn php_foreach_lloc() {
7880        check_metrics::<PhpParser>(
7881            "<?php
7882foreach ($items as $k => $v) {
7883    echo $v;
7884}",
7885            "foo.php",
7886            |metric| {
7887                assert_eq!(metric.loc.sloc(), 4);
7888                assert_eq!(metric.loc.ploc(), 4);
7889                assert_eq!(metric.loc.lloc(), 2);
7890                assert_eq!(metric.loc.cloc(), 0);
7891                assert_eq!(metric.loc.blank(), 0);
7892                insta::assert_json_snapshot!(metric.loc);
7893            },
7894        );
7895    }
7896
7897    #[test]
7898    fn php_try_lloc() {
7899        check_metrics::<PhpParser>(
7900            "<?php
7901try {
7902    $a = 1;
7903} catch (\\Exception $e) {
7904    $a = 0;
7905} finally {
7906    $b = 2;
7907}",
7908            "foo.php",
7909            |metric| {
7910                assert_eq!(metric.loc.sloc(), 8);
7911                assert_eq!(metric.loc.ploc(), 8);
7912                assert_eq!(metric.loc.lloc(), 4);
7913                assert_eq!(metric.loc.cloc(), 0);
7914                assert_eq!(metric.loc.blank(), 0);
7915                insta::assert_json_snapshot!(metric.loc);
7916            },
7917        );
7918    }
7919
7920    #[test]
7921    fn php_class_loc() {
7922        check_metrics::<PhpParser>(
7923            "<?php
7924class A {
7925    public int $x = 0;
7926    private const Y = 1;
7927    public function f(): int {
7928        return $this->x;
7929    }
7930}",
7931            "foo.php",
7932            |metric| {
7933                assert_eq!(metric.loc.sloc(), 8);
7934                assert_eq!(metric.loc.ploc(), 8);
7935                assert_eq!(metric.loc.lloc(), 3);
7936                assert_eq!(metric.loc.cloc(), 0);
7937                assert_eq!(metric.loc.blank(), 0);
7938                insta::assert_json_snapshot!(metric.loc);
7939            },
7940        );
7941    }
7942
7943    #[test]
7944    fn php_namespace_use_lloc() {
7945        check_metrics::<PhpParser>(
7946            "<?php
7947namespace App;
7948use App\\Foo;
7949use App\\Bar;
7950$a = 1;",
7951            "foo.php",
7952            |metric| {
7953                assert_eq!(metric.loc.sloc(), 5);
7954                assert_eq!(metric.loc.ploc(), 5);
7955                assert_eq!(metric.loc.lloc(), 3);
7956                assert_eq!(metric.loc.cloc(), 0);
7957                assert_eq!(metric.loc.blank(), 0);
7958                insta::assert_json_snapshot!(metric.loc);
7959            },
7960        );
7961    }
7962
7963    #[test]
7964    fn php_general_loc() {
7965        check_metrics::<PhpParser>(
7966            "<?php
7967// header
7968namespace App;
7969use App\\Foo;
7970
7971class Bar {
7972    public int $n = 0;
7973
7974    public function add(int $x): int {
7975        if ($x > 0) {
7976            return $this->n + $x;
7977        }
7978        return $this->n;
7979    }
7980}",
7981            "foo.php",
7982            |metric| {
7983                assert_eq!(metric.loc.sloc(), 15);
7984                assert_eq!(metric.loc.ploc(), 12);
7985                assert_eq!(metric.loc.lloc(), 5);
7986                assert_eq!(metric.loc.cloc(), 1);
7987                assert_eq!(metric.loc.blank(), 2);
7988                insta::assert_json_snapshot!(metric.loc);
7989            },
7990        );
7991    }
7992
7993    #[test]
7994    fn php_match_in_expression_lloc() {
7995        // Match inside another expression (e.g. assignment RHS) — the
7996        // outer expression_statement counts, the inner match arms do not.
7997        check_metrics::<PhpParser>(
7998            "<?php
7999$y = 10 + match ($x) { 1 => 2, default => 0 };",
8000            "foo.php",
8001            |metric| {
8002                assert_eq!(metric.loc.sloc(), 2);
8003                assert_eq!(metric.loc.ploc(), 2);
8004                assert_eq!(metric.loc.lloc(), 1);
8005                assert_eq!(metric.loc.cloc(), 0);
8006                assert_eq!(metric.loc.blank(), 0);
8007                insta::assert_json_snapshot!(metric.loc);
8008            },
8009        );
8010    }
8011
8012    #[test]
8013    fn php_html_island_ploc() {
8014        // Embedded HTML between PHP tags ("text interpolation"). HTML
8015        // rows must contribute to PLOC (they are not blank and not a
8016        // PHP comment); this test locks that behavior so a future
8017        // grammar bump or impl tweak that excludes `text` nodes from
8018        // the default PLOC branch is caught.
8019        check_metrics::<PhpParser>(
8020            "<?php if ($cond): ?>
8021<div>hello</div>
8022<p>world</p>
8023<?php endif; ?>",
8024            "foo.php",
8025            |metric| {
8026                assert_eq!(metric.loc.sloc(), 4);
8027                assert_eq!(metric.loc.ploc(), 3);
8028                assert_eq!(metric.loc.lloc(), 1);
8029                assert_eq!(metric.loc.cloc(), 0);
8030                assert_eq!(metric.loc.blank(), 1);
8031                insta::assert_json_snapshot!(metric.loc);
8032            },
8033        );
8034    }
8035
8036    #[test]
8037    fn php_short_echo_tag_ploc() {
8038        // `<?=` is the same `php_tag` kind as `<?php` per
8039        // tree-sitter-php 0.24.2. A regression that re-classified `<?=`
8040        // would shift PLOC; this test pins the current behavior.
8041        check_metrics::<PhpParser>("<p><?= $name ?></p>", "foo.php", |metric| {
8042            assert_eq!(metric.loc.sloc(), 1);
8043            assert_eq!(metric.loc.ploc(), 1);
8044            assert_eq!(metric.loc.lloc(), 1);
8045            assert_eq!(metric.loc.cloc(), 0);
8046            assert_eq!(metric.loc.blank(), 0);
8047            insta::assert_json_snapshot!(metric.loc);
8048        });
8049    }
8050
8051    #[test]
8052    fn elixir_blank() {
8053        // Two blank lines separate three top-level expressions.
8054        check_metrics::<ElixirParser>(
8055            "defmodule Foo do\n\n  def a, do: :a\n\n  def b, do: :b\nend\n",
8056            "foo.ex",
8057            |metric| {
8058                assert_eq!(metric.loc.sloc(), 6);
8059                assert_eq!(metric.loc.ploc(), 4);
8060                assert_eq!(metric.loc.lloc(), 3);
8061                assert_eq!(metric.loc.cloc(), 0);
8062                assert_eq!(metric.loc.blank(), 2);
8063                insta::assert_json_snapshot!(
8064                    metric.loc,
8065                    @r#"
8066                {
8067                  "sloc": 6,
8068                  "ploc": 4,
8069                  "lloc": 3,
8070                  "cloc": 0,
8071                  "blank": 2,
8072                  "sloc_average": 1.5,
8073                  "ploc_average": 1.0,
8074                  "lloc_average": 0.75,
8075                  "cloc_average": 0.0,
8076                  "blank_average": 0.5,
8077                  "sloc_min": 1,
8078                  "sloc_max": 6,
8079                  "cloc_min": 0,
8080                  "cloc_max": 0,
8081                  "ploc_min": 1,
8082                  "ploc_max": 4,
8083                  "lloc_min": 1,
8084                  "lloc_max": 3,
8085                  "blank_min": 0,
8086                  "blank_max": 2
8087                }
8088                "#
8089                );
8090            },
8091        );
8092    }
8093
8094    #[test]
8095    fn elixir_no_zero_blank() {
8096        // Blank line interleaved with code that carries trailing comments —
8097        // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
8098        check_metrics::<ElixirParser>(
8099            "defmodule Foo do\n  def f, do: :ok\n\n  def g, do: :ok # trailing\n  def h, do: :ok # trailing\nend\n",
8100            "foo.ex",
8101            |metric| {
8102                assert_eq!(metric.loc.sloc(), 6);
8103                assert_eq!(metric.loc.ploc(), 5);
8104                assert_eq!(metric.loc.cloc(), 2);
8105                assert_eq!(metric.loc.blank(), 1);
8106            },
8107        );
8108    }
8109
8110    #[test]
8111    fn elixir_blank_zero_sanity() {
8112        // Sanity check: blank must report 0, never go negative, when the
8113        // input has no blank lines.
8114        check_metrics::<ElixirParser>(
8115            "defmodule Foo do\n  def f, do: :ok\nend\n",
8116            "foo.ex",
8117            |metric| {
8118                assert_eq!(metric.loc.blank(), 0);
8119            },
8120        );
8121    }
8122
8123    #[test]
8124    fn elixir_cloc() {
8125        // Mix of standalone comments and a comment on the same line as
8126        // code. Elixir has no block comment syntax — only `#` lines.
8127        check_metrics::<ElixirParser>(
8128            "# top\ndefmodule Foo do\n  # body\n  def f, do: :ok # trailing\nend\n",
8129            "foo.ex",
8130            |metric| {
8131                assert_eq!(metric.loc.cloc(), 3);
8132            },
8133        );
8134    }
8135
8136    #[test]
8137    fn elixir_lloc() {
8138        // Two statements at the top level of the module body — the
8139        // `defmodule` call itself counts as one statement (since its
8140        // parent is `Source`), and each `def` inside its `do_block`
8141        // counts too: 1 + 2 = 3.
8142        check_metrics::<ElixirParser>(
8143            "defmodule Foo do\n  def a, do: 1\n  def b, do: 2\nend\n",
8144            "foo.ex",
8145            |metric| {
8146                assert_eq!(metric.loc.lloc(), 3);
8147            },
8148        );
8149    }
8150
8151    #[test]
8152    fn elixir_no_nested_call_lloc() {
8153        // Calls nested inside another call's arguments are NOT direct
8154        // children of a statement container, so they do not bump LLOC.
8155        // Three syntactic calls (`defmodule`, `def`, `IO.puts`) → 3.
8156        check_metrics::<ElixirParser>(
8157            "defmodule Foo do\n  def f do\n    IO.puts(Enum.join([1, 2, 3], \", \"))\n  end\nend\n",
8158            "foo.ex",
8159            |metric| {
8160                assert_eq!(metric.loc.lloc(), 3);
8161            },
8162        );
8163    }
8164
8165    #[test]
8166    fn elixir_no_binary_operator_inside_call_lloc() {
8167        // Binary operators inside call arguments are sub-expressions,
8168        // not statements. A single `def` body containing `IO.puts(a + b)`
8169        // produces 3 LLOC (defmodule, def, IO.puts) — the `a + b`
8170        // binary_operator is not a direct child of any statement
8171        // container.
8172        check_metrics::<ElixirParser>(
8173            "defmodule Foo do\n  def f(a, b) do\n    IO.puts(a + b)\n  end\nend\n",
8174            "foo.ex",
8175            |metric| {
8176                assert_eq!(metric.loc.lloc(), 3);
8177            },
8178        );
8179    }
8180
8181    #[test]
8182    fn elixir_stab_clause_counts_lloc() {
8183        // Each `stab_clause` arm in a `case do ... end` is a direct
8184        // child of the inner `do_block`, so each one is its own LLOC.
8185        // defmodule + def + case + 3 arms = 6 logical lines.
8186        check_metrics::<ElixirParser>(
8187            "defmodule Foo do\n  def f(x) do\n    case x do\n      1 -> :a\n      2 -> :b\n      _ -> :c\n    end\n  end\nend\n",
8188            "foo.ex",
8189            |metric| {
8190                assert_eq!(metric.loc.lloc(), 6);
8191            },
8192        );
8193    }
8194
8195    #[test]
8196    fn elixir_no_comment_lloc() {
8197        // Comments are direct children of a statement container but
8198        // are routed through the dedicated `Comment` arm in `compute`,
8199        // so they MUST NOT bump LLOC. Only `defmodule` and `def`
8200        // contribute LLOC here.
8201        check_metrics::<ElixirParser>(
8202            "# leading\ndefmodule Foo do\n  # inside\n  def f, do: :ok\n  # trailing\nend\n",
8203            "foo.ex",
8204            |metric| {
8205                assert_eq!(metric.loc.lloc(), 2);
8206            },
8207        );
8208    }
8209
8210    #[test]
8211    fn elixir_no_do_token_lloc() {
8212        // The `do` and `end` keyword tokens are unnamed leaves inside a
8213        // `do_block`; they must not be counted as statements. A body
8214        // with one expression produces exactly 2 LLOC (defmodule and
8215        // the inner expression).
8216        check_metrics::<ElixirParser>("defmodule Foo do\n  :ok\nend\n", "foo.ex", |metric| {
8217            // `:ok` is an `Atom` whose parent is the module-call's
8218            // `do_block`; that counts. Plus the `defmodule` call.
8219            assert_eq!(metric.loc.lloc(), 2);
8220        });
8221    }
8222
8223    #[test]
8224    fn elixir_no_keyword_pair_lloc() {
8225        // `key: value` keyword pairs inside an argument list (`def f,
8226        // do: :ok`) are children of an `arguments` / `keywords` node,
8227        // not a statement container, so they don't bump LLOC.
8228        check_metrics::<ElixirParser>(
8229            "defmodule Foo do\n  def add(a, b), do: a + b\nend\n",
8230            "foo.ex",
8231            |metric| {
8232                // defmodule (1) + def (1) = 2
8233                assert_eq!(metric.loc.lloc(), 2);
8234            },
8235        );
8236    }
8237
8238    #[test]
8239    fn elixir_no_string_content_lloc() {
8240        // `quoted_content` chunks inside a heredoc / regular string are
8241        // structural and don't represent statements. A `@moduledoc`
8242        // attribute call with a multi-line string contributes exactly
8243        // one LLOC (the `@moduledoc` call), not one per content line.
8244        check_metrics::<ElixirParser>(
8245            "defmodule Foo do\n  @moduledoc \"\"\"\n  line one\n  line two\n  \"\"\"\n  def f, do: :ok\nend\n",
8246            "foo.ex",
8247            |metric| {
8248                // defmodule + @moduledoc + def = 3
8249                assert_eq!(metric.loc.lloc(), 3);
8250            },
8251        );
8252    }
8253
8254    #[test]
8255    fn elixir_rescue_arm_counts_lloc() {
8256        // Each rescue arm's body has a single expression (e.g. `:bad`)
8257        // that counts as one LLOC; the `stab_clause` header itself is
8258        // skipped. The rescue_block named node is also a direct child
8259        // of try's do_block, so it contributes one LLOC too.
8260        // Total: defmodule + def + try + do_it() + rescue_block
8261        //        + 2 arm bodies = 7.
8262        check_metrics::<ElixirParser>(
8263            "defmodule Foo do\n  def safe do\n    try do\n      do_it()\n    rescue\n      ArgumentError -> :bad\n      RuntimeError -> :worse\n    end\n  end\nend\n",
8264            "foo.ex",
8265            |metric| {
8266                assert_eq!(metric.loc.lloc(), 7);
8267            },
8268        );
8269    }
8270
8271    #[test]
8272    fn elixir_no_arg_punctuation_lloc() {
8273        // Function-call arguments (`a, b` inside `def add(a, b)`) are
8274        // children of an `arguments` node, not of a statement container.
8275        // They MUST NOT inflate LLOC.
8276        check_metrics::<ElixirParser>(
8277            "defmodule Foo do\n  def add(a, b, c, d) do\n    a + b + c + d\n  end\nend\n",
8278            "foo.ex",
8279            |metric| {
8280                // defmodule + def + (a+b+c+d) = 3
8281                assert_eq!(metric.loc.lloc(), 3);
8282            },
8283        );
8284    }
8285
8286    #[test]
8287    fn elixir_no_list_element_lloc() {
8288        // List literal elements live under a `list` node, not a
8289        // statement container — they must not bump LLOC.
8290        check_metrics::<ElixirParser>(
8291            "defmodule Foo do\n  def f do\n    [:a, :b, :c, :d]\n  end\nend\n",
8292            "foo.ex",
8293            |metric| {
8294                // defmodule + def + the list expression = 3
8295                assert_eq!(metric.loc.lloc(), 3);
8296            },
8297        );
8298    }
8299
8300    #[test]
8301    fn elixir_no_map_field_lloc() {
8302        // Map `pair`s live under `map`, not a statement container.
8303        check_metrics::<ElixirParser>(
8304            "defmodule Foo do\n  def f do\n    %{a: 1, b: 2, c: 3}\n  end\nend\n",
8305            "foo.ex",
8306            |metric| {
8307                assert_eq!(metric.loc.lloc(), 3);
8308            },
8309        );
8310    }
8311
8312    #[test]
8313    fn elixir_anonymous_fn_body_lloc() {
8314        // `lloc()` on the Unit space returns the aggregate (own +
8315        // nested-space) count. Even though the anonymous_function is
8316        // its own function space, the merge step pulls its `lloc` back
8317        // into the parent. Counts:
8318        //   Unit own: defmodule, def, `add = fn ...`, final `add` = 4
8319        //   anon-fn:  `x + 1` body expression                       = 1
8320        //   aggregated total                                        = 5
8321        check_metrics::<ElixirParser>(
8322            "defmodule Foo do\n  def f do\n    add = fn x -> x + 1 end\n    add\n  end\nend\n",
8323            "foo.ex",
8324            |metric| {
8325                assert_eq!(metric.loc.lloc(), 5);
8326            },
8327        );
8328    }
8329
8330    #[test]
8331    fn ruby_blank() {
8332        // The parser's root span starts at the first non-blank line, so
8333        // a blank line must sit BETWEEN code lines to be counted.
8334        // expected: line 3 is blank → blank = 1.
8335        check_metrics::<RubyParser>("def foo\n  a = 1\n\n  a + 1\nend\n", "foo.rb", |metric| {
8336            assert_eq!(metric.loc.blank(), 1);
8337        });
8338    }
8339
8340    #[test]
8341    fn ruby_no_zero_blank() {
8342        // Mirrors `rust_no_zero_blank`: the blank counter must stay
8343        // non-zero when blank lines sit between code lines that carry
8344        // trailing comments. Catches regressions in the SLOC −
8345        // (PLOC ∪ CLOC) union math when PLOC and CLOC line-sets
8346        // overlap.
8347        check_metrics::<RubyParser>(
8348            "def foo  # entry\n  pool = 0\n\n  server = -42  # negative\n\n  ok = false\nend\n",
8349            "foo.rb",
8350            |metric| {
8351                assert_eq!(metric.loc.blank(), 2);
8352            },
8353        );
8354    }
8355
8356    #[test]
8357    fn ruby_cloc() {
8358        // 3 comment lines.
8359        check_metrics::<RubyParser>(
8360            "# one\n# two\n# three\ndef foo\nend\n",
8361            "foo.rb",
8362            |metric| {
8363                assert_eq!(metric.loc.cloc(), 3);
8364            },
8365        );
8366    }
8367
8368    #[test]
8369    fn ruby_lloc() {
8370        // expected: 3 logical lines = `def` (Method) + `if` (If) +
8371        // `while` (While). Bare expression-statements (assignments,
8372        // calls) are intentionally NOT counted.
8373        check_metrics::<RubyParser>(
8374            "def foo(a)\n  if a\n    a += 1\n  end\n  while a > 0\n    a -= 1\n  end\nend\n",
8375            "foo.rb",
8376            |metric| {
8377                assert_eq!(metric.loc.lloc(), 3);
8378            },
8379        );
8380    }
8381
8382    #[test]
8383    fn ruby_no_call_lloc() {
8384        // expected: 1 logical line (the surrounding `def`). The bare
8385        // method calls `puts 'hello'` and `puts 'world'` are
8386        // intentionally NOT counted — there is no expression_statement
8387        // wrapper to disambiguate them from sub-expressions.
8388        check_metrics::<RubyParser>(
8389            "def foo\n  puts 'hello'\n  puts 'world'\nend\n",
8390            "foo.rb",
8391            |metric| {
8392                assert_eq!(metric.loc.lloc(), 1);
8393            },
8394        );
8395    }
8396
8397    #[test]
8398    fn ruby_no_assignment_lloc() {
8399        // Same rationale as `ruby_no_call_lloc`. expected: 1 lloc
8400        // (the `def`); raw assignments aren't counted.
8401        check_metrics::<RubyParser>(
8402            "def foo\n  a = 1\n  b = 2\n  c = a + b\nend\n",
8403            "foo.rb",
8404            |metric| {
8405                assert_eq!(metric.loc.lloc(), 1);
8406            },
8407        );
8408    }
8409
8410    #[test]
8411    fn ruby_modifier_lloc() {
8412        // Postfix modifier forms each count as one logical line. A
8413        // `return … if …` parses as an `IfModifier` wrapping a `Return`;
8414        // both fire the LLOC arm so the modifier line contributes +2.
8415        // expected: def(1) + if_modifier(1) + inner return(1)
8416        // + while_modifier(1) + rescue_modifier(1) = 5.
8417        check_metrics::<RubyParser>(
8418            "def foo(a)\n  return a if a.nil?\n  a -= 1 while a > 0\n  parse(a) rescue nil\nend\n",
8419            "foo.rb",
8420            |metric| {
8421                assert_eq!(metric.loc.lloc(), 5);
8422            },
8423        );
8424    }
8425
8426    #[test]
8427    fn ruby_class_lloc() {
8428        // expected: 1 class + 1 module + 2 methods = 4.
8429        check_metrics::<RubyParser>(
8430            "module M\n  class C\n    def foo\n    end\n    def bar\n    end\n  end\nend\n",
8431            "foo.rb",
8432            |metric| {
8433                assert_eq!(metric.loc.lloc(), 4);
8434            },
8435        );
8436    }
8437
8438    #[test]
8439    fn ruby_begin_rescue_lloc() {
8440        // expected: 1 def + 1 begin = 2. Rescue clauses are part of
8441        // the begin construct and not separately counted; the bare
8442        // expression body lines are not statements.
8443        check_metrics::<RubyParser>(
8444            "def foo\n  begin\n    risky\n  rescue StandardError\n    nil\n  end\nend\n",
8445            "foo.rb",
8446            |metric| {
8447                assert_eq!(metric.loc.lloc(), 2);
8448            },
8449        );
8450    }
8451
8452    #[test]
8453    fn ruby_nested_defs_lloc() {
8454        // Each `Method` declaration contributes one logical line.
8455        // expected: outer `def` + inner `def` = 2.
8456        check_metrics::<RubyParser>(
8457            "def outer\n  def inner\n    1\n  end\nend\n",
8458            "foo.rb",
8459            |metric| {
8460                assert_eq!(metric.loc.lloc(), 2);
8461            },
8462        );
8463    }
8464
8465    #[test]
8466    fn ruby_no_block_body_lloc() {
8467        // A top-level `[1,2,3].each do |x| puts x end` produces zero
8468        // logical lines: the surrounding `.each` is a `Call` (not in
8469        // the LLOC arm), the `DoBlock` is a closure (also not a
8470        // statement), and the `puts x` inside is another call. This
8471        // pins the documented expression-statement exclusion.
8472        check_metrics::<RubyParser>(
8473            "[1, 2, 3].each do |x|\n  puts x\nend\n",
8474            "foo.rb",
8475            |metric| {
8476                assert_eq!(metric.loc.lloc(), 0);
8477            },
8478        );
8479    }
8480
8481    #[test]
8482    fn ruby_no_lambda_body_lloc() {
8483        // `add = ->(a, b) { a + b }` produces zero logical lines for
8484        // the same reason as `ruby_no_block_body_lloc`: assignments,
8485        // calls, and lambda bodies are intentionally not statements
8486        // in this impl.
8487        check_metrics::<RubyParser>("add = ->(a, b) {\n  a + b\n}\n", "foo.rb", |metric| {
8488            assert_eq!(metric.loc.lloc(), 0);
8489        });
8490    }
8491
8492    #[test]
8493    fn ruby_heredoc_lloc_and_blank() {
8494        // A `<<~TXT` heredoc contributes: SLOC = every line in the file
8495        // (including the heredoc body); LLOC = just the surrounding `def`.
8496        // #778: the heredoc-body rows (`one`, `two`) hold real string text,
8497        // so they are credited to PLOC like Python's multi-line strings
8498        // (#415) rather than mislabelled as blank. Every row is now code.
8499        // expected: sloc = 7, ploc = 7, lloc = 1, blank = 0.
8500        check_metrics::<RubyParser>(
8501            "def foo\n  msg = <<~TXT\n    one\n    two\n  TXT\n  msg\nend\n",
8502            "foo.rb",
8503            |metric| {
8504                assert_eq!(metric.loc.sloc(), 7);
8505                assert_eq!(metric.loc.ploc(), 7);
8506                assert_eq!(metric.loc.lloc(), 1);
8507                assert_eq!(metric.loc.blank(), 0);
8508            },
8509        );
8510    }
8511
8512    #[test]
8513    fn ruby_semicolon_multistatement_lloc_undercount() {
8514        // Documented limitation: Ruby has no `expression_statement`
8515        // wrapper, so `;`-separated multi-statement lines collapse to
8516        // a single LLOC bump (the surrounding `def`). A future
8517        // statement-counter that walks BlockBody children would
8518        // change this — pin the current behaviour so the regression
8519        // is visible.
8520        check_metrics::<RubyParser>(
8521            "def foo\n  a = 1; b = 2; a + b\nend\n",
8522            "foo.rb",
8523            |metric| {
8524                assert_eq!(metric.loc.lloc(), 1);
8525            },
8526        );
8527    }
8528
8529    #[test]
8530    fn ruby_ploc_skips_comments_and_blanks() {
8531        // PLOC counts physical instruction lines: code-bearing lines
8532        // only. Comments and blanks are excluded.
8533        check_metrics::<RubyParser>("# header\n\ndef foo\n  a = 1\nend\n", "foo.rb", |metric| {
8534            assert_eq!(metric.loc.ploc(), 3);
8535            assert_eq!(metric.loc.cloc(), 1);
8536            assert_eq!(metric.loc.blank(), 1);
8537        });
8538    }
8539
8540    // -----------------------------------------------------------------
8541    // Issue #195: nested-function/closure LLOC tests across languages.
8542    // Mirrors the prior art for Rust (`rust_function_in_loop_lloc`,
8543    // `rust_closure_expression_lloc`), Mozjs (`mozjs_nested_function_loc`),
8544    // Bash (`bash_nested_function_loc`), and TypeScript
8545    // (`typescript_nested_functions_loc`, `tsx_nested_functions_loc`).
8546    // -----------------------------------------------------------------
8547
8548    #[test]
8549    fn python_nested_def_lloc() {
8550        // Nested `def`: the inner function declaration plus the outer
8551        // body's `return inner()` are both LLOC; the outer `def` header
8552        // and the inner `return 1` belong to their own function spaces.
8553        check_metrics::<PythonParser>(
8554            "def outer():\n    def inner():\n        return 1\n    return inner()\n",
8555            "foo.py",
8556            |metric| {
8557                assert_eq!(metric.loc.sloc(), 4);
8558                assert_eq!(metric.loc.ploc(), 4);
8559                assert_eq!(metric.loc.lloc(), 2);
8560                assert_eq!(metric.loc.cloc(), 0);
8561                assert_eq!(metric.loc.blank(), 0);
8562                insta::assert_json_snapshot!(metric.loc);
8563            },
8564        );
8565    }
8566
8567    #[test]
8568    fn python_lambda_in_def_lloc() {
8569        // `lambda x: x + 1` is an expression, not a Python `function_definition`,
8570        // so it does not start a new function space. The two LLOC come from
8571        // the assignment `f = lambda ...` and the `return f(2)` statement.
8572        check_metrics::<PythonParser>(
8573            "def outer():\n    f = lambda x: x + 1\n    return f(2)\n",
8574            "foo.py",
8575            |metric| {
8576                assert_eq!(metric.loc.sloc(), 3);
8577                assert_eq!(metric.loc.ploc(), 3);
8578                assert_eq!(metric.loc.lloc(), 2);
8579                assert_eq!(metric.loc.cloc(), 0);
8580                assert_eq!(metric.loc.blank(), 0);
8581                insta::assert_json_snapshot!(metric.loc);
8582            },
8583        );
8584    }
8585
8586    #[test]
8587    fn python_match_statement_lloc() {
8588        // `match` (PEP 634) is a control-flow statement that must add one
8589        // LLOC like `if`/`try`, plus each `return` in a case body (#462).
8590        // Its `case_clause` children add nothing, mirroring how
8591        // `elif_clause`/`else_clause` are absent from the LLOC arm: the
8592        // construct counts once and the statements inside count via their
8593        // own arms. Here: match(1) + two `return`s = 3.
8594        check_metrics::<PythonParser>(
8595            "def f(x):\n    match x:\n        case 1: return 1\n        case _: return 0\n",
8596            "foo.py",
8597            |metric| {
8598                assert_eq!(metric.loc.lloc(), 3);
8599                insta::assert_json_snapshot!(metric.loc);
8600            },
8601        );
8602    }
8603
8604    #[test]
8605    fn python_match_lloc_matches_if_else() {
8606        // Parity with the equivalent two-branch `if`/`else`: both have the
8607        // construct keyword (1) plus two `return` bodies (2), so each must
8608        // report an identical LLOC of 3. The match form previously
8609        // undercounted (2) because `match_statement` was absent from the
8610        // LLOC arm (#462). `check_metrics` takes a non-capturing `fn`, so
8611        // the shared expectation is pinned to the same literal in both
8612        // closures rather than threaded through a captured variable.
8613        check_metrics::<PythonParser>(
8614            "def f(x):\n    if x == 1: return 1\n    else: return 0\n",
8615            "foo.py",
8616            |metric| assert_eq!(metric.loc.lloc(), 3),
8617        );
8618        check_metrics::<PythonParser>(
8619            "def f(x):\n    match x:\n        case 1: return 1\n        case _: return 0\n",
8620            "foo.py",
8621            |metric| assert_eq!(metric.loc.lloc(), 3),
8622        );
8623    }
8624
8625    #[test]
8626    fn python_type_alias_lloc() {
8627        // A `type` alias (PEP 695) is a leaf statement, counted like an
8628        // assignment. `type Alias = int` followed by `x = 1` is two LLOC;
8629        // before #462 the alias fell through to the `_` arm and the file
8630        // reported only 1.
8631        check_metrics::<PythonParser>("type Alias = int\nx = 1\n", "foo.py", |metric| {
8632            assert_eq!(metric.loc.lloc(), 2);
8633            insta::assert_json_snapshot!(metric.loc);
8634        });
8635    }
8636
8637    #[test]
8638    fn java_local_class_in_method_lloc() {
8639        // A `class` declared inside a method body produces its own function
8640        // space, so the outer method's LLOC only sees `return new Local().v();`
8641        // and the body of `v()` contributes the second LLOC.
8642        check_metrics::<JavaParser>(
8643            "class Foo {\n    int bar() {\n        class Local {\n            int v() { return 1; }\n        }\n        return new Local().v();\n    }\n}\n",
8644            "foo.java",
8645            |metric| {
8646                assert_eq!(metric.loc.sloc(), 8);
8647                assert_eq!(metric.loc.ploc(), 8);
8648                assert_eq!(metric.loc.lloc(), 2);
8649                assert_eq!(metric.loc.cloc(), 0);
8650                assert_eq!(metric.loc.blank(), 0);
8651                insta::assert_json_snapshot!(metric.loc);
8652            },
8653        );
8654    }
8655
8656    #[test]
8657    fn java_lambda_in_method_lloc() {
8658        // Java lambdas are expressions; the two LLOC come from the
8659        // `IntUnaryOperator f = x -> x + 1;` declaration and the
8660        // `f.applyAsInt(3);` expression statement.
8661        check_metrics::<JavaParser>(
8662            "class Foo {\n    void bar() {\n        java.util.function.IntUnaryOperator f = x -> x + 1;\n        f.applyAsInt(3);\n    }\n}\n",
8663            "foo.java",
8664            |metric| {
8665                assert_eq!(metric.loc.sloc(), 6);
8666                assert_eq!(metric.loc.ploc(), 6);
8667                assert_eq!(metric.loc.lloc(), 2);
8668                assert_eq!(metric.loc.cloc(), 0);
8669                assert_eq!(metric.loc.blank(), 0);
8670                insta::assert_json_snapshot!(metric.loc);
8671            },
8672        );
8673    }
8674
8675    #[test]
8676    fn groovy_blank() {
8677        // Blank lines + simple statements. Newlines act as the
8678        // statement terminator; PLOC counts the two declaration lines.
8679        check_metrics::<GroovyParser>("int x = 1\n\n\nint y = 2", "foo.groovy", |metric| {
8680            assert_eq!(metric.loc.sloc(), 4);
8681            assert_eq!(metric.loc.ploc(), 2);
8682            assert_eq!(metric.loc.lloc(), 2);
8683            assert_eq!(metric.loc.blank(), 2);
8684        });
8685    }
8686
8687    #[test]
8688    fn groovy_no_zero_blank() {
8689        // A single line with no blanks: blank() == 0.
8690        check_metrics::<GroovyParser>("int x = 1", "foo.groovy", |metric| {
8691            assert_eq!(metric.loc.sloc(), 1);
8692            assert_eq!(metric.loc.blank(), 0);
8693        });
8694    }
8695
8696    #[test]
8697    fn groovy_cloc_line_comments() {
8698        check_metrics::<GroovyParser>(
8699            "// first comment
8700            int x = 1
8701            // second comment
8702            int y = 2",
8703            "foo.groovy",
8704            |metric| {
8705                assert_eq!(metric.loc.cloc(), 2);
8706                assert_eq!(metric.loc.ploc(), 2);
8707            },
8708        );
8709    }
8710
8711    #[test]
8712    fn groovy_cloc_block_comment() {
8713        check_metrics::<GroovyParser>(
8714            "/* multi
8715               line
8716               comment */
8717            int x = 1",
8718            "foo.groovy",
8719            |metric| {
8720                // Block comment spans 3 lines → cloc == 3.
8721                assert_eq!(metric.loc.cloc(), 3);
8722            },
8723        );
8724    }
8725
8726    #[test]
8727    fn groovy_cloc_groovydoc_comment() {
8728        // Groovy `/** … */` `groovydoc_comment` counts as CLOC. The
8729        // `Loc` arm already handled it; this pins it alongside the
8730        // restored `is_comment` parity (#697).
8731        check_metrics::<GroovyParser>(
8732            "/** groovydoc */
8733class A {
8734  int x = 1
8735}",
8736            "foo.groovy",
8737            |metric| {
8738                assert_eq!(metric.loc.cloc(), 1);
8739                assert_eq!(metric.loc.ploc(), 3);
8740            },
8741        );
8742    }
8743
8744    #[test]
8745    fn groovy_simple_lloc() {
8746        // One LLOC per simple expression statement.
8747        check_metrics::<GroovyParser>(
8748            "int a = 1
8749            int b = 2
8750            int c = 3",
8751            "foo.groovy",
8752            |metric| {
8753                assert_eq!(metric.loc.lloc(), 3);
8754            },
8755        );
8756    }
8757
8758    #[test]
8759    fn groovy_no_local_variable_declaration_in_for_lloc() {
8760        // The variable declaration inside a classic `for` init slot
8761        // does NOT count as an LLOC (it's an expression part of the
8762        // for-loop). Same gating as Java's `java_for_lloc`.
8763        check_metrics::<GroovyParser>(
8764            "for (int i = 0; i < 10; i++) {
8765                println(i)
8766            }",
8767            "foo.groovy",
8768            |metric| {
8769                // for-statement (1) + expression-statement `println(i)` (1) = 2
8770                assert_eq!(metric.loc.lloc(), 2);
8771            },
8772        );
8773    }
8774
8775    #[test]
8776    fn groovy_lambda_in_method_lloc() {
8777        // Closures contain a statement list — the dekobon grammar wraps
8778        // a single-expression body in `expression_statement` rather than
8779        // emitting the expression directly (as Java's `lambda_expression`
8780        // does), so a one-line closure body counts as its own LLOC.
8781        // Declaration `def f = …` (1) + closure body `x + 1` (1) +
8782        // call `f(3)` (1) = 3.
8783        check_metrics::<GroovyParser>(
8784            "class Foo {
8785                void bar() {
8786                    def f = { x -> x + 1 }
8787                    f(3)
8788                }
8789            }",
8790            "foo.groovy",
8791            |metric| {
8792                assert_eq!(metric.loc.lloc(), 3);
8793            },
8794        );
8795    }
8796
8797    #[test]
8798    fn groovy_try_lloc() {
8799        // try-statement counts as one LLOC; the catch body's
8800        // statements count separately.
8801        check_metrics::<GroovyParser>(
8802            "void f() {
8803                try {
8804                    risky()
8805                } catch (Exception e) {
8806                    handle(e)
8807                }
8808            }",
8809            "foo.groovy",
8810            |metric| {
8811                // try(1) + risky() expr-stmt(1) + handle() expr-stmt(1) = 3
8812                assert_eq!(metric.loc.lloc(), 3);
8813            },
8814        );
8815    }
8816
8817    #[test]
8818    fn groovy_class_loc() {
8819        // Source-file-level totals across multiple methods.
8820        check_metrics::<GroovyParser>(
8821            "class A {
8822                void f() {
8823                    int x = 1
8824                }
8825                void g() {
8826                    int y = 2
8827                }
8828            }",
8829            "foo.groovy",
8830            |metric| {
8831                // 8 lines of non-comment content: `class A {`, two
8832                // `void` headers, two `int … = …` body statements,
8833                // three closing braces.
8834                assert_eq!(metric.loc.ploc(), 8);
8835                assert_eq!(metric.loc.cloc(), 0);
8836                // Two expression-statement LLOCs (`int x = 1`,
8837                // `int y = 2`).
8838                assert_eq!(metric.loc.lloc(), 2);
8839            },
8840        );
8841    }
8842
8843    #[test]
8844    fn groovy_partial_parse_recovers_unit() {
8845        // Malformed input parses with ERROR but still emits a Unit
8846        // root via `spaces.rs` fallback (lesson 9). The single
8847        // source line is counted as SLOC even when the parse fails
8848        // mid-expression.
8849        check_metrics::<GroovyParser>("def x = (((", "foo.groovy", |metric| {
8850            assert_eq!(metric.loc.sloc(), 1);
8851            assert_eq!(metric.loc.blank(), 0);
8852        });
8853    }
8854
8855    #[test]
8856    fn groovy_sloc() {
8857        // Mirrors `java_sloc`: basic per-line count across a mix of
8858        // statements and a blank line.
8859        check_metrics::<GroovyParser>(
8860            "int a = 1
8861            int b = 2
8862
8863            int c = 3",
8864            "foo.groovy",
8865            |metric| {
8866                assert_eq!(metric.loc.sloc(), 4);
8867                assert_eq!(metric.loc.ploc(), 3);
8868                assert_eq!(metric.loc.blank(), 1);
8869            },
8870        );
8871    }
8872
8873    #[test]
8874    fn groovy_single_ploc() {
8875        // Mirrors `java_single_ploc`: one non-blank, non-comment
8876        // line of code => ploc == 1.
8877        check_metrics::<GroovyParser>("int x = 42", "foo.groovy", |metric| {
8878            assert_eq!(metric.loc.ploc(), 1);
8879            assert_eq!(metric.loc.cloc(), 0);
8880        });
8881    }
8882
8883    #[test]
8884    fn groovy_multi_ploc() {
8885        // Multiple statements on separate lines all contribute to
8886        // PLOC. Mirrors `java_multi_ploc`.
8887        check_metrics::<GroovyParser>(
8888            "int a = 1
8889            int b = 2
8890            int c = 3
8891            int d = 4",
8892            "foo.groovy",
8893            |metric| {
8894                assert_eq!(metric.loc.ploc(), 4);
8895                assert_eq!(metric.loc.lloc(), 4);
8896            },
8897        );
8898    }
8899
8900    #[test]
8901    fn groovy_single_statement_lloc() {
8902        // A single expression statement contributes one LLOC.
8903        // Mirrors `java_single_statement_lloc`.
8904        check_metrics::<GroovyParser>("println 'hi'", "foo.groovy", |metric| {
8905            assert_eq!(metric.loc.lloc(), 1);
8906        });
8907    }
8908
8909    #[test]
8910    fn groovy_for_lloc() {
8911        // The classical `for` statement itself counts as one LLOC;
8912        // the body's `println(i)` adds another. The init-slot
8913        // var-decl is suppressed by the LocalVariableDeclaration
8914        // ancestor-check (same rule as `java_for_lloc`).
8915        check_metrics::<GroovyParser>(
8916            "for (int i = 0; i < 100; i++) {
8917                println(i)
8918            }",
8919            "foo.groovy",
8920            |metric| {
8921                // ForStatement(1) + println-expr(1) = 2
8922                assert_eq!(metric.loc.lloc(), 2);
8923            },
8924        );
8925    }
8926
8927    #[test]
8928    fn groovy_foreach_lloc() {
8929        // `for (item in list)` parses as `enhanced_for_statement` —
8930        // counts as one LLOC.
8931        check_metrics::<GroovyParser>(
8932            "for (item in items) {
8933                println(item)
8934            }",
8935            "foo.groovy",
8936            |metric| {
8937                // EnhancedForStatement(1) + println(1) = 2
8938                assert_eq!(metric.loc.lloc(), 2);
8939            },
8940        );
8941    }
8942
8943    #[test]
8944    fn groovy_while_lloc() {
8945        // `while` itself is one LLOC; each body statement adds
8946        // another. Mirrors `java_while_lloc`.
8947        check_metrics::<GroovyParser>(
8948            "int i = 0
8949            while (i < 10) {
8950                i++
8951                println(i)
8952            }",
8953            "foo.groovy",
8954            |metric| {
8955                // int i = 0 (1) + while (1) + i++ (1) + println (1) = 4
8956                assert_eq!(metric.loc.lloc(), 4);
8957            },
8958        );
8959    }
8960
8961    #[test]
8962    fn groovy_do_while_lloc() {
8963        // `do…while` is one LLOC plus its body. Mirrors
8964        // `java_do_while_lloc`.
8965        check_metrics::<GroovyParser>(
8966            "int i = 0
8967            do {
8968                i++
8969            } while (i < 5)",
8970            "foo.groovy",
8971            |metric| {
8972                // int i = 0 (1) + do (1) + i++ (1) = 3
8973                assert_eq!(metric.loc.lloc(), 3);
8974            },
8975        );
8976    }
8977
8978    #[test]
8979    fn groovy_continue_lloc() {
8980        // `continue` is an LLOC. Same gating as `java_continue_lloc`.
8981        check_metrics::<GroovyParser>(
8982            "for (int i = 0; i < 10; i++) {
8983                if (i == 5) {
8984                    continue
8985                }
8986                println(i)
8987            }",
8988            "foo.groovy",
8989            |metric| {
8990                // for(1) + if(1) + continue(1) + println(1) = 4
8991                assert_eq!(metric.loc.lloc(), 4);
8992            },
8993        );
8994    }
8995
8996    #[test]
8997    fn groovy_expressions_lloc() {
8998        // A bag of expression statements: each independent
8999        // expr-stmt is one LLOC. Mirrors `java_expressions_lloc`.
9000        check_metrics::<GroovyParser>(
9001            "int a = 1
9002            a = 2
9003            a += 3
9004            println(a)
9005            doSomething()",
9006            "foo.groovy",
9007            |metric| {
9008                // 5 expression-statement lines.
9009                assert_eq!(metric.loc.lloc(), 5);
9010            },
9011        );
9012    }
9013
9014    #[test]
9015    fn groovy_throw_lloc() {
9016        // `throw` is one LLOC via the `ThrowStatement` arm.
9017        check_metrics::<GroovyParser>(
9018            "throw new RuntimeException('bad')",
9019            "foo.groovy",
9020            |metric| {
9021                assert_eq!(metric.loc.lloc(), 1);
9022            },
9023        );
9024    }
9025
9026    #[test]
9027    fn groovy_general_loc() {
9028        // Comprehensive mix: class + method + control flow.
9029        // Mirrors `java_general_loc`'s coverage shape.
9030        //
9031        // LLOC = 4, fully attributable:
9032        //   IfStatement (the outer if/else):     +1
9033        //   `println(x)`     (JuxtFunctionCall):  +1
9034        //   `println 'neg'` (JuxtFunctionCall):  +1
9035        //   `return`        (ReturnStatement):   +1
9036        // The else-branch's `expression_statement (closure)`
9037        // wrapper does NOT count — see the bare-Closure carve-out
9038        // in `impl Loc for GroovyCode::compute`.
9039        check_metrics::<GroovyParser>(
9040            "class A {
9041                void f(int x) {
9042                    if (x > 0) {
9043                        println(x)
9044                    } else {
9045                        println 'neg'
9046                    }
9047                    return
9048                }
9049            }",
9050            "foo.groovy",
9051            |metric| {
9052                assert_eq!(metric.loc.lloc(), 4);
9053                assert_eq!(metric.loc.cloc(), 0);
9054            },
9055        );
9056    }
9057
9058    #[test]
9059    fn csharp_local_function_in_method_lloc() {
9060        // C# local functions (`int Inner(int x) { ... }` inside `Bar()`)
9061        // open their own function space, so the outer method sees only
9062        // `return Inner(2);` plus the inner body's `return x + 1;`.
9063        check_metrics::<CsharpParser>(
9064            "class Foo {\n    int Bar() {\n        int Inner(int x) { return x + 1; }\n        return Inner(2);\n    }\n}\n",
9065            "foo.cs",
9066            |metric| {
9067                assert_eq!(metric.loc.sloc(), 6);
9068                assert_eq!(metric.loc.ploc(), 6);
9069                assert_eq!(metric.loc.lloc(), 2);
9070                assert_eq!(metric.loc.cloc(), 0);
9071                assert_eq!(metric.loc.blank(), 0);
9072                insta::assert_json_snapshot!(metric.loc);
9073            },
9074        );
9075    }
9076
9077    #[test]
9078    fn csharp_lambda_in_method_lloc() {
9079        // C# lambdas are expressions: the two LLOC come from the
9080        // `Func<int,int> f = x => x + 1;` declaration and the `f(3);` call.
9081        check_metrics::<CsharpParser>(
9082            "class Foo {\n    void Bar() {\n        System.Func<int, int> f = x => x + 1;\n        f(3);\n    }\n}\n",
9083            "foo.cs",
9084            |metric| {
9085                assert_eq!(metric.loc.sloc(), 6);
9086                assert_eq!(metric.loc.ploc(), 6);
9087                assert_eq!(metric.loc.lloc(), 2);
9088                assert_eq!(metric.loc.cloc(), 0);
9089                assert_eq!(metric.loc.blank(), 0);
9090                insta::assert_json_snapshot!(metric.loc);
9091            },
9092        );
9093    }
9094
9095    #[test]
9096    fn cpp_lambda_in_function_lloc() {
9097        // C++11 lambdas are expressions. The outer function `bar()` produces
9098        // two LLOC for the body: `auto f = [](int x) { return x + 1; };` and
9099        // `return f(2);`. The lambda's inner `return x + 1;` is part of the
9100        // lambda body inside the same function space (lambdas do not open a
9101        // new FuncSpace in this implementation), so it adds a third LLOC.
9102        // Closes the parity gap with #195 (which covered 11 other
9103        // languages but omitted C++).
9104        check_metrics::<CppParser>(
9105            "int bar() {\n    auto f = [](int x) { return x + 1; };\n    return f(2);\n}\n",
9106            "foo.cpp",
9107            |metric| {
9108                assert_eq!(metric.loc.sloc(), 4);
9109                assert_eq!(metric.loc.ploc(), 4);
9110                assert_eq!(metric.loc.lloc(), 3);
9111                assert_eq!(metric.loc.cloc(), 0);
9112                assert_eq!(metric.loc.blank(), 0);
9113                insta::assert_json_snapshot!(metric.loc);
9114            },
9115        );
9116    }
9117
9118    #[test]
9119    fn javascript_nested_function_lloc() {
9120        // Nested function_declaration: 4 LLOC = outer's `return inner();`,
9121        // inner's `return 1;`, plus the two function declarations
9122        // themselves (the JS Checker counts function declarations as LLOC).
9123        check_metrics::<JavascriptParser>(
9124            "function outer() {\n    function inner() {\n        return 1;\n    }\n    return inner();\n}\n",
9125            "foo.js",
9126            |metric| {
9127                assert_eq!(metric.loc.sloc(), 6);
9128                assert_eq!(metric.loc.ploc(), 6);
9129                assert_eq!(metric.loc.lloc(), 2);
9130                assert_eq!(metric.loc.cloc(), 0);
9131                assert_eq!(metric.loc.blank(), 0);
9132                insta::assert_json_snapshot!(metric.loc);
9133            },
9134        );
9135    }
9136
9137    #[test]
9138    fn javascript_arrow_function_lloc() {
9139        // The arrow function `(x) => x + 1` is an expression: the LLOC
9140        // come from `const inner = ...;` and `return inner(2);`.
9141        check_metrics::<JavascriptParser>(
9142            "function outer() {\n    const inner = (x) => x + 1;\n    return inner(2);\n}\n",
9143            "foo.js",
9144            |metric| {
9145                assert_eq!(metric.loc.sloc(), 4);
9146                assert_eq!(metric.loc.ploc(), 4);
9147                assert_eq!(metric.loc.lloc(), 2);
9148                assert_eq!(metric.loc.cloc(), 0);
9149                assert_eq!(metric.loc.blank(), 0);
9150                insta::assert_json_snapshot!(metric.loc);
9151            },
9152        );
9153    }
9154
9155    #[test]
9156    fn kotlin_lambda_literal_in_fun_lloc() {
9157        // A lambda literal (`{ x -> x + 1 }`) assigned to a `val` plus the
9158        // following call yields two LLOC at the outer function.
9159        check_metrics::<KotlinParser>(
9160            "fun outer() {\n    val f: (Int) -> Int = { x -> x + 1 }\n    f(3)\n}\n",
9161            "foo.kt",
9162            |metric| {
9163                assert_eq!(metric.loc.sloc(), 4);
9164                assert_eq!(metric.loc.ploc(), 4);
9165                assert_eq!(metric.loc.lloc(), 2);
9166                assert_eq!(metric.loc.cloc(), 0);
9167                assert_eq!(metric.loc.blank(), 0);
9168                insta::assert_json_snapshot!(metric.loc);
9169            },
9170        );
9171    }
9172
9173    #[test]
9174    fn kotlin_local_fun_in_fun_lloc() {
9175        // Kotlin's local `fun inner(...)` is also a function_declaration,
9176        // so it opens its own space; the outer LLOC reduces to `inner(3)`,
9177        // and the inner body contributes the second LLOC.
9178        check_metrics::<KotlinParser>(
9179            "fun outer() {\n    fun inner(x: Int): Int { return x + 1 }\n    inner(3)\n}\n",
9180            "foo.kt",
9181            |metric| {
9182                assert_eq!(metric.loc.sloc(), 4);
9183                assert_eq!(metric.loc.ploc(), 4);
9184                assert_eq!(metric.loc.lloc(), 2);
9185                assert_eq!(metric.loc.cloc(), 0);
9186                assert_eq!(metric.loc.blank(), 0);
9187                insta::assert_json_snapshot!(metric.loc);
9188            },
9189        );
9190    }
9191
9192    #[test]
9193    fn kotlin_object_expression_in_fun_lloc() {
9194        // An `object : Runnable { ... }` expression with an overridden
9195        // method whose body invokes `println("hi")`. LLOC: `val r = ...`,
9196        // the override's body call, and the outer `r.run()` call = 3.
9197        check_metrics::<KotlinParser>(
9198            "fun outer() {\n    val r = object : Runnable { override fun run() { println(\"hi\") } }\n    r.run()\n}\n",
9199            "foo.kt",
9200            |metric| {
9201                assert_eq!(metric.loc.sloc(), 4);
9202                assert_eq!(metric.loc.ploc(), 4);
9203                assert_eq!(metric.loc.lloc(), 3);
9204                assert_eq!(metric.loc.cloc(), 0);
9205                assert_eq!(metric.loc.blank(), 0);
9206                insta::assert_json_snapshot!(metric.loc);
9207            },
9208        );
9209    }
9210
9211    #[test]
9212    fn go_function_literal_initializer_lloc() {
9213        // `inner := func(x int) int { return x + 1 }` — the function
9214        // literal opens its own space; LLOC visible on the outer space:
9215        // the assignment + `return inner(2)` = 2, plus the literal's
9216        // `return x + 1` body = 3 aggregated.
9217        check_metrics::<GoParser>(
9218            "package main\nfunc outer() int {\n    inner := func(x int) int { return x + 1 }\n    return inner(2)\n}\n",
9219            "foo.go",
9220            |metric| {
9221                assert_eq!(metric.loc.sloc(), 5);
9222                assert_eq!(metric.loc.ploc(), 5);
9223                assert_eq!(metric.loc.lloc(), 3);
9224                assert_eq!(metric.loc.cloc(), 0);
9225                assert_eq!(metric.loc.blank(), 0);
9226                insta::assert_json_snapshot!(metric.loc);
9227            },
9228        );
9229    }
9230
9231    #[test]
9232    fn php_anonymous_function_in_function_lloc() {
9233        // Anonymous function `function ($x) { return $x + 1; }`: outer
9234        // sees the assignment + `return $f(2);`, the closure body adds
9235        // `return $x + 1;` for 3 LLOC aggregated.
9236        check_metrics::<PhpParser>(
9237            "<?php\nfunction outer() {\n    $f = function ($x) { return $x + 1; };\n    return $f(2);\n}\n",
9238            "foo.php",
9239            |metric| {
9240                assert_eq!(metric.loc.sloc(), 5);
9241                assert_eq!(metric.loc.ploc(), 5);
9242                assert_eq!(metric.loc.lloc(), 3);
9243                assert_eq!(metric.loc.cloc(), 0);
9244                assert_eq!(metric.loc.blank(), 0);
9245                insta::assert_json_snapshot!(metric.loc);
9246            },
9247        );
9248    }
9249
9250    #[test]
9251    fn php_arrow_function_in_function_lloc() {
9252        // The `fn ($x) => $x + 1` arrow function is an expression; the
9253        // outer function sees only its assignment and the `return $f(2);`.
9254        check_metrics::<PhpParser>(
9255            "<?php\nfunction outer() {\n    $f = fn ($x) => $x + 1;\n    return $f(2);\n}\n",
9256            "foo.php",
9257            |metric| {
9258                assert_eq!(metric.loc.sloc(), 5);
9259                assert_eq!(metric.loc.ploc(), 5);
9260                assert_eq!(metric.loc.lloc(), 2);
9261                assert_eq!(metric.loc.cloc(), 0);
9262                assert_eq!(metric.loc.blank(), 0);
9263                insta::assert_json_snapshot!(metric.loc);
9264            },
9265        );
9266    }
9267
9268    #[test]
9269    fn lua_nested_local_function_lloc() {
9270        // Two nested `local function` declarations: outer + inner both
9271        // count as `function_declaration` LLOC, plus the two `return`
9272        // statements = 4 aggregated.
9273        check_metrics::<LuaParser>(
9274            "local function outer()\n    local function inner()\n        return 1\n    end\n    return inner()\nend\n",
9275            "foo.lua",
9276            |metric| {
9277                assert_eq!(metric.loc.sloc(), 6);
9278                assert_eq!(metric.loc.ploc(), 6);
9279                assert_eq!(metric.loc.lloc(), 4);
9280                assert_eq!(metric.loc.cloc(), 0);
9281                assert_eq!(metric.loc.blank(), 0);
9282                insta::assert_json_snapshot!(metric.loc);
9283            },
9284        );
9285    }
9286
9287    #[test]
9288    fn lua_function_expression_in_local_decl_lloc() {
9289        // `local f = function (x) return x + 1 end` — the function
9290        // expression is its own space; aggregated LLOC: outer
9291        // declaration, the inner expression's declaration, the inner
9292        // `return x + 1`, and the outer `return f(2)` = 4.
9293        check_metrics::<LuaParser>(
9294            "local function outer()\n    local f = function (x) return x + 1 end\n    return f(2)\nend\n",
9295            "foo.lua",
9296            |metric| {
9297                assert_eq!(metric.loc.sloc(), 4);
9298                assert_eq!(metric.loc.ploc(), 4);
9299                assert_eq!(metric.loc.lloc(), 4);
9300                assert_eq!(metric.loc.cloc(), 0);
9301                assert_eq!(metric.loc.blank(), 0);
9302                insta::assert_json_snapshot!(metric.loc);
9303            },
9304        );
9305    }
9306
9307    #[test]
9308    fn tcl_apply_closure_lloc() {
9309        // `apply $f 2` is a regular Tcl command, not a separate function
9310        // space — tree-sitter-tcl does not model `apply { ... }` as a
9311        // closure construct distinct from any other command. We assert
9312        // the observed LLOC (proc, set, apply, plus the nested `expr`
9313        // command substitution inside the lambda body) so any future
9314        // change to lambda-body counting is caught here.
9315        check_metrics::<TclParser>(
9316            "proc outer {} {\n    set f [list x {return [expr {$x + 1}]}]\n    apply $f 2\n}\n",
9317            "foo.tcl",
9318            |metric| {
9319                assert_eq!(metric.loc.sloc(), 4);
9320                assert_eq!(metric.loc.ploc(), 4);
9321                assert_eq!(metric.loc.lloc(), 4);
9322                assert_eq!(metric.loc.cloc(), 0);
9323                assert_eq!(metric.loc.blank(), 0);
9324                insta::assert_json_snapshot!(metric.loc);
9325            },
9326        );
9327    }
9328
9329    #[test]
9330    fn perl_anonymous_sub_in_sub_lloc() {
9331        // Anonymous sub `sub { ... }` opens its own function space; the
9332        // outer LLOC counts the `my $f = ...;` declaration plus
9333        // `return $f->(2);`, and the anonymous sub contributes
9334        // `return $_[0] + 1;` for 2 LLOC.
9335        //
9336        // NOTE: a prior LLOC for this construct exists as
9337        // `perl_lloc_anonymous_function` (top-level form) — this test
9338        // asserts the same shape *inside* another sub, exercising space
9339        // nesting.
9340        check_metrics::<PerlParser>(
9341            "sub outer {\n    my $f = sub { return $_[0] + 1 };\n    return $f->(2);\n}\n",
9342            "foo.pl",
9343            |metric| {
9344                assert_eq!(metric.loc.sloc(), 4);
9345                assert_eq!(metric.loc.ploc(), 4);
9346                assert_eq!(metric.loc.lloc(), 2);
9347                assert_eq!(metric.loc.cloc(), 0);
9348                assert_eq!(metric.loc.blank(), 0);
9349                insta::assert_json_snapshot!(metric.loc);
9350            },
9351        );
9352    }
9353
9354    #[test]
9355    fn perl_named_sub_in_sub_lloc() {
9356        // Perl `sub` declarations are not LLOC (see
9357        // `perl_lloc_function_definition_not_counted`); inside `outer`,
9358        // only `return inner();` is LLOC, and `inner`'s `return 1` is in
9359        // its own space contributing one more aggregated LLOC.
9360        // Total aggregated LLOC: 1.
9361        //
9362        // Observation: lloc=1, not 2. Perl LLOC is anchored on `;`
9363        // tokens whose parent is `SourceFile` or `Block` (see
9364        // `PerlCode::compute` in this file). The bare `return 1` inside
9365        // `sub inner { ... }` has no trailing `;`, so it does not bump
9366        // LLOC. The outer `return inner();` carries the only SEMI.
9367        // This is intentional Perl behaviour and not a bug — Perl
9368        // requires `;` between statements; a single trailing statement
9369        // before `}` is syntactically optional. Asserted as-is.
9370        check_metrics::<PerlParser>(
9371            "sub outer {\n    sub inner { return 1 }\n    return inner();\n}\n",
9372            "foo.pl",
9373            |metric| {
9374                assert_eq!(metric.loc.sloc(), 4);
9375                assert_eq!(metric.loc.ploc(), 4);
9376                assert_eq!(metric.loc.lloc(), 1);
9377                assert_eq!(metric.loc.cloc(), 0);
9378                assert_eq!(metric.loc.blank(), 0);
9379                insta::assert_json_snapshot!(metric.loc);
9380            },
9381        );
9382    }
9383
9384    #[test]
9385    fn elixir_fn_inside_def_lloc() {
9386        // `fn x -> x + 1 end` inside a `def`: defmodule + def +
9387        // `f = fn ...` + `f.(2)` = 4 own LLOC for the Unit space, plus
9388        // the anonymous fn body `x + 1` = 1 nested, aggregated 5.
9389        check_metrics::<ElixirParser>(
9390            "defmodule Foo do\n  def outer do\n    f = fn x -> x + 1 end\n    f.(2)\n  end\nend\n",
9391            "foo.ex",
9392            |metric| {
9393                assert_eq!(metric.loc.sloc(), 6);
9394                assert_eq!(metric.loc.ploc(), 6);
9395                assert_eq!(metric.loc.lloc(), 5);
9396                assert_eq!(metric.loc.cloc(), 0);
9397                assert_eq!(metric.loc.blank(), 0);
9398                insta::assert_json_snapshot!(metric.loc);
9399            },
9400        );
9401    }
9402
9403    /// Regression for #437: `Loc` min/max must fold each nested function
9404    /// space's *own* min/max, not its aggregate value, so the smallest
9405    /// (and largest) leaf function propagates to the root. Before the fix,
9406    /// merge folded `other.sloc()` and a guarded `compute_minmax` skipped
9407    /// containers, so `sloc_min` reflected only top-level spaces.
9408    ///
9409    /// Layout: Unit (whole file) -> class C -> two methods of *different*
9410    /// sizes. The smaller method is the global minimum; the file/class
9411    /// spans are larger; `sloc_min` must be the small leaf, not the class
9412    /// or unit span. Verified against the pre-fix code by reverting the
9413    /// merge/compute_minmax change (it reports the unit span instead).
9414    #[test]
9415    fn rust_nested_min_max_propagates() {
9416        check_metrics::<RustParser>(
9417            "struct C;\nimpl C {\n    fn small(&self) {\n        let _ = 1;\n    }\n    fn big(&self) {\n        let _ = 1;\n        let _ = 2;\n        let _ = 3;\n    }\n}\n",
9418            "c.rs",
9419            |metric| {
9420                // Spaces: Unit + impl C + small() + big().
9421                // small() spans 3 rows (signature .. closing brace),
9422                // big() spans 5 rows. The Unit/impl spans are larger
9423                // still. sloc_min must be the smallest leaf (3), not the
9424                // top-level span; sloc_max must be the largest space.
9425                let loc = &metric.loc;
9426                assert!(
9427                    loc.sloc_min() <= loc.sloc(),
9428                    "sloc_min {} must not exceed unit sloc {}",
9429                    loc.sloc_min(),
9430                    loc.sloc()
9431                );
9432                assert_eq!(loc.sloc_min(), 3, "smallest leaf method span");
9433                assert_eq!(loc.sloc_max(), loc.sloc(), "largest space is the unit");
9434                // The smallest leaf has one statement; min must reflect it.
9435                assert_eq!(loc.lloc_min(), 1);
9436            },
9437        );
9438    }
9439
9440    /// Java sibling of `rust_nested_min_max_propagates` (#437). The
9441    /// `java_class_loc` snapshot above showed the bug directly: a class
9442    /// with methods reported `sloc_min == sloc` (the unit span). Here we
9443    /// assert the smallest method propagates.
9444    #[test]
9445    fn java_nested_min_max_propagates() {
9446        check_metrics::<JavaParser>(
9447            "public class C {\n  void small() {\n    int x = 1;\n  }\n  void big() {\n    int a = 1;\n    int b = 2;\n    int c = 3;\n  }\n}\n",
9448            "C.java",
9449            |metric| {
9450                // Spaces: Unit + class C + small() + big().
9451                let loc = &metric.loc;
9452                assert!(loc.sloc_min() <= loc.sloc());
9453                assert_eq!(loc.sloc_min(), 3, "smallest leaf method span");
9454                assert_eq!(loc.sloc_max(), loc.sloc());
9455                assert_eq!(loc.lloc_min(), 1);
9456            },
9457        );
9458    }
9459
9460    /// Python sibling of `rust_nested_min_max_propagates` (#437). Python
9461    /// nesting is class -> method, mirroring the worked example in the
9462    /// issue (file -> class C -> method m).
9463    #[test]
9464    fn python_nested_min_max_propagates() {
9465        check_metrics::<PythonParser>(
9466            "class C:\n    def small(self):\n        x = 1\n    def big(self):\n        a = 1\n        b = 2\n        c = 3\n",
9467            "c.py",
9468            |metric| {
9469                // Spaces: Unit + class C + small() + big().
9470                let loc = &metric.loc;
9471                assert!(loc.sloc_min() <= loc.sloc());
9472                assert_eq!(loc.sloc_min(), 2, "smallest leaf method span");
9473                assert_eq!(loc.sloc_max(), loc.sloc());
9474                assert_eq!(loc.lloc_min(), 1);
9475            },
9476        );
9477    }
9478
9479    /// `blank()` is `sloc - ploc - only_comment_lines`, an f64 subtraction
9480    /// that can go negative when a space's physical and comment line
9481    /// attribution overlaps its span row count. It must clamp at 0 so the
9482    /// serialized value is never negative (#437).
9483    ///
9484    /// Built from a synthetic [`Stats`] rather than a parsed fixture: the
9485    /// current grammars do not emit a parsed space whose root `sloc` is
9486    /// smaller than `ploc + only_comment_lines`, so a source fixture cannot
9487    /// drive the subtraction negative and would pass against the pre-clamp
9488    /// code (proving nothing). Setting `sloc = ploc = only_comment_lines = 1`
9489    /// yields a pre-clamp `blank` of `1 - 1 - 1 = -1`; reverting the
9490    /// `.max(0.0)` makes this assertion fail with `-1`.
9491    #[test]
9492    fn blank_clamps_negative_to_zero() {
9493        let mut stats = Stats::default();
9494        // A single-row span ending mid-line => sloc() of 1 row.
9495        stats.sloc.start = 0;
9496        // End row 0 ending mid-line, so that row counts: sloc() == 1.
9497        stats.sloc.end_line = 1;
9498        // ploc() is the cardinality of the physical-line set => 1.
9499        stats.ploc.lines.insert(0);
9500        // One comment-only line on the same single row.
9501        stats.cloc.only_comment_line_starts.insert(0);
9502
9503        // Pre-clamp this is 1 - 1 - 1 = -1.
9504        assert_eq!(stats.sloc(), 1);
9505        assert_eq!(stats.ploc(), 1);
9506        assert_eq!(
9507            stats.blank(),
9508            0,
9509            "blank() must clamp the negative subtraction to 0"
9510        );
9511    }
9512
9513    /// A physical line shared by two *sibling* function spaces must
9514    /// count once in the parent that merges them.
9515    ///
9516    /// This is the semantics `Ploc::merge` / `Cloc::merge` encode, and
9517    /// the reason the per-space line stores are sets rather than
9518    /// counters. It is asserted here across four language families —
9519    /// Rust, the C family, the JS family, and Python's indentation-based
9520    /// spaces — because the merge is shared by all of them and each
9521    /// family reaches it through a different `Loc::compute` body.
9522    ///
9523    /// Every fixture puts both spaces on one row, so a merge that summed
9524    /// instead of unioning would report `ploc`/`cloc` of 2 against an
9525    /// `sloc` of 1 — an impossible reading that also drives `blank`
9526    /// negative.
9527    #[test]
9528    fn sibling_spaces_sharing_a_line_count_it_once() {
9529        check_metrics::<RustParser>(
9530            "fn a() { let x = 1; } fn b() { let y = 2; }",
9531            "foo.rs",
9532            |metric| {
9533                assert_eq!(metric.loc.sloc(), 1);
9534                assert_eq!(metric.loc.ploc(), 1, "one physical row, two spaces");
9535                assert_eq!(metric.loc.blank(), 0);
9536            },
9537        );
9538
9539        check_metrics::<CppParser>(
9540            "int a() { return 1; } int b() { return 2; }",
9541            "foo.cpp",
9542            |metric| {
9543                assert_eq!(metric.loc.sloc(), 1);
9544                assert_eq!(metric.loc.ploc(), 1, "one physical row, two spaces");
9545                assert_eq!(metric.loc.blank(), 0);
9546            },
9547        );
9548
9549        check_metrics::<JavascriptParser>(
9550            "function a() { let x = 1; } function b() { let y = 2; }",
9551            "foo.js",
9552            |metric| {
9553                assert_eq!(metric.loc.sloc(), 1);
9554                assert_eq!(metric.loc.ploc(), 1, "one physical row, two spaces");
9555                assert_eq!(metric.loc.blank(), 0);
9556            },
9557        );
9558
9559        // Python cannot open two spaces on one row, so the shared row is
9560        // the nested `def`'s: it belongs to the inner space's span and to
9561        // the outer space's, and the unit merges both.
9562        check_metrics::<PythonParser>(
9563            "def a():
9564    def b(): return 1",
9565            "foo.py",
9566            |metric| {
9567                assert_eq!(metric.loc.sloc(), 2);
9568                assert_eq!(metric.loc.ploc(), 2, "two physical rows, three spaces");
9569                assert_eq!(metric.loc.blank(), 0);
9570            },
9571        );
9572    }
9573
9574    /// The same union property for comment lines: two sibling spaces that
9575    /// each carry a comment on the one shared row must yield `cloc == 1`.
9576    ///
9577    /// Summing rather than unioning would report `cloc == 2` against
9578    /// `sloc == 1`, the `cloc > sloc` state that pushes MI's
9579    /// comments_percentage above 100% (the failure mode of issue #461,
9580    /// here across the space merge rather than within one space).
9581    #[test]
9582    fn sibling_spaces_sharing_a_comment_line_count_it_once() {
9583        check_metrics::<CppParser>(
9584            "int a() { /*x*/ return 1; } int b() { /*y*/ return 2; }",
9585            "foo.cpp",
9586            |metric| {
9587                assert_eq!(metric.loc.sloc(), 1);
9588                assert_eq!(metric.loc.cloc(), 1, "one comment row, two spaces");
9589                assert!(
9590                    metric.loc.cloc() <= metric.loc.sloc(),
9591                    "cloc must never exceed sloc"
9592                );
9593            },
9594        );
9595
9596        check_metrics::<RustParser>(
9597            "fn a() { /*x*/ let p = 1; } fn b() { /*y*/ let q = 2; }",
9598            "foo.rs",
9599            |metric| {
9600                assert_eq!(metric.loc.sloc(), 1);
9601                assert_eq!(metric.loc.cloc(), 1, "one comment row, two spaces");
9602                assert!(
9603                    metric.loc.cloc() <= metric.loc.sloc(),
9604                    "cloc must never exceed sloc"
9605                );
9606            },
9607        );
9608    }
9609
9610    /// A row inside a chain of nested spaces is folded upward once per
9611    /// level, and must still count once at the top.
9612    ///
9613    /// Fifteen levels rather than two: at one level a union and a sum
9614    /// agree whenever the sets happen to be disjoint, and the point of
9615    /// #1109 is the repeated fold. The body row belongs to every level's
9616    /// span, so a merge that accumulated would report `ploc == 15`.
9617    #[test]
9618    fn a_row_folded_through_nested_spaces_counts_once() {
9619        const DEPTH: usize = 15;
9620        let source = format!(
9621            "{}let x = 1;{}",
9622            "fn f() { ".repeat(DEPTH),
9623            "} ".repeat(DEPTH)
9624        );
9625
9626        check_metrics::<RustParser>(&source, "foo.rs", |metric| {
9627            assert_eq!(metric.loc.sloc(), 1);
9628            assert_eq!(metric.loc.ploc(), 1, "one physical row, {DEPTH} spaces");
9629            assert_eq!(metric.loc.ploc_max(), 1);
9630            assert_eq!(metric.loc.blank(), 0);
9631        });
9632    }
9633
9634    /// Two inline block comments on a single code line must count as a
9635    /// single comment line, not one per comment node. Pre-fix this
9636    /// reported `cloc = 2` (one increment per node) for a one-line
9637    /// construct, violating `cloc <= sloc`/`cloc <= ploc` and pushing
9638    /// the MI comments_percentage above 100% (issue #461). Reverting
9639    /// the per-line de-dup in `add_code_comment_line` makes the
9640    /// `cloc == 1` assertions fail with `2`.
9641    #[test]
9642    fn cloc_multiple_block_comments_one_line_cpp() {
9643        check_metrics::<CppParser>(
9644            "int f(int /*a*/, int /*b*/) { return 1; }",
9645            "foo.cpp",
9646            |metric| {
9647                assert_eq!(metric.loc.cloc(), 1, "two inline comments => 1 cloc");
9648                assert!(
9649                    metric.loc.cloc() <= metric.loc.sloc(),
9650                    "cloc must not exceed sloc"
9651                );
9652                assert!(
9653                    metric.loc.cloc() <= metric.loc.ploc(),
9654                    "cloc must not exceed ploc for a single-line construct"
9655                );
9656            },
9657        );
9658    }
9659
9660    /// Sibling-language coverage: `add_cloc_lines` is shared across
9661    /// every block-comment language, so the Rust path must behave
9662    /// identically to C++ (issue #461).
9663    #[test]
9664    fn cloc_multiple_block_comments_one_line_rust() {
9665        check_metrics::<RustParser>(
9666            "fn f(/*a*/ x: i32, /*b*/ y: i32) -> i32 { 1 }",
9667            "foo.rs",
9668            |metric| {
9669                assert_eq!(metric.loc.cloc(), 1, "two inline comments => 1 cloc");
9670                assert!(
9671                    metric.loc.cloc() <= metric.loc.sloc(),
9672                    "cloc must not exceed sloc"
9673                );
9674            },
9675        );
9676    }
9677
9678    /// Guard against over-de-dup: a single multi-line block comment
9679    /// must still contribute one comment line per physical line it
9680    /// spans. The de-dup keys on the start row only, so the three
9681    /// independent continuation lines are unaffected (issue #461).
9682    #[test]
9683    fn cloc_multiline_block_comment_counts_each_line() {
9684        check_metrics::<CppParser>(
9685            "int g() {\n  /* l1\n     l2\n     l3 */\n  return 0;\n}",
9686            "foo.cpp",
9687            |metric| {
9688                assert_eq!(
9689                    metric.loc.cloc(),
9690                    3,
9691                    "a 3-line block comment counts 3 comment lines"
9692                );
9693            },
9694        );
9695    }
9696
9697    /// Two *standalone* block comments on a single physical line (no
9698    /// code) must count as one comment line, not one per node. #461
9699    /// deduped only inline co-located comments via the code-comment
9700    /// path; the standalone path bumped `only_comment_lines` per node,
9701    /// so `/*a*/ /*b*/` reported `cloc = 2` for a single line —
9702    /// violating `cloc <= sloc`. Reverting the per-line set in
9703    /// `add_only_comment_lines` makes the `cloc == 1` assertion fail
9704    /// with `2` (verified by reverting to `only_comment_lines += …`).
9705    #[test]
9706    fn cloc_multiple_standalone_block_comments_one_line_cpp() {
9707        check_metrics::<CppParser>("/*a*/ /*b*/", "foo.cpp", |metric| {
9708            assert_eq!(
9709                metric.loc.cloc(),
9710                1,
9711                "two standalone comments on one line => 1 cloc"
9712            );
9713            assert_eq!(metric.loc.sloc(), 1);
9714            assert!(
9715                metric.loc.cloc() <= metric.loc.sloc(),
9716                "cloc must not exceed sloc"
9717            );
9718        });
9719    }
9720
9721    /// Sibling-language coverage: the standalone de-dup lives in the
9722    /// shared `add_only_comment_lines` helper, so Rust must match C++
9723    /// (issue #461 follow-up).
9724    #[test]
9725    fn cloc_multiple_standalone_block_comments_one_line_rust() {
9726        check_metrics::<RustParser>("/*a*/ /*b*/", "foo.rs", |metric| {
9727            assert_eq!(
9728                metric.loc.cloc(),
9729                1,
9730                "two standalone comments on one line => 1 cloc"
9731            );
9732            assert_eq!(metric.loc.sloc(), 1);
9733            assert!(
9734                metric.loc.cloc() <= metric.loc.sloc(),
9735                "cloc must not exceed sloc"
9736            );
9737        });
9738    }
9739
9740    /// File-level guard: a comment-only physical line followed by a
9741    /// real code line still counts the comment line once and never
9742    /// exceeds sloc, even after the per-space merge that previously
9743    /// summed `code_comment_lines` (and double-counted a boundary line)
9744    /// rather than reading the per-line set. Three standalone comments
9745    /// share line 1, so the whole file has exactly one comment line.
9746    #[test]
9747    fn cloc_standalone_comments_then_code_no_double_count() {
9748        check_metrics::<CppParser>("/*a*/ /*b*/ /*c*/\nint x = 1;\n", "foo.cpp", |metric| {
9749            assert_eq!(metric.loc.sloc(), 2);
9750            assert_eq!(
9751                metric.loc.cloc(),
9752                1,
9753                "only line 1 carries comments => 1 cloc"
9754            );
9755            assert!(
9756                metric.loc.cloc() <= metric.loc.sloc(),
9757                "cloc must not exceed sloc"
9758            );
9759        });
9760    }
9761
9762    /// Interior blank lines are counted as BLANK and excluded from PLOC.
9763    /// sloc 6 (every line) / ploc 4 (4 code lines) / lloc 3 (handler + 2
9764    /// `set`s) / cloc 0 / blank 2.
9765    #[test]
9766    fn irules_blank() {
9767        check_metrics::<IrulesParser>(
9768            "when X {\n\n    set x 1\n\n    set y 2\n}\n",
9769            "foo.irule",
9770            |metric| {
9771                assert_eq!(metric.loc.sloc(), 6);
9772                assert_eq!(metric.loc.ploc(), 4);
9773                assert_eq!(metric.loc.lloc(), 3);
9774                assert_eq!(metric.loc.cloc(), 0);
9775                assert_eq!(metric.loc.blank(), 2);
9776            },
9777        );
9778    }
9779
9780    /// A handler body with no blank lines reports zero BLANK. lloc 3 =
9781    /// handler + `set` + `log` command.
9782    #[test]
9783    fn irules_no_zero_blank() {
9784        check_metrics::<IrulesParser>(
9785            "when HTTP_REQUEST {\n    set x 1\n    log local0. $x\n}\n",
9786            "foo.irule",
9787            |metric| {
9788                assert_eq!(metric.loc.sloc(), 4);
9789                assert_eq!(metric.loc.ploc(), 4);
9790                assert_eq!(metric.loc.lloc(), 3);
9791                assert_eq!(metric.loc.blank(), 0);
9792            },
9793        );
9794    }
9795
9796    /// `#`-prefixed comment lines are counted as CLOC (iRules has no block
9797    /// comments, so each comment node spans exactly one line).
9798    ///
9799    /// expected: rows 0-2 are comment-only, row 3 is the sole code row.
9800    /// `ploc` and `sloc` are asserted, not just `cloc`/`blank`: without
9801    /// them this test passed all the way through #1135, which credited
9802    /// each of the three comment rows to PLOC as well (`ploc == 4`,
9803    /// `cloc + ploc == 7` against `sloc == 4`).
9804    #[test]
9805    fn irules_cloc() {
9806        check_metrics::<IrulesParser>(
9807            "# a\n# b\n# c\nwhen X { set x 1 }\n",
9808            "foo.irule",
9809            |metric| {
9810                assert_eq!(metric.loc.sloc(), 4);
9811                assert_eq!(metric.loc.ploc(), 1);
9812                assert_eq!(metric.loc.cloc(), 3);
9813                assert_eq!(metric.loc.blank(), 0);
9814            },
9815        );
9816    }
9817
9818    /// LLOC counts each statement once: handler header, `if`, `set`, and the
9819    /// generic `log` command = 4. The `switch_arm` headers are not counted
9820    /// (their bodies' commands are), verified in `irules_switch_lloc`.
9821    #[test]
9822    fn irules_lloc() {
9823        check_metrics::<IrulesParser>(
9824            "when X {\n    if { $a } {\n        set x 1\n    }\n    log local0. done\n}\n",
9825            "foo.irule",
9826            |metric| {
9827                assert_eq!(metric.loc.lloc(), 4);
9828            },
9829        );
9830    }
9831
9832    /// A command inside `[...]` (`command_substitution`) is a sub-expression,
9833    /// not a top-level statement, so it does not add to LLOC. Here lloc 2 =
9834    /// handler + `set`; the inner `expr` is NOT counted. Removing the
9835    /// `CommandSubstitution` guard would push lloc to 3 — this is the loc
9836    /// gating-decision regression test.
9837    #[test]
9838    fn irules_no_command_substitution_lloc() {
9839        check_metrics::<IrulesParser>(
9840            "when X {\n    set y [expr { 1 + 2 }]\n}\n",
9841            "foo.irule",
9842            |metric| {
9843                assert_eq!(metric.loc.lloc(), 2);
9844            },
9845        );
9846    }
9847
9848    /// `switch` counts once; each arm's *body* command counts, but the
9849    /// `switch_arm` pattern/body pair itself is not a logical line. lloc 4 =
9850    /// handler + `switch` + two `set`s (one per arm body).
9851    #[test]
9852    fn irules_switch_lloc() {
9853        check_metrics::<IrulesParser>(
9854            "when X {\n    switch $h {\n        a { set r 1 }\n        b { set r 2 }\n    }\n}\n",
9855            "foo.irule",
9856            |metric| {
9857                assert_eq!(metric.loc.lloc(), 4);
9858            },
9859        );
9860    }
9861
9862    /// A `proc` definition and its `return` command are each one logical
9863    /// line: lloc 2.
9864    #[test]
9865    fn irules_proc_lloc() {
9866        check_metrics::<IrulesParser>(
9867            "proc f { a } {\n    return $a\n}\n",
9868            "foo.irule",
9869            |metric| {
9870                assert_eq!(metric.loc.lloc(), 2);
9871            },
9872        );
9873    }
9874
9875    /// Objective-C blank-line accounting: two code lines separated by
9876    /// blank lines.
9877    #[test]
9878    fn objc_blank() {
9879        check_metrics::<ObjcParser>(
9880            "
9881
9882            int a = 42;
9883
9884            int b = 43;
9885
9886            ",
9887            "foo.m",
9888            |metric| {
9889                assert_eq!(metric.loc.blank(), 1);
9890                insta::assert_json_snapshot!(metric.loc, @r#"
9891                {
9892                  "sloc": 3,
9893                  "ploc": 2,
9894                  "lloc": 2,
9895                  "cloc": 0,
9896                  "blank": 1,
9897                  "sloc_average": 3.0,
9898                  "ploc_average": 2.0,
9899                  "lloc_average": 2.0,
9900                  "cloc_average": 0.0,
9901                  "blank_average": 1.0,
9902                  "sloc_min": 3,
9903                  "sloc_max": 3,
9904                  "cloc_min": 0,
9905                  "cloc_max": 0,
9906                  "ploc_min": 2,
9907                  "ploc_max": 2,
9908                  "lloc_min": 2,
9909                  "lloc_max": 2,
9910                  "blank_min": 1,
9911                  "blank_max": 1
9912                }
9913                "#);
9914            },
9915        );
9916    }
9917
9918    /// Objective-C comment accounting: a block comment and a line
9919    /// comment each contribute to `cloc`.
9920    #[test]
9921    fn objc_cloc() {
9922        check_metrics::<ObjcParser>(
9923            "/* Block comment
9924            still the block */
9925            // Line comment
9926            int a = 42; // trailing",
9927            "foo.m",
9928            |metric| {
9929                insta::assert_json_snapshot!(metric.loc, @r#"
9930                {
9931                  "sloc": 4,
9932                  "ploc": 1,
9933                  "lloc": 1,
9934                  "cloc": 4,
9935                  "blank": 0,
9936                  "sloc_average": 4.0,
9937                  "ploc_average": 1.0,
9938                  "lloc_average": 1.0,
9939                  "cloc_average": 4.0,
9940                  "blank_average": 0.0,
9941                  "sloc_min": 4,
9942                  "sloc_max": 4,
9943                  "cloc_min": 4,
9944                  "cloc_max": 4,
9945                  "ploc_min": 1,
9946                  "ploc_max": 1,
9947                  "lloc_min": 1,
9948                  "lloc_max": 1,
9949                  "blank_min": 0,
9950                  "blank_max": 0
9951                }
9952                "#);
9953            },
9954        );
9955    }
9956
9957    /// Objective-C logical-line accounting: a method whose body has three
9958    /// statements. The `method_definition` opens a function space but is
9959    /// not itself a logical line; each statement adds one.
9960    #[test]
9961    fn objc_lloc() {
9962        check_metrics::<ObjcParser>(
9963            "@implementation Foo
9964- (int)bar {
9965    int a = 1;
9966    int b = 2;
9967    return a + b;
9968}
9969@end
9970",
9971            "foo.m",
9972            |metric| {
9973                // expected: decl `int a` (1) + decl `int b` (1) +
9974                // `return` (1) = 3.
9975                assert_eq!(metric.loc.lloc(), 3);
9976                insta::assert_json_snapshot!(metric.loc, @r#"
9977                {
9978                  "sloc": 7,
9979                  "ploc": 7,
9980                  "lloc": 3,
9981                  "cloc": 0,
9982                  "blank": 0,
9983                  "sloc_average": 2.3333333333333335,
9984                  "ploc_average": 2.3333333333333335,
9985                  "lloc_average": 1.0,
9986                  "cloc_average": 0.0,
9987                  "blank_average": 0.0,
9988                  "sloc_min": 5,
9989                  "sloc_max": 7,
9990                  "cloc_min": 0,
9991                  "cloc_max": 0,
9992                  "ploc_min": 5,
9993                  "ploc_max": 7,
9994                  "lloc_min": 3,
9995                  "lloc_max": 3,
9996                  "blank_min": 0,
9997                  "blank_max": 0
9998                }
9999                "#);
10000            },
10001        );
10002    }
10003
10004    /// Objective-C for-header gating: the `int i = 0` declaration in a
10005    /// classic `for` init slot is part of the `for` statement's logical
10006    /// line and must NOT add a second one (mirrors the C / C++ gate).
10007    /// Reverting the `count_specific_ancestors` gate would push lloc from
10008    /// 2 to 3.
10009    #[test]
10010    fn objc_no_declaration_in_for_header_lloc() {
10011        check_metrics::<ObjcParser>(
10012            "@implementation Foo
10013- (void)bar {
10014    for (int i = 0; i < 10; ++i) {
10015        [self use:i];
10016    }
10017}
10018@end
10019",
10020            "foo.m",
10021            |metric| {
10022                // expected: for-statement (1) + body expression
10023                // `[self use:i]` (1) = 2. The header `int i = 0`
10024                // declaration is gated out; without the gate this is 3.
10025                assert_eq!(metric.loc.lloc(), 2);
10026                insta::assert_json_snapshot!(metric.loc, @r#"
10027                {
10028                  "sloc": 7,
10029                  "ploc": 7,
10030                  "lloc": 2,
10031                  "cloc": 0,
10032                  "blank": 0,
10033                  "sloc_average": 2.3333333333333335,
10034                  "ploc_average": 2.3333333333333335,
10035                  "lloc_average": 0.6666666666666666,
10036                  "cloc_average": 0.0,
10037                  "blank_average": 0.0,
10038                  "sloc_min": 5,
10039                  "sloc_max": 7,
10040                  "cloc_min": 0,
10041                  "cloc_max": 0,
10042                  "ploc_min": 5,
10043                  "ploc_max": 7,
10044                  "lloc_min": 2,
10045                  "lloc_max": 2,
10046                  "blank_min": 0,
10047                  "blank_max": 0
10048                }
10049                "#);
10050            },
10051        );
10052    }
10053
10054    #[test]
10055    fn objc_at_directives_lloc() {
10056        // The only ObjC-specific LLOC work the impl does beyond the C
10057        // inheritance: `@synchronized` is a dedicated `synchronized_statement`
10058        // node (counts as a logical line), but `@autoreleasepool` emits
10059        // only a keyword token with no wrapping node, so its *header* adds
10060        // nothing and only its inner statements count.
10061        check_metrics::<ObjcParser>(
10062            "@implementation Foo
10063- (void)bar {
10064    @synchronized (self) {
10065        [self use];
10066    }
10067    @autoreleasepool {
10068        [self use];
10069    }
10070}
10071@end
10072",
10073            "foo.m",
10074            |metric| {
10075                // expected: @synchronized statement (1) + its body
10076                // `[self use]` (1) + the @autoreleasepool body `[self use]`
10077                // (1) = 3. The `@autoreleasepool` header contributes no
10078                // logical line (it has no node); if it did, this would be 4.
10079                assert_eq!(metric.loc.lloc(), 3);
10080                insta::assert_json_snapshot!(metric.loc, @r#"
10081                {
10082                  "sloc": 10,
10083                  "ploc": 10,
10084                  "lloc": 3,
10085                  "cloc": 0,
10086                  "blank": 0,
10087                  "sloc_average": 3.3333333333333335,
10088                  "ploc_average": 3.3333333333333335,
10089                  "lloc_average": 1.0,
10090                  "cloc_average": 0.0,
10091                  "blank_average": 0.0,
10092                  "sloc_min": 8,
10093                  "sloc_max": 10,
10094                  "cloc_min": 0,
10095                  "cloc_max": 0,
10096                  "ploc_min": 8,
10097                  "ploc_max": 10,
10098                  "lloc_min": 3,
10099                  "lloc_max": 3,
10100                  "blank_min": 0,
10101                  "blank_max": 0
10102                }
10103                "#);
10104            },
10105        );
10106    }
10107
10108    /// Analyses `source` byte-for-byte as Rust.
10109    ///
10110    /// Goes through `metrics_verbatim` rather than `check_metrics`
10111    /// because the #1051 cases end at EOF; see that helper for why.
10112    fn rust_loc(source: &[u8]) -> Stats {
10113        metrics_verbatim(crate::LANG::Rust, source, crate::MetricsOptions::default()).loc
10114    }
10115
10116    /// #1051: a Rust doc comment ending at EOF has no trailing newline for
10117    /// the scanner to consume, so its `LineComment` node ends on its own
10118    /// start row and discounting a row underflowed. On row 0 that panicked
10119    /// (debug: the subtraction; release: a hash-table capacity overflow in
10120    /// `add_only_comment_lines`). On any later row release did not crash —
10121    /// it silently reported one `cloc` too few.
10122    #[test]
10123    fn rust_doc_comment_at_eof_does_not_underflow() {
10124        // `end == start == 0` — underflowed at the subtraction itself.
10125        // expected: the sole row is one comment-only line, no code.
10126        let outer = rust_loc(b"/// x");
10127        assert_eq!(outer.cloc(), 1);
10128        assert_eq!(outer.ploc(), 0);
10129
10130        let inner = rust_loc(b"//! x");
10131        assert_eq!(inner.cloc(), 1);
10132        assert_eq!(inner.ploc(), 0);
10133
10134        // `end == start > 0` — the subtraction succeeded but drove `end`
10135        // below `start`. Release silently counted `cloc == 0` here.
10136        // expected: row 0 is code, row 1 is comment-only.
10137        let after_code = rust_loc(b"fn f(){}\n/// x");
10138        assert_eq!(after_code.cloc(), 1);
10139        assert_eq!(after_code.ploc(), 1);
10140
10141        // Two doc comments, the second at EOF: both rows are comment-only.
10142        // Pre-fix release reported 1 here, not 2.
10143        assert_eq!(rust_loc(b"/// a\n/// b").cloc(), 2);
10144
10145        // A doc comment sharing its row with code. `let` is not valid at
10146        // file scope, but tree-sitter parses it as a clean `let_declaration`
10147        // + `line_comment`, which is the only way to reach the
10148        // comment-after-code branch (no *valid* Rust puts `///` after code).
10149        let trailing = rust_loc(b"let x = 1; /// d");
10150        assert_eq!(trailing.cloc(), 1);
10151        assert_eq!(trailing.ploc(), 1);
10152    }
10153
10154    /// A doc comment at EOF must count exactly like a plain line comment at
10155    /// EOF. The `DocComment` adjustment exists only to discount the newline
10156    /// the scanner consumes; at EOF there is none to discount, so the two
10157    /// shapes are indistinguishable for LOC purposes.
10158    #[test]
10159    fn rust_doc_comment_at_eof_matches_plain_comment() {
10160        let plain = rust_loc(b"// x");
10161        // Pin the baseline absolutely too: parity alone would still hold if
10162        // both sides moved together, and would then read as a regression
10163        // when the un-newline-terminated `sloc` accounting is corrected.
10164        assert_eq!(plain.cloc(), 1);
10165        assert_eq!(plain.ploc(), 0);
10166
10167        for doc in [&b"/// x"[..], &b"//! x"[..]] {
10168            let doc = rust_loc(doc);
10169            assert_eq!(doc.cloc(), plain.cloc());
10170            assert_eq!(doc.ploc(), plain.ploc());
10171            assert_eq!(doc.sloc(), plain.sloc());
10172            assert_eq!(doc.blank(), plain.blank());
10173        }
10174    }
10175
10176    /// The newline-terminated path must stay unchanged by the #1051 guard.
10177    /// A `DocComment` node really does span one row more than it renders
10178    /// whenever the scanner consumed a newline, and that row must still be
10179    /// excluded — otherwise the guard would silently become a no-op and
10180    /// inflate CLOC for every doc-commented Rust file.
10181    #[test]
10182    fn rust_doc_comment_with_trailing_newline_still_discounts_the_row() {
10183        // expected: one rendered comment row, not two.
10184        assert_eq!(rust_loc(b"/// x\n").cloc(), 1);
10185        // expected: two consecutive doc comments are two rows, not four.
10186        assert_eq!(rust_loc(b"/// a\n/// b\n").cloc(), 2);
10187
10188        // The common real-world shape: doc comment attached to an item.
10189        // expected: row 0 comment-only, row 1 code.
10190        let documented = rust_loc(b"/// doc\nfn f() {}\n");
10191        assert_eq!(documented.cloc(), 1);
10192        assert_eq!(documented.ploc(), 1);
10193    }
10194
10195    /// CRLF is the boundary the guard must *not* fire on. `\r` is ordinary
10196    /// content to `process_line_doc_content`, so it consumes the following
10197    /// `\n` and the node does span an extra row — the discount is still
10198    /// owed. A lone trailing `\r` at EOF is the opposite case. Without this,
10199    /// a future grammar bump that stops consuming the newline would leave
10200    /// every LF test passing while the discount silently became dead code.
10201    #[test]
10202    fn rust_doc_comment_crlf_still_discounts_the_row() {
10203        // Newline consumed despite the `\r`: discount applies.
10204        assert_eq!(rust_loc(b"/// x\r\n").cloc(), 1);
10205        // expected: row 0 code, row 1 comment-only.
10206        let after_code = rust_loc(b"fn f(){}\r\n/// x");
10207        assert_eq!(after_code.cloc(), 1);
10208        assert_eq!(after_code.ploc(), 1);
10209        // Lone `\r`, then EOF: no newline consumed, so no discount is owed.
10210        assert_eq!(rust_loc(b"/// x\r").cloc(), 1);
10211    }
10212
10213    /// One-line, deliberately un-newline-terminated source, one entry per
10214    /// language whose `Loc` implementation is not a documented no-op.
10215    ///
10216    /// `sloc` is computed once, in the shared [`Sloc`], from the unit
10217    /// span the grammar hands us — so the only per-language variable is
10218    /// where each grammar puts the root node's end position. #1067 was
10219    /// possible precisely because that position was assumed rather than
10220    /// read, so the sweep is exhaustive rather than a sample: a future
10221    /// grammar whose root does *not* run to end-of-input has to show up
10222    /// here rather than silently lose a row.
10223    ///
10224    /// `Preproc` and `Ccomment` are excluded, but not because they are
10225    /// exempt from the rule. Their `Loc` impls are no-ops
10226    /// (`implement_metric_trait!(Loc, PreprocCode, CcommentCode)`, #188),
10227    /// so the *node-accumulated* sub-metrics are 0 by design — yet
10228    /// `sloc` is not node-accumulated: the walker anchors every Unit's
10229    /// row span at finalization, their synthetic Unit root included, so
10230    /// they carry a real span and drift with #1067 exactly as the
10231    /// languages below do. What they cannot join is the second sweep, whose final
10232    /// `mi != 0` assertion is unreachable with `ploc == 0`. They get
10233    /// their own check in
10234    /// `no_op_loc_grammars_still_count_their_unterminated_row`.
10235    const UNTERMINATED_ONE_LINERS: &[(crate::LANG, &[u8])] = &[
10236        (crate::LANG::Rust, b"fn main() {}"),
10237        (crate::LANG::C, b"int main(void) { return 0; }"),
10238        (crate::LANG::Cpp, b"int main() { return 0; }"),
10239        (crate::LANG::Mozcpp, b"int main() { return 0; }"),
10240        (crate::LANG::Objc, b"int main(void) { return 0; }"),
10241        (crate::LANG::Csharp, b"class C { void M() {} }"),
10242        (crate::LANG::Java, b"class C { void m() {} }"),
10243        (crate::LANG::Kotlin, b"fun main() {}"),
10244        (crate::LANG::Groovy, b"def f() {}"),
10245        (crate::LANG::Go, b"package main"),
10246        (crate::LANG::Javascript, b"function f() {}"),
10247        (crate::LANG::Mozjs, b"function f() {}"),
10248        (crate::LANG::Typescript, b"function f(): void {}"),
10249        (crate::LANG::Tsx, b"function f() {}"),
10250        (crate::LANG::Python, b"def f(): pass"),
10251        (crate::LANG::Ruby, b"def f; end"),
10252        (crate::LANG::Php, b"<?php function f() {}"),
10253        (crate::LANG::Perl, b"sub f { return 1; }"),
10254        (crate::LANG::Bash, b"f() { echo hi; }"),
10255        (crate::LANG::Lua, b"function f() end"),
10256        (crate::LANG::Tcl, b"proc f {} {}"),
10257        (crate::LANG::Irules, b"proc f {} {}"),
10258        (crate::LANG::Elixir, b"defmodule M do end"),
10259    ];
10260
10261    /// #1067: `Sloc::sloc()` derived the unit's row count as `end - start`,
10262    /// which is only right when a trailing newline pushes the root node's
10263    /// end onto a phantom extra row. Source that stops mid-line — anything
10264    /// not newline-terminated — lost its final row, so a one-line file
10265    /// reported `sloc == 0`.
10266    ///
10267    /// Uses [`metrics_verbatim`], not `check_metrics`: the latter trims and
10268    /// re-appends a trailing newline, which makes this entire input class
10269    /// unreachable and the test vacuous (the same blind spot that hid
10270    /// #1051).
10271    #[test]
10272    fn unterminated_one_line_file_reports_one_source_line() {
10273        for (lang, source) in UNTERMINATED_ONE_LINERS {
10274            let text = String::from_utf8_lossy(source);
10275            let loc = metrics_verbatim(*lang, source, MetricsOptions::default()).loc;
10276            assert_eq!(loc.sloc(), 1, "{lang:?} sloc for {text:?}");
10277            // The largest space can never be bigger than the file itself.
10278            assert_eq!(loc.sloc_max(), 1, "{lang:?} sloc_max for {text:?}");
10279            // The invariant documented on `Stats::with_cloc_sloc`: every
10280            // physical line is code, comment, both, or blank, so the code
10281            // and comment-only tallies cannot together exceed the row
10282            // count. `sloc == 0` broke it for any unterminated file whose
10283            // last line carried content.
10284            assert!(
10285                loc.cloc() + loc.ploc() <= loc.sloc(),
10286                "{lang:?}: cloc {} + ploc {} exceeds sloc {} for {text:?}",
10287                loc.cloc(),
10288                loc.ploc(),
10289                loc.sloc(),
10290            );
10291        }
10292    }
10293
10294    /// The two grammars whose `Loc` impl is the macro's no-op still get a
10295    /// `sloc`, so they drift with #1067 like everything else.
10296    ///
10297    /// Their root is not a `SpaceKind::Unit`, so `metrics_inner` pushes a
10298    /// synthetic Unit, whose row span `anchor_unit_sloc_span` fills in at
10299    /// finalization — a span the no-op `compute` never touches but
10300    /// `Sloc::sloc()` still measures.
10301    /// Before #1067 an unterminated one-liner measured `0` rows here too.
10302    /// Kept apart from [`UNTERMINATED_ONE_LINERS`] only because the
10303    /// `mi != 0` half of the sweep below cannot hold with `ploc == 0`.
10304    #[test]
10305    fn no_op_loc_grammars_still_count_their_unterminated_row() {
10306        for (lang, source) in [
10307            (crate::LANG::Preproc, &b"#define A 1"[..]),
10308            (crate::LANG::Ccomment, &b"/* c */"[..]),
10309        ] {
10310            let bare = metrics_verbatim(lang, source, MetricsOptions::default()).loc;
10311            assert_eq!(bare.sloc(), 1, "{lang:?} unterminated sloc");
10312            let mut terminated = source.to_vec();
10313            terminated.push(b'\n');
10314            let terminated = metrics_verbatim(lang, &terminated, MetricsOptions::default()).loc;
10315            assert_eq!(
10316                bare.sloc(),
10317                terminated.sloc(),
10318                "{lang:?} sloc must not depend on the trailing newline"
10319            );
10320            // The node-accumulated sub-metrics are the ones #188 zeroes.
10321            assert_eq!((bare.ploc(), bare.cloc(), bare.lloc()), (0, 0, 0));
10322        }
10323    }
10324
10325    /// A backslash-continued `#define` body is one `PreprocArg` node
10326    /// spanning every continuation row, so each of those rows is PLOC.
10327    ///
10328    /// The four C-family `Loc` impls carry an identical arm for this
10329    /// (`tree-sitter-cpp` does not expand macros — see the comment at
10330    /// each site), and until #1229 only C++'s copy was exercised: the
10331    /// other three were the sole uncovered lines in that PR. They are
10332    /// deliberate clones, so a fixture for one is a fixture for all
10333    /// four, and `Mozcpp` in particular owns no file extension and can
10334    /// only be reached by naming the language.
10335    ///
10336    /// Measured, and confirmed discriminating by deleting the arm from
10337    /// all four modules: `ploc` is 4 with it and 3 without, the lost row
10338    /// being the macro's last continuation line. `sloc` is 5 (three
10339    /// macro rows, one blank, one `main`) and `lloc` is 1 — the single
10340    /// `return` statement — since a `#define` declares no statement.
10341    #[test]
10342    fn a_continued_macro_body_counts_every_row_it_spans() {
10343        // Rows: 0-2 are the macro, 3 is blank, 4 is `main`.
10344        const CONTINUED_MACRO: &[u8] =
10345            b"#define SUM(a, b) \\\n    ((a) + \\\n     (b))\n\nint main(void) { return SUM(1, 2); }\n";
10346
10347        for lang in [
10348            crate::LANG::C,
10349            crate::LANG::Cpp,
10350            crate::LANG::Mozcpp,
10351            crate::LANG::Objc,
10352        ] {
10353            let loc = metrics_verbatim(lang, CONTINUED_MACRO, MetricsOptions::default()).loc;
10354            assert_eq!(
10355                loc.ploc(),
10356                4,
10357                "{lang:?}: every continuation row of the macro body is code"
10358            );
10359            assert_eq!(loc.sloc(), 5, "{lang:?} sloc");
10360            assert_eq!(loc.lloc(), 1, "{lang:?} lloc");
10361            assert_eq!(loc.cloc(), 0, "{lang:?} cloc");
10362            assert_eq!(loc.blank(), 1, "{lang:?} blank");
10363        }
10364    }
10365
10366    /// Whether the last line ends in a newline is a formatting detail, not
10367    /// a property of the code — no LOC sub-metric, and therefore no MI
10368    /// value, may depend on it. This is the invariant #1067 violated, and
10369    /// it pins the fix from both sides: the newline-terminated path (the
10370    /// one every in-tree harness exercises) must not move either.
10371    ///
10372    /// **The invariant is now unconditional.** It used to be scoped to
10373    /// source containing a token: whitespace-only input collapsed most
10374    /// grammars' roots to a zero-width node at end-of-input, leaving
10375    /// `sloc` no span to measure, so `b"  "` reported one row and
10376    /// `b"  \n"` reported none (#1087). #1247 anchored the unit's `sloc`
10377    /// span to the span the unit reports, which removed that dependence
10378    /// on where the root node happens to start. The whitespace-only class
10379    /// is swept separately — it cannot ride this test, whose closing
10380    /// `assert_ne!` requires a non-zero MI — in
10381    /// [`whitespace_only_input_is_uniform_across_grammars`].
10382    #[test]
10383    fn trailing_newline_does_not_change_loc_or_mi() {
10384        for (lang, source) in UNTERMINATED_ONE_LINERS {
10385            let text = String::from_utf8_lossy(source);
10386            let bare = metrics_verbatim(*lang, source, MetricsOptions::default());
10387            let mut newline_terminated = source.to_vec();
10388            newline_terminated.push(b'\n');
10389            let terminated =
10390                metrics_verbatim(*lang, &newline_terminated, MetricsOptions::default());
10391
10392            assert_eq!(
10393                bare.loc.sloc(),
10394                terminated.loc.sloc(),
10395                "{lang:?} sloc {text:?}"
10396            );
10397            assert_eq!(
10398                bare.loc.ploc(),
10399                terminated.loc.ploc(),
10400                "{lang:?} ploc {text:?}"
10401            );
10402            assert_eq!(
10403                bare.loc.cloc(),
10404                terminated.loc.cloc(),
10405                "{lang:?} cloc {text:?}"
10406            );
10407            assert_eq!(
10408                bare.loc.lloc(),
10409                terminated.loc.lloc(),
10410                "{lang:?} lloc {text:?}"
10411            );
10412            assert_eq!(
10413                bare.loc.blank(),
10414                terminated.loc.blank(),
10415                "{lang:?} blank {text:?}"
10416            );
10417            // The MI knock-on: `mi::inputs_are_empty` short-circuits to
10418            // 0.0 on `sloc <= 0`, so before the fix every unterminated
10419            // one-liner reported MI 0 while its newline-terminated twin
10420            // reported a real score. Inputs are now identical, so the
10421            // three formulas agree bit-for-bit.
10422            assert_eq!(
10423                bare.mi.original(),
10424                terminated.mi.original(),
10425                "{lang:?} mi {text:?}"
10426            );
10427            assert_eq!(
10428                bare.mi.sei(),
10429                terminated.mi.sei(),
10430                "{lang:?} mi.sei {text:?}"
10431            );
10432            assert_eq!(
10433                bare.mi.visual_studio(),
10434                terminated.mi.visual_studio(),
10435                "{lang:?} mi.visual_studio {text:?}",
10436            );
10437            assert_ne!(bare.mi.original(), 0.0, "{lang:?} mi must not be zeroed");
10438        }
10439    }
10440
10441    /// The second #1067 symptom: `b"fn f(){}\n/// x"` reported `sloc == 1`
10442    /// with `ploc == 1` *and* `cloc == 1`, so `cloc + ploc > sloc`. The
10443    /// file has two rows; only the missing one made the sums disagree.
10444    #[test]
10445    fn unterminated_trailing_comment_upholds_the_cloc_ploc_invariant() {
10446        // expected: row 0 is code, row 1 is comment-only, nothing blank.
10447        let loc = rust_loc(b"fn f(){}\n/// x");
10448        assert_eq!(loc.sloc(), 2);
10449        assert_eq!(loc.ploc(), 1);
10450        assert_eq!(loc.cloc(), 1);
10451        assert_eq!(loc.blank(), 0);
10452    }
10453
10454    /// Degenerate inputs, pinned so the end-column rule in
10455    /// `Node::end_line` cannot drift into fabricating rows for files
10456    /// that have none.
10457    #[test]
10458    fn degenerate_inputs_report_their_real_row_count() {
10459        // No bytes, no rows.
10460        assert_eq!(rust_loc(b"").sloc(), 0);
10461        // One row of whitespace, unterminated: the root node is empty but
10462        // sits at column 3, so the row is real and counts as blank.
10463        let spaces = rust_loc(b"   ");
10464        assert_eq!(spaces.sloc(), 1);
10465        assert_eq!(spaces.blank(), 1);
10466        // Newline-terminated whitespace was the #1087 carve-out: most
10467        // grammars collapse the root to a zero-width node at end-of-input
10468        // (`(1, 0)..(1, 0)` for `"\n"`), so the measured span had no rows
10469        // left to attribute and `sloc` was 0 for a file that plainly has
10470        // one. #1247 retired that: the unit's `sloc` span is now anchored
10471        // to the span the unit *reports*, which #1195 already anchored at
10472        // line 1, so a collapsed root no longer costs the file its rows.
10473        let one = rust_loc(b"\n");
10474        assert_eq!((one.sloc(), one.ploc(), one.blank()), (1, 0, 1));
10475        let two = rust_loc(b"\n\n");
10476        assert_eq!((two.sloc(), two.ploc(), two.blank()), (2, 0, 2));
10477    }
10478
10479    /// Every grammar whose `Loc` behaviour this module owns, including the
10480    /// two whose `compute` is the `implement_metric_trait!` no-op.
10481    ///
10482    /// [`UNTERMINATED_ONE_LINERS`] carries a fixture per language because
10483    /// its sweeps need parseable code; the whitespace-only sweep needs
10484    /// only the language, and must not omit `Preproc`/`Ccomment` — their
10485    /// synthetic Unit root is anchored like any other, so they carry a
10486    /// real span and answer the #1087/#1247 question too.
10487    fn all_loc_grammars() -> impl Iterator<Item = crate::LANG> {
10488        UNTERMINATED_ONE_LINERS
10489            .iter()
10490            .map(|(lang, _)| *lang)
10491            .chain([crate::LANG::Preproc, crate::LANG::Ccomment])
10492    }
10493
10494    /// #1087 accepted whitespace-only source as the one input class where
10495    /// a trailing newline moved `sloc` — twenty grammars collapse the root
10496    /// to a zero-width node at end-of-input, so `"  "` reported one row
10497    /// and `"  \n"` reported none, while five grammars (Elixir, Tcl,
10498    /// iRules, Preproc, Ccomment) kept the span and were newline-
10499    /// independent already. This sweep was written to pin both halves.
10500    ///
10501    /// #1247 removed the premise. The carve-out was a consequence of
10502    /// measuring the unit's `sloc` span from the root node's first token;
10503    /// once that span is anchored to the one the unit *reports* (#1195
10504    /// anchored the reported span at line 1), a collapsed root no longer
10505    /// erases the file's rows and every grammar answers alike. The sweep
10506    /// stays, with the split list retired: it now pins the *absence* of a
10507    /// per-grammar difference, which is the property a future grammar bump
10508    /// or walker change could still break.
10509    ///
10510    /// The unterminated side is unchanged and was always uniform: every
10511    /// grammar reports the row, as one blank line.
10512    #[test]
10513    fn whitespace_only_input_is_uniform_across_grammars() {
10514        // Spaces and tabs both, so a grammar that lexes one as extra and
10515        // the other as an error token cannot hide behind the sweep.
10516        for bare in [&b"  "[..], b"\t\t"] {
10517            let mut newline_terminated = bare.to_vec();
10518            newline_terminated.push(b'\n');
10519            for lang in all_loc_grammars() {
10520                let text = String::from_utf8_lossy(bare);
10521                let unterminated = metrics_verbatim(lang, bare, MetricsOptions::default()).loc;
10522                let terminated =
10523                    metrics_verbatim(lang, &newline_terminated, MetricsOptions::default()).loc;
10524
10525                assert_eq!(
10526                    (unterminated.sloc(), unterminated.blank()),
10527                    (1, 1),
10528                    "{lang:?}: unterminated {text:?} is one blank row for every grammar"
10529                );
10530
10531                assert_eq!(
10532                    (terminated.sloc(), terminated.blank()),
10533                    (1, 1),
10534                    "{lang:?}: newline-terminated {text:?} is the same one blank \
10535                     row — whether the grammar collapses its root at \
10536                     end-of-input is no longer observable in loc (#1247)"
10537                );
10538
10539                // Whitespace is never code and never a comment, whichever
10540                // side of the carve-out the grammar sits on. This is the
10541                // assertion #1135 broke for Tcl and iRules, whose row
10542                // terminator used to land in their PLOC catch-all.
10543                for loc in [&unterminated, &terminated] {
10544                    assert_eq!(
10545                        (loc.ploc(), loc.cloc(), loc.lloc()),
10546                        (0, 0, 0),
10547                        "{lang:?}: whitespace is neither code nor comment"
10548                    );
10549                }
10550            }
10551        }
10552    }
10553
10554    /// #1247: the unit anchors its *reported* span at line 1 (#1195) but
10555    /// measured its `sloc` span from the root node's first token, so blank
10556    /// rows above that token counted in neither `sloc` nor `blank` — while
10557    /// byte-identical rows one line lower counted in both.
10558    ///
10559    /// Swept across four grammars because the per-language `Loc` impls
10560    /// mirror each other. Tcl is in the list because it was already
10561    /// *right*: its row terminator is a token child of the root, so the
10562    /// root already started at row 0 and Tcl reported `sloc 4` for the
10563    /// Rust fixture's `sloc 1`. A per-language fix would have had to know
10564    /// which grammars were which; the walker-level anchor does not, and
10565    /// Tcl is the case that catches one being applied twice.
10566    ///
10567    /// `space_verbatim`, not `check_metrics`: the shim trims leading and
10568    /// trailing newlines, which deletes this test's entire subject.
10569    #[test]
10570    fn leading_blank_rows_count_in_the_units_sloc_and_blank() {
10571        const LEADING_BLANKS: u64 = 3;
10572        for (lang, body, ploc) in [
10573            (crate::LANG::Rust, &b"fn a() {}\n"[..], 1),
10574            (crate::LANG::Python, b"def a():\n    pass\n", 2),
10575            (crate::LANG::C, b"int f() { return 0; }\n", 1),
10576            (crate::LANG::Tcl, b"puts hi\n", 1),
10577        ] {
10578            let mut source = vec![b'\n'; LEADING_BLANKS as usize];
10579            source.extend_from_slice(body);
10580            let space = space_verbatim(lang, &source, MetricsOptions::default());
10581            let loc = &space.metrics.loc;
10582            assert_eq!(
10583                (loc.sloc(), loc.ploc(), loc.cloc(), loc.blank()),
10584                (LEADING_BLANKS + ploc, ploc, 0, LEADING_BLANKS),
10585                "{lang:?}: the leading rows are blank, not absent"
10586            );
10587            // The disagreement the issue is named for: the unit's own
10588            // reported span and its `sloc` are two spellings of one
10589            // number, and were not before.
10590            assert_eq!(
10591                loc.sloc() as usize,
10592                space.end_line - space.start_line + 1,
10593                "{lang:?}: sloc equals the rows of the unit's reported span"
10594            );
10595        }
10596    }
10597
10598    /// The two controls from #1247's evidence table. Both were already
10599    /// correct, and both are how the inconsistency was visible at all: a
10600    /// comment on line 1 flipped a byte-identical file from `sloc 1` to
10601    /// `sloc 4`, because comments are in the tree and blank rows are not.
10602    /// A fix that reached past the unit would move one of these.
10603    #[test]
10604    fn interior_blanks_and_leading_comments_are_unmoved_by_the_anchor() {
10605        // expected: rows 1 and 3 are code, row 2 is blank.
10606        let interior = rust_loc(b"fn a() {}\n\nfn b() {}\n");
10607        assert_eq!(
10608            (
10609                interior.sloc(),
10610                interior.ploc(),
10611                interior.cloc(),
10612                interior.blank()
10613            ),
10614            (3, 2, 0, 1)
10615        );
10616        // expected: row 1 is comment-only, rows 2-3 blank, row 4 code.
10617        let leading_comment = rust_loc(b"// c\n\n\nfn a() {}\n");
10618        assert_eq!(
10619            (
10620                leading_comment.sloc(),
10621                leading_comment.ploc(),
10622                leading_comment.cloc(),
10623                leading_comment.blank()
10624            ),
10625            (4, 1, 1, 2)
10626        );
10627    }
10628
10629    /// The anchor is gated on `SpaceKind::Unit`, and that gate is the
10630    /// entire separation between "the file starts at line 1" and "every
10631    /// space starts at line 1". Asserted on the nested space's `sloc` as
10632    /// well as its span, because only the `sloc` half is new.
10633    #[test]
10634    fn the_unit_anchor_does_not_reach_nested_spaces() {
10635        let space = space_verbatim(
10636            crate::LANG::Rust,
10637            b"\n\n\nfn a() {\n    let x = 1;\n}\n",
10638            MetricsOptions::default(),
10639        );
10640        assert_eq!((space.start_line, space.end_line), (1, 6));
10641        assert_eq!(space.metrics.loc.sloc(), 6, "the file has six rows");
10642        let nested = &space.spaces[0];
10643        assert_eq!((nested.start_line, nested.end_line), (4, 6));
10644        assert_eq!(
10645            nested.metrics.loc.sloc(),
10646            3,
10647            "the function's own three rows, not the file's six"
10648        );
10649        assert_eq!(nested.metrics.loc.blank(), 0, "the function has no blanks");
10650    }
10651
10652    /// `Sloc::exclude_span` subtracts each pruned subtree's row count from
10653    /// the enclosing span, so widening that span at the top could in
10654    /// principle desynchronise the two. It cannot: the rows the anchor
10655    /// adds are above the first token, and no pruned subtree can overlap
10656    /// them. Pinned rather than argued, since the failure mode is a
10657    /// silent `saturating_sub` clamp to 0 rather than a panic (#722,
10658    /// #1247).
10659    #[test]
10660    fn exclude_tests_pruning_composes_with_the_unit_anchor() {
10661        // Rows 1-3 blank, 4 `fn a`, 5 blank, 6 `#[test]`, 7-9 `fn t`.
10662        let source = b"\n\n\nfn a() {}\n\n#[test]\nfn t() {\n    assert!(true);\n}\n";
10663        let kept = metrics_verbatim(
10664            crate::LANG::Rust,
10665            source,
10666            MetricsOptions::default().with_exclude_tests(true),
10667        )
10668        .loc;
10669        // The pruned node is the `fn t` item, rows 7-9; its `#[test]`
10670        // attribute is a sibling and stays, which is #722's shape and not
10671        // something the anchor changes. What the anchor decides is the
10672        // other end: `blank` is 4 rather than 1, because rows 1-3 are now
10673        // inside the span the pruning subtracts from.
10674        assert_eq!(
10675            (kept.sloc(), kept.ploc(), kept.cloc(), kept.blank()),
10676            (6, 2, 0, 4),
10677            "the three pruned rows leave; the three leading blanks stay"
10678        );
10679
10680        let unpruned = rust_loc(source);
10681        assert_eq!(
10682            (unpruned.sloc(), unpruned.ploc(), unpruned.blank()),
10683            (9, 5, 4),
10684            "the same file unpruned — the anchor is what makes both blank counts 4"
10685        );
10686    }
10687
10688    /// The non-unit half of the same off-by-one. tree-sitter-perl's
10689    /// `function_definition` swallows the newline after the closing brace
10690    /// of a file's **last** `sub`, so that node's span ends at column 0 of
10691    /// a row it does not occupy. The old unconditional `+ 1` credited that
10692    /// row, inflating the last sub of every Perl file by one line — here,
10693    /// reporting a 3-row `sub` as 4.
10694    #[test]
10695    fn perl_last_sub_does_not_absorb_the_trailing_newline() {
10696        // Two identical 3-row subs; only the second hits the quirk.
10697        let space = space_verbatim(
10698            crate::LANG::Perl,
10699            b"sub f {\n    return 1;\n}\nsub g {\n    return 2;\n}\n",
10700            MetricsOptions::default(),
10701        );
10702        assert_eq!(space.metrics.loc.sloc(), 6, "the file has six rows");
10703        let subs: Vec<u64> = space
10704            .spaces
10705            .iter()
10706            .map(|child| child.metrics.loc.sloc())
10707            .collect();
10708        assert_eq!(subs, vec![3, 3], "both subs occupy three rows");
10709    }
10710
10711    /// #1135: Tcl and its iRules dialect are the only grammars here that
10712    /// surface the row terminator as a token child of the root. `LF`'s
10713    /// start row is the row it *terminates*, so the `_` catch-all in
10714    /// their `Loc` impls inserted that row into PLOC — turning every
10715    /// comment-only and whitespace-only row into a line of code.
10716    ///
10717    /// A wholly empty row never showed the defect: the `LF` that starts
10718    /// on it is the one tree-sitter collapses at end-of-input. A row of
10719    /// *whitespace* does, and trailing whitespace on an otherwise blank
10720    /// line is ordinary in real source — which is what makes the two
10721    /// spellings' disagreement the sharpest assertion here.
10722    ///
10723    /// Uses [`metrics_verbatim`] so the fixtures reach the parser
10724    /// byte-for-byte; `check_metrics` rewrites the trailing newline.
10725    #[test]
10726    fn tcl_family_does_not_count_terminator_rows_as_code() {
10727        for lang in [crate::LANG::Tcl, crate::LANG::Irules] {
10728            // Three rows: code, whitespace-only, code.
10729            let padded = metrics_verbatim(
10730                lang,
10731                b"proc f {} {}\n   \nproc g {} {}\n",
10732                MetricsOptions::default(),
10733            )
10734            .loc;
10735            assert_eq!(padded.sloc(), 3, "{lang:?} sloc");
10736            assert_eq!(padded.ploc(), 2, "{lang:?} ploc");
10737            assert_eq!(padded.blank(), 1, "{lang:?} blank");
10738
10739            // Whether a blank row carries spaces is not a property of the
10740            // code, so the empty-row spelling must agree exactly. Pre-fix
10741            // this side stayed correct while the padded side reported
10742            // `ploc 3 / blank 0`.
10743            let empty = metrics_verbatim(
10744                lang,
10745                b"proc f {} {}\n\nproc g {} {}\n",
10746                MetricsOptions::default(),
10747            )
10748            .loc;
10749            assert_eq!(
10750                (padded.sloc(), padded.ploc(), padded.blank()),
10751                (empty.sloc(), empty.ploc(), empty.blank()),
10752                "{lang:?}: trailing whitespace on a blank row must not make it code"
10753            );
10754
10755            // The comment-only half of the same defect. It escaped
10756            // `unterminated_one_line_file_reports_one_source_line`
10757            // because that sweep's fixtures are one-liners with no
10758            // comment row. The cross-language version of this case is
10759            // `a_comment_row_is_never_counted_as_code`; the two rows here
10760            // stay so the Tcl-family regression reads in one place.
10761            let commented = metrics_verbatim(
10762                lang,
10763                b"# lead-in\nproc f {} {}\n",
10764                MetricsOptions::default(),
10765            )
10766            .loc;
10767            assert_eq!(commented.sloc(), 2, "{lang:?} commented sloc");
10768            assert_eq!(commented.ploc(), 1, "{lang:?} commented ploc");
10769            assert_eq!(commented.cloc(), 1, "{lang:?} commented cloc");
10770            assert_eq!(commented.blank(), 0, "{lang:?} commented blank");
10771        }
10772    }
10773
10774    /// The comment spellings to sweep for each language: the line form
10775    /// every language has, then the block and doc forms where one exists.
10776    ///
10777    /// Nothing here needs to be exhaustive per language — the defect this
10778    /// guards against is a stray *token* inside (or terminating) a
10779    /// comment node reaching a PLOC catch-all, which any one spelling of
10780    /// a comment exposes. The block and doc entries are there because
10781    /// those nodes have child tokens the line form does not.
10782    fn comment_spellings(lang: crate::LANG) -> &'static [&'static str] {
10783        use crate::LANG::*;
10784        match lang {
10785            Python | Ruby | Bash | Elixir | Tcl | Irules | Perl => &["# c"],
10786            Lua => &["-- c", "--[[ c ]]"],
10787            Php => &["# c", "/* c */", "/** c */"],
10788            Rust => &["// c", "/* c */", "/// c"],
10789            _ => &["// c", "/* c */"],
10790        }
10791    }
10792
10793    /// A comment-only row is never a physical line of code — in any
10794    /// language, in any comment spelling, on either side of the code.
10795    ///
10796    /// Two separate defects broke this, both by letting a token reach the
10797    /// `_` catch-all that ends `stats.ploc.lines.insert(start)`. In Tcl
10798    /// and iRules it was the row terminator, whose start row is the row
10799    /// it terminates (#1135). In Perl it was the `#` *inside* the
10800    /// `comments` node, which additionally tripped
10801    /// `check_comment_ends_on_code_line` into reclassifying the row from
10802    /// comment-only to code-and-comment (#1137).
10803    ///
10804    /// Neither was visible to the per-language `*_cloc` tests — several
10805    /// assert `cloc` and `blank` and leave `ploc` unpinned — nor to
10806    /// `unterminated_one_line_file_reports_one_source_line`, whose
10807    /// fixtures carry no comment row at all. Since the failure mode is
10808    /// structural rather than language-specific, the sweep is per
10809    /// language rather than a sample.
10810    #[test]
10811    fn a_comment_row_is_never_counted_as_code() {
10812        for (lang, code) in UNTERMINATED_ONE_LINERS {
10813            // PHP is the one language whose code fixture must open the
10814            // file: outside `<?php` every row is inline HTML, so a
10815            // comment placed before it is not a comment at all.
10816            let must_lead = *lang == crate::LANG::Php;
10817            for &comment in comment_spellings(*lang) {
10818                let orders: &[bool] = if must_lead { &[false] } else { &[true, false] };
10819                for &comment_first in orders {
10820                    let mut src = Vec::new();
10821                    let (first, second): (&[u8], &[u8]) = if comment_first {
10822                        (comment.as_bytes(), code)
10823                    } else {
10824                        (code, comment.as_bytes())
10825                    };
10826                    src.extend_from_slice(first);
10827                    src.push(b'\n');
10828                    src.extend_from_slice(second);
10829                    src.push(b'\n');
10830
10831                    let loc = metrics_verbatim(*lang, &src, MetricsOptions::default()).loc;
10832                    let text = String::from_utf8_lossy(&src);
10833                    assert_eq!(loc.sloc(), 2, "{lang:?} sloc for {text:?}");
10834                    assert_eq!(
10835                        loc.ploc(),
10836                        1,
10837                        "{lang:?} ploc for {text:?} — the comment row is not code"
10838                    );
10839                    assert_eq!(loc.cloc(), 1, "{lang:?} cloc for {text:?}");
10840                    assert_eq!(loc.blank(), 0, "{lang:?} blank for {text:?}");
10841                }
10842            }
10843        }
10844    }
10845
10846    /// A `var` / `let` / `const` declaration is an executable statement and
10847    /// counts one LLOC, the same as Java's `LocalVariableDeclaration` and
10848    /// Rust's `let` (#1283 — before the fix a declarations-only file
10849    /// reported `lloc 0`). The fourth row is one `variable_declaration`
10850    /// carrying two declarators, so it counts once, not twice. The fifth row
10851    /// is a `using_declaration` — the grammar's third executable declaration
10852    /// kind, which TypeScript and TSX do not have.
10853    #[test]
10854    fn javascript_declaration_lloc() {
10855        check_metrics::<JavascriptParser>(
10856            "var a = 1;\nlet b = 2;\nconst c = 3;\nvar d = 4, e = 5;\nusing r = open();\n",
10857            "foo.js",
10858            |metric| {
10859                assert_eq!(metric.loc.sloc(), 5);
10860                assert_eq!(metric.loc.ploc(), 5);
10861                assert_eq!(metric.loc.lloc(), 5);
10862                assert_eq!(metric.loc.cloc(), 0);
10863                assert_eq!(metric.loc.blank(), 0);
10864            },
10865        );
10866    }
10867
10868    /// The classic `for (var i = 0; …)` header is part of the `ForStatement`,
10869    /// which already counts its own LLOC, so the header declaration must not
10870    /// add a second one. `for (const x of …)` and `for (var k in …)` need no
10871    /// carve-out at all: the grammar inlines the `const` / `var` keyword into
10872    /// `for_in_statement` and emits no declaration node — they are here so a
10873    /// carve-out wrongly widened to `ForInStatement` still has an input that
10874    /// notices. `var s = i;` in the loop *body* is a real logical line: the
10875    /// `StatementBlock` stops the ancestor walk (#1283).
10876    ///
10877    /// expected: for-statement 1 + body declaration 1 + for-of 1 + for-in 1 = 4
10878    #[test]
10879    fn javascript_for_header_declaration_not_double_counted() {
10880        check_metrics::<JavascriptParser>(
10881            "function f(arr, obj) {\n    for (var i = 0; i < 3; i++) {\n        var s = i;\n    }\n    for (const x of arr) {}\n    for (var k in obj) {}\n}\n",
10882            "foo.js",
10883            |metric| {
10884                assert_eq!(metric.loc.sloc(), 7);
10885                assert_eq!(metric.loc.ploc(), 7);
10886                assert_eq!(metric.loc.lloc(), 4);
10887                assert_eq!(metric.loc.cloc(), 0);
10888                assert_eq!(metric.loc.blank(), 0);
10889            },
10890        );
10891    }
10892
10893    /// `export const a = 1;` is one logical line, not two: the declaration
10894    /// nests inside the `ExportStatement`, whose arm already counted the row.
10895    /// The declaration inside the exported function body still counts — the
10896    /// `StatementBlock` stops the ancestor walk before the `ExportStatement`
10897    /// is reached (#1283).
10898    ///
10899    /// expected: 4 export statements + the `const c = 4;` in `f`'s body = 5
10900    #[test]
10901    fn javascript_exported_declaration_counts_once() {
10902        check_metrics::<JavascriptParser>(
10903            "export const a = 1;\nexport let b = 2;\nexport default 3;\nexport function f() { const c = 4; }\n",
10904            "foo.js",
10905            |metric| {
10906                assert_eq!(metric.loc.sloc(), 4);
10907                assert_eq!(metric.loc.ploc(), 4);
10908                assert_eq!(metric.loc.lloc(), 5);
10909                assert_eq!(metric.loc.cloc(), 0);
10910                assert_eq!(metric.loc.blank(), 0);
10911            },
10912        );
10913    }
10914
10915    /// A `var` / `let` / `const` declaration is an executable statement and
10916    /// counts one LLOC, the same as Java's `LocalVariableDeclaration` and
10917    /// Rust's `let` (#1283 — before the fix a declarations-only file
10918    /// reported `lloc 0`). The fourth row is one `variable_declaration`
10919    /// carrying two declarators, so it counts once, not twice. The fifth row
10920    /// is a `using_declaration` — the grammar's third executable declaration
10921    /// kind, which TypeScript and TSX do not have.
10922    #[test]
10923    fn mozjs_declaration_lloc() {
10924        check_metrics::<MozjsParser>(
10925            "var a = 1;\nlet b = 2;\nconst c = 3;\nvar d = 4, e = 5;\nusing r = open();\n",
10926            "foo.js",
10927            |metric| {
10928                assert_eq!(metric.loc.sloc(), 5);
10929                assert_eq!(metric.loc.ploc(), 5);
10930                assert_eq!(metric.loc.lloc(), 5);
10931                assert_eq!(metric.loc.cloc(), 0);
10932                assert_eq!(metric.loc.blank(), 0);
10933            },
10934        );
10935    }
10936
10937    /// The classic `for (var i = 0; …)` header is part of the `ForStatement`,
10938    /// which already counts its own LLOC, so the header declaration must not
10939    /// add a second one. `for (const x of …)` and `for (var k in …)` need no
10940    /// carve-out at all: the grammar inlines the `const` / `var` keyword into
10941    /// `for_in_statement` and emits no declaration node — they are here so a
10942    /// carve-out wrongly widened to `ForInStatement` still has an input that
10943    /// notices. `var s = i;` in the loop *body* is a real logical line: the
10944    /// `StatementBlock` stops the ancestor walk (#1283).
10945    ///
10946    /// expected: for-statement 1 + body declaration 1 + for-of 1 + for-in 1 = 4
10947    #[test]
10948    fn mozjs_for_header_declaration_not_double_counted() {
10949        check_metrics::<MozjsParser>(
10950            "function f(arr, obj) {\n    for (var i = 0; i < 3; i++) {\n        var s = i;\n    }\n    for (const x of arr) {}\n    for (var k in obj) {}\n}\n",
10951            "foo.js",
10952            |metric| {
10953                assert_eq!(metric.loc.sloc(), 7);
10954                assert_eq!(metric.loc.ploc(), 7);
10955                assert_eq!(metric.loc.lloc(), 4);
10956                assert_eq!(metric.loc.cloc(), 0);
10957                assert_eq!(metric.loc.blank(), 0);
10958            },
10959        );
10960    }
10961
10962    /// `export const a = 1;` is one logical line, not two: the declaration
10963    /// nests inside the `ExportStatement`, whose arm already counted the row.
10964    /// The declaration inside the exported function body still counts — the
10965    /// `StatementBlock` stops the ancestor walk before the `ExportStatement`
10966    /// is reached (#1283).
10967    ///
10968    /// expected: 4 export statements + the `const c = 4;` in `f`'s body = 5
10969    #[test]
10970    fn mozjs_exported_declaration_counts_once() {
10971        check_metrics::<MozjsParser>(
10972            "export const a = 1;\nexport let b = 2;\nexport default 3;\nexport function f() { const c = 4; }\n",
10973            "foo.js",
10974            |metric| {
10975                assert_eq!(metric.loc.sloc(), 4);
10976                assert_eq!(metric.loc.ploc(), 4);
10977                assert_eq!(metric.loc.lloc(), 5);
10978                assert_eq!(metric.loc.cloc(), 0);
10979                assert_eq!(metric.loc.blank(), 0);
10980            },
10981        );
10982    }
10983
10984    /// A `var` / `let` / `const` declaration is an executable statement and
10985    /// counts one LLOC, the same as Java's `LocalVariableDeclaration` and
10986    /// Rust's `let` (#1283 — before the fix a declarations-only file
10987    /// reported `lloc 0`). The fourth row is one `variable_declaration`
10988    /// carrying two declarators, so it counts once, not twice.
10989    #[test]
10990    fn typescript_declaration_lloc() {
10991        check_metrics::<TypescriptParser>(
10992            "var a: number = 1;\nlet b = 2;\nconst c = 3;\nvar d = 4, e = 5;\n",
10993            "foo.ts",
10994            |metric| {
10995                assert_eq!(metric.loc.sloc(), 4);
10996                assert_eq!(metric.loc.ploc(), 4);
10997                assert_eq!(metric.loc.lloc(), 4);
10998                assert_eq!(metric.loc.cloc(), 0);
10999                assert_eq!(metric.loc.blank(), 0);
11000            },
11001        );
11002    }
11003
11004    /// The classic `for (var i = 0; …)` header is part of the `ForStatement`,
11005    /// which already counts its own LLOC, so the header declaration must not
11006    /// add a second one. `for (const x of …)` and `for (var k in …)` need no
11007    /// carve-out at all: the grammar inlines the `const` / `var` keyword into
11008    /// `for_in_statement` and emits no declaration node — they are here so a
11009    /// carve-out wrongly widened to `ForInStatement` still has an input that
11010    /// notices. `var s = i;` in the loop *body* is a real logical line: the
11011    /// `StatementBlock` stops the ancestor walk (#1283).
11012    ///
11013    /// expected: for-statement 1 + body declaration 1 + for-of 1 + for-in 1 = 4
11014    #[test]
11015    fn typescript_for_header_declaration_not_double_counted() {
11016        check_metrics::<TypescriptParser>(
11017            "function f(arr, obj) {\n    for (var i = 0; i < 3; i++) {\n        var s = i;\n    }\n    for (const x of arr) {}\n    for (var k in obj) {}\n}\n",
11018            "foo.ts",
11019            |metric| {
11020                assert_eq!(metric.loc.sloc(), 7);
11021                assert_eq!(metric.loc.ploc(), 7);
11022                assert_eq!(metric.loc.lloc(), 4);
11023                assert_eq!(metric.loc.cloc(), 0);
11024                assert_eq!(metric.loc.blank(), 0);
11025            },
11026        );
11027    }
11028
11029    /// `export const a = 1;` is one logical line, not two: the declaration
11030    /// nests inside the `ExportStatement`, whose arm already counted the row.
11031    /// The declaration inside the exported function body still counts — the
11032    /// `StatementBlock` stops the ancestor walk before the `ExportStatement`
11033    /// is reached (#1283).
11034    ///
11035    /// expected: 4 export statements + the `const c = 4;` in `f`'s body = 5
11036    ///
11037    /// The TypeScript spelling also pins `export declare const …`, where an
11038    /// `ambient_declaration` sits between the export and the declaration: the
11039    /// carve-out walks the ancestor chain rather than checking the parent, so
11040    /// it still sees the enclosing `ExportStatement`.
11041    #[test]
11042    fn typescript_exported_declaration_counts_once() {
11043        check_metrics::<TypescriptParser>(
11044            "export const a: number = 1;\nexport declare const b: string;\nexport default 3;\nexport function f(): void { const c = 4; }\n",
11045            "foo.ts",
11046            |metric| {
11047                assert_eq!(metric.loc.sloc(), 4);
11048                assert_eq!(metric.loc.ploc(), 4);
11049                assert_eq!(metric.loc.lloc(), 5);
11050                assert_eq!(metric.loc.cloc(), 0);
11051                assert_eq!(metric.loc.blank(), 0);
11052            },
11053        );
11054    }
11055
11056    /// A `var` / `let` / `const` declaration is an executable statement and
11057    /// counts one LLOC, the same as Java's `LocalVariableDeclaration` and
11058    /// Rust's `let` (#1283 — before the fix a declarations-only file
11059    /// reported `lloc 0`). The fourth row is one `variable_declaration`
11060    /// carrying two declarators, so it counts once, not twice.
11061    #[test]
11062    fn tsx_declaration_lloc() {
11063        check_metrics::<TsxParser>(
11064            "var a: number = 1;\nlet b = 2;\nconst c = 3;\nvar d = 4, e = 5;\n",
11065            "foo.tsx",
11066            |metric| {
11067                assert_eq!(metric.loc.sloc(), 4);
11068                assert_eq!(metric.loc.ploc(), 4);
11069                assert_eq!(metric.loc.lloc(), 4);
11070                assert_eq!(metric.loc.cloc(), 0);
11071                assert_eq!(metric.loc.blank(), 0);
11072            },
11073        );
11074    }
11075
11076    /// The classic `for (var i = 0; …)` header is part of the `ForStatement`,
11077    /// which already counts its own LLOC, so the header declaration must not
11078    /// add a second one. `for (const x of …)` and `for (var k in …)` need no
11079    /// carve-out at all: the grammar inlines the `const` / `var` keyword into
11080    /// `for_in_statement` and emits no declaration node — they are here so a
11081    /// carve-out wrongly widened to `ForInStatement` still has an input that
11082    /// notices. `var s = i;` in the loop *body* is a real logical line: the
11083    /// `StatementBlock` stops the ancestor walk (#1283).
11084    ///
11085    /// expected: for-statement 1 + body declaration 1 + for-of 1 + for-in 1 = 4
11086    #[test]
11087    fn tsx_for_header_declaration_not_double_counted() {
11088        check_metrics::<TsxParser>(
11089            "function f(arr, obj) {\n    for (var i = 0; i < 3; i++) {\n        var s = i;\n    }\n    for (const x of arr) {}\n    for (var k in obj) {}\n}\n",
11090            "foo.tsx",
11091            |metric| {
11092                assert_eq!(metric.loc.sloc(), 7);
11093                assert_eq!(metric.loc.ploc(), 7);
11094                assert_eq!(metric.loc.lloc(), 4);
11095                assert_eq!(metric.loc.cloc(), 0);
11096                assert_eq!(metric.loc.blank(), 0);
11097            },
11098        );
11099    }
11100
11101    /// `export const a = 1;` is one logical line, not two: the declaration
11102    /// nests inside the `ExportStatement`, whose arm already counted the row.
11103    /// The declaration inside the exported function body still counts — the
11104    /// `StatementBlock` stops the ancestor walk before the `ExportStatement`
11105    /// is reached (#1283).
11106    ///
11107    /// expected: 4 export statements + the `const c = 4;` in `f`'s body = 5
11108    ///
11109    /// The TypeScript spelling also pins `export declare const …`, where an
11110    /// `ambient_declaration` sits between the export and the declaration: the
11111    /// carve-out walks the ancestor chain rather than checking the parent, so
11112    /// it still sees the enclosing `ExportStatement`.
11113    #[test]
11114    fn tsx_exported_declaration_counts_once() {
11115        check_metrics::<TsxParser>(
11116            "export const a: number = 1;\nexport declare const b: string;\nexport default 3;\nexport function f(): void { const c = 4; }\n",
11117            "foo.tsx",
11118            |metric| {
11119                assert_eq!(metric.loc.sloc(), 4);
11120                assert_eq!(metric.loc.ploc(), 4);
11121                assert_eq!(metric.loc.lloc(), 5);
11122                assert_eq!(metric.loc.cloc(), 0);
11123                assert_eq!(metric.loc.blank(), 0);
11124            },
11125        );
11126    }
11127
11128    /// A brace-less `for` body is a declaration with the *same* parent as
11129    /// the header, so a carve-out keyed on the enclosing kind alone
11130    /// dropped it (#1283 review): `for (…) var s = i;` counted one
11131    /// logical line where the braced spelling and `for (…) x++;` count
11132    /// two. The header is identified by the `initializer` field instead.
11133    /// The nested spelling — a `switch` as the brace-less body — is the
11134    /// same miss one level down.
11135    ///
11136    /// expected: for 1 + body declaration 1 = 2; for 1 + switch 1 +
11137    /// case declaration 1 = 3.
11138    #[test]
11139    fn js_family_braceless_for_body_declaration_counts() {
11140        const BODY: &str = "for (var i = 0; i < 3; i++) var s = i;\n";
11141        const NESTED: &str = "for (let i = 0; i < 2; i++) switch (i) { case 0: let y = 1; }\n";
11142        check_metrics::<JavascriptParser>(BODY, "foo.js", |m| assert_eq!(m.loc.lloc(), 2));
11143        check_metrics::<MozjsParser>(BODY, "foo.js", |m| assert_eq!(m.loc.lloc(), 2));
11144        check_metrics::<TypescriptParser>(BODY, "foo.ts", |m| assert_eq!(m.loc.lloc(), 2));
11145        check_metrics::<TsxParser>(BODY, "foo.tsx", |m| assert_eq!(m.loc.lloc(), 2));
11146        check_metrics::<JavascriptParser>(NESTED, "foo.js", |m| assert_eq!(m.loc.lloc(), 3));
11147        check_metrics::<MozjsParser>(NESTED, "foo.js", |m| assert_eq!(m.loc.lloc(), 3));
11148        check_metrics::<TypescriptParser>(NESTED, "foo.ts", |m| assert_eq!(m.loc.lloc(), 3));
11149        check_metrics::<TsxParser>(NESTED, "foo.tsx", |m| assert_eq!(m.loc.lloc(), 3));
11150    }
11151
11152    /// Ambient declarations execute nothing — `declare const x: T;` has
11153    /// no initializer to run — so they are no logical line, whether the
11154    /// `declare` is top-level or the declaration sits inside a
11155    /// `declare namespace` / `declare module` body (#1283 review: only
11156    /// the `export declare` spelling was carved out, so a `.d.ts` file
11157    /// reported one LLOC per `declare const`).
11158    ///
11159    /// expected: 0 — every row is ambient.
11160    #[test]
11161    fn typescript_ambient_declarations_are_not_logical_lines() {
11162        const SRC: &str = "declare const VERSION: string;\ndeclare let mutable: number;\ndeclare namespace NS { const inner: number; }\ndeclare module \"m\" { let y: string; }\n";
11163        check_metrics::<TypescriptParser>(SRC, "foo.ts", |m| assert_eq!(m.loc.lloc(), 0));
11164        check_metrics::<TsxParser>(SRC, "foo.tsx", |m| assert_eq!(m.loc.lloc(), 0));
11165    }
11166}