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::lang_helpers::python::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 rows 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 record each
105    // pruned subtree's rows here and subtract the cardinality in
106    // `sloc()` so SLOC drops in step with `ploc`/`cloc`/`lloc`
107    // (issue #722).
108    //
109    // A set rather than the counter this was until #1417, because rows
110    // shared with retained code have to be identifiable and removed
111    // before the subtraction — see `Stats::settle_excluded_rows`. It is
112    // the single source of truth; there is no cached count to fall out
113    // of step with it, matching what `ploc`/`cloc` already gave up
114    // when they became bitsets (#1109).
115    excluded_rows: LineSet,
116    sloc_min: usize,
117    sloc_max: usize,
118}
119
120impl Default for Sloc {
121    fn default() -> Self {
122        Self {
123            start: 0,
124            end_line: 0,
125            excluded_rows: LineSet::default(),
126            sloc_min: usize::MAX,
127            sloc_max: 0,
128        }
129    }
130}
131
132impl Sloc {
133    /// The `SLOC` metric value for this space (source lines, including blanks and comments).
134    #[inline]
135    #[must_use]
136    pub fn sloc(&self) -> u64 {
137        // This metric counts the number of physical lines this space
138        // occupies, including blanks and comments.
139        let span = span_rows(self.start, self.end_line);
140        // Subtract the rows belonging to `exclude_tests`-pruned subtrees
141        // (issue #722). `saturating_sub` is defensive: the set is
142        // clamped to the span and its rows are a subset of it, but a
143        // future caller that records a row outside the span must not
144        // wrap to `u64::MAX`.
145        span.saturating_sub(self.excluded_rows.len()) as u64
146    }
147
148    /// Records the rows of a pruned (`exclude_tests`) subtree so that
149    /// `sloc()` drops in step with the node-accumulated loc sub-metrics.
150    /// The arguments are the pruned node's own start row and
151    /// `Node::end_line`, so the rows recorded follow the same rule the
152    /// enclosing span was measured with.
153    ///
154    /// A *set* rather than the running count this kept until #1417. A
155    /// pruned item can share its first or last physical row with a
156    /// retained sibling — `fn a() {} #[cfg(test)] mod t { … }` is one
157    /// row, of which nothing is removed — and a count cannot tell that
158    /// row from one the prune really took, so `sloc()` fell below
159    /// `ploc()`. The set also makes two pruned siblings on one row
160    /// subtract that row once instead of twice.
161    ///
162    /// The rows recorded here are the pruned node's *whole* span;
163    /// [`Stats::settle_excluded_rows`] is what takes the shared ones
164    /// back out, once the retained sets for this space are complete.
165    #[inline]
166    pub(crate) fn exclude_span(&mut self, start_row: usize, end_line: usize) {
167        // `end_line` is the 1-based inclusive last row, so the last
168        // 0-based row is `end_line - 1`. Guarding here rather than
169        // leaning on `insert_range`'s inverted-span return, which would
170        // need `end_line - 1` computed first: at `end_line == 0` that
171        // saturates to row 0 and records a row the node does not
172        // occupy, and an empty span is not an inverted one.
173        if end_line <= start_row {
174            debug_assert!(
175                end_line == start_row,
176                "exclude_span: end_line {end_line} < start_row {start_row}"
177            );
178            return;
179        }
180        // Exact under the guard — `end_line > start_row >= 0` gives
181        // `end_line >= 1` — and saturating for the module's
182        // `arithmetic_side_effects` warning, which cannot see that.
183        // The row count inserted is `span_rows(start_row, end_line)`,
184        // byte-identical to the count this used to add.
185        self.excluded_rows
186            .insert_range(start_row, end_line.saturating_sub(1));
187    }
188
189    /// The `Sloc` metric minimum value. See `min_or_zero` for the
190    /// `usize::MAX` sentinel guard.
191    #[inline]
192    #[must_use]
193    pub fn sloc_min(&self) -> u64 {
194        min_or_zero(self.sloc_min)
195    }
196
197    /// The `Sloc` metric maximum value.
198    #[inline]
199    #[must_use]
200    pub fn sloc_max(&self) -> u64 {
201        self.sloc_max as u64
202    }
203
204    /// Folds `other` into `self`, updating the min/max accumulators and
205    /// unioning the child's `exclude_tests`-pruned rows.
206    #[inline]
207    pub fn merge(&mut self, other: &Sloc) {
208        // Fold the child's own min/max (not its aggregate `sloc()`), so the
209        // granularity of deeply nested function spaces propagates to the
210        // root. This matches every sibling metric (cyclomatic, cognitive,
211        // exit, nargs, nom, tokens, abc) and fixed issue #437.
212        self.sloc_min = self.sloc_min.min(other.sloc_min);
213        self.sloc_max = self.sloc_max.max(other.sloc_max);
214
215        // Propagate the child's pruned rows upward so an ancestor's
216        // span-based `sloc()` drops by the same rows, mirroring how `Ploc`
217        // unions its line-set upward (`Ploc::merge`). The prune hook records
218        // each pruned subtree's span only on its innermost enclosing
219        // func-space; without this fold a `#[test] fn` inside a retained
220        // `impl`/`trait`/closure would shrink only that space's `sloc`,
221        // leaving every enclosing space (including the unit, which feeds
222        // MI's SLOC term) inflated (issue #741, #722 follow-up). Each
223        // ancestor's span already includes the pruned rows exactly once, so
224        // subtracting them once per level cannot double-count: pruned
225        // subtrees never descend, so a nested pruned item is recorded on a
226        // single space and folded up one altitude at a time.
227        //
228        // Union rather than the `saturating_add` this was until #1417.
229        // Addition double-counted two pruned siblings sharing one
230        // physical row, and — the reason the fold is safe to leave
231        // exactly as it is — the child arrives here already settled
232        // against its own retained rows, which the parent's later
233        // `settle_excluded_rows` can only narrow further:
234        // `child_retained ⊆ parent_retained`, so
235        // `(X \ child_retained) \ parent_retained == X \ parent_retained`.
236        // The order the two settle in therefore does not matter.
237        self.excluded_rows.union_with(&other.excluded_rows);
238    }
239
240    #[inline]
241    pub(crate) fn compute_minmax(&mut self) {
242        // Fold this space's own span unconditionally so containers (Unit,
243        // classes) participate in min/max, matching the sibling metrics'
244        // convention. Each space runs this before being merged upward, so
245        // the guarded form previously here dropped nested leaves (#437).
246        self.sloc_min = self.sloc_min.min(self.sloc() as usize);
247        self.sloc_max = self.sloc_max.max(self.sloc() as usize);
248    }
249}
250
251/// The `PLoc` metric suite.
252#[derive(Debug, Clone, PartialEq)]
253pub struct Ploc {
254    lines: LineSet,
255    ploc_min: usize,
256    ploc_max: usize,
257}
258
259impl Default for Ploc {
260    fn default() -> Self {
261        Self {
262            lines: LineSet::default(),
263            ploc_min: usize::MAX,
264            ploc_max: 0,
265        }
266    }
267}
268
269impl Ploc {
270    /// The `PLOC` metric value for this space (physical lines of code, excluding blanks and comments).
271    #[inline]
272    #[must_use]
273    pub fn ploc(&self) -> u64 {
274        // This metric counts the number of instruction lines in a code
275        // https://en.wikipedia.org/wiki/Source_lines_of_code
276        self.lines.len() as u64
277    }
278
279    /// The `Ploc` metric minimum value. See `min_or_zero` for the
280    /// `usize::MAX` sentinel guard.
281    #[inline]
282    #[must_use]
283    pub fn ploc_min(&self) -> u64 {
284        min_or_zero(self.ploc_min)
285    }
286
287    /// The `Ploc` metric maximum value.
288    #[inline]
289    #[must_use]
290    pub fn ploc_max(&self) -> u64 {
291        self.ploc_max as u64
292    }
293
294    /// Folds `other` into `self`, unioning the line set and updating min/max.
295    #[inline]
296    pub fn merge(&mut self, other: &Ploc) {
297        // Union the child's physical lines in, so a line shared with a
298        // sibling space is counted once. A word-wise OR rather than an
299        // insert per row: a line inside D nested spaces is folded upward
300        // D times (#1109).
301        self.lines.union_with(&other.lines);
302
303        // Fold the child's own min/max so nested spaces propagate (#437).
304        self.ploc_min = self.ploc_min.min(other.ploc_min);
305        self.ploc_max = self.ploc_max.max(other.ploc_max);
306    }
307
308    #[inline]
309    pub(crate) fn compute_minmax(&mut self) {
310        // Fold this space's own value unconditionally so containers
311        // participate, matching the sibling metrics' convention (#437).
312        // Bound once: `ploc()` is a popcount over the whole word array
313        // since #1109, not the O(1) `HashSet::len` it used to be.
314        let ploc = self.ploc() as usize;
315        self.ploc_min = self.ploc_min.min(ploc);
316        self.ploc_max = self.ploc_max.max(ploc);
317    }
318}
319
320/// The `CLoc` metric suite.
321#[derive(Debug, Clone, PartialEq)]
322pub struct Cloc {
323    // Physical lines that are comment-only (no code). Feeds both
324    // `cloc()` and the `blank` metric (`sloc - ploc - only.len()`).
325    // A set rather than a counter so two standalone block comments on
326    // one physical line (`/*a*/ /*b*/`) contribute a single comment
327    // line, not one per node (issue #461 follow-up). Each spanned row
328    // of a genuine multi-line block comment is a distinct key, so it
329    // still counts once per line.
330    only_comment_line_starts: LineSet,
331    // Physical lines carrying both code and comment (`int x; /*c*/`).
332    // A line with several inline block comments (`f(int /*a*/, int
333    // /*b*/)`) must contribute a single comment line, not one per
334    // node, otherwise cloc can exceed sloc/ploc and push the MI
335    // comments_percentage above 100% (issue #461). Mirrors `Ploc`'s
336    // per-line de-dup via `Ploc::lines`; disjoint from
337    // `only_comment_line_starts` by construction.
338    code_comment_line_starts: LineSet,
339    comment_line_end: Option<usize>,
340    cloc_min: usize,
341    cloc_max: usize,
342}
343
344impl Default for Cloc {
345    fn default() -> Self {
346        Self {
347            only_comment_line_starts: LineSet::default(),
348            code_comment_line_starts: LineSet::default(),
349            comment_line_end: Option::default(),
350            cloc_min: usize::MAX,
351            cloc_max: 0,
352        }
353    }
354}
355
356impl Cloc {
357    /// The `CLOC` metric value for this space (comment lines, standalone + trailing).
358    #[inline]
359    #[must_use]
360    pub fn cloc(&self) -> u64 {
361        // Comments are counted regardless of their placement
362        // https://en.wikipedia.org/wiki/Source_lines_of_code
363        //
364        // Derive from the per-physical-line sets rather than summed
365        // counters so co-located comments (standalone or inline) count
366        // their shared line once and a comment line shared across
367        // merged spaces is not double-counted (issue #461). The two
368        // sets are disjoint by construction, but a union is used
369        // defensively so a stray overlap cannot inflate the count.
370        self.only_comment_line_starts
371            .union_len(&self.code_comment_line_starts) as u64
372    }
373
374    /// The `Cloc` metric minimum value. See `min_or_zero` for the
375    /// `usize::MAX` sentinel guard.
376    #[inline]
377    #[must_use]
378    pub fn cloc_min(&self) -> u64 {
379        min_or_zero(self.cloc_min)
380    }
381
382    /// The `Cloc` metric maximum value.
383    #[inline]
384    #[must_use]
385    pub fn cloc_max(&self) -> u64 {
386        self.cloc_max as u64
387    }
388
389    /// Folds `other` into `self`, summing comment counts and updating min/max.
390    #[inline]
391    pub fn merge(&mut self, other: &Cloc) {
392        // Union both per-line sets so a comment line shared across
393        // merged spaces is counted once (mirrors `Ploc`'s line union).
394        self.only_comment_line_starts
395            .union_with(&other.only_comment_line_starts);
396        self.code_comment_line_starts
397            .union_with(&other.code_comment_line_starts);
398
399        // Fold the child's own min/max so nested spaces propagate (#437).
400        self.cloc_min = self.cloc_min.min(other.cloc_min);
401        self.cloc_max = self.cloc_max.max(other.cloc_max);
402    }
403
404    #[inline]
405    pub(crate) fn compute_minmax(&mut self) {
406        // Fold this space's own value unconditionally so containers
407        // participate, matching the sibling metrics' convention (#437).
408        // Bound once: `cloc()` is a `union_len` over both word arrays
409        // since #1109, so calling it twice is six array scans per space.
410        let cloc = self.cloc() as usize;
411        self.cloc_min = self.cloc_min.min(cloc);
412        self.cloc_max = self.cloc_max.max(cloc);
413    }
414}
415
416/// The `LLoc` metric suite.
417#[derive(Debug, Clone, PartialEq)]
418pub struct Lloc {
419    logical_lines: usize,
420    lloc_min: usize,
421    lloc_max: usize,
422}
423
424impl Default for Lloc {
425    fn default() -> Self {
426        Self {
427            logical_lines: 0,
428            lloc_min: usize::MAX,
429            lloc_max: 0,
430        }
431    }
432}
433
434impl Lloc {
435    /// The `LLOC` metric value for this space (logical statements).
436    #[inline]
437    #[must_use]
438    pub fn lloc(&self) -> u64 {
439        // This metric counts the number of statements in a code
440        // https://en.wikipedia.org/wiki/Source_lines_of_code
441        self.logical_lines as u64
442    }
443
444    /// The `Lloc` metric minimum value. See `min_or_zero` for the
445    /// `usize::MAX` sentinel guard.
446    #[inline]
447    #[must_use]
448    pub fn lloc_min(&self) -> u64 {
449        min_or_zero(self.lloc_min)
450    }
451
452    /// The `Lloc` metric maximum value.
453    #[inline]
454    #[must_use]
455    pub fn lloc_max(&self) -> u64 {
456        self.lloc_max as u64
457    }
458
459    /// Records one logical statement.
460    ///
461    /// Exists so the 23 per-language `Loc` impls name the operation
462    /// instead of each reaching into a private field, which also keeps
463    /// the module's `arithmetic_side_effects` carve-out to this one
464    /// line rather than 36 of them (#1152). Saturating is unreachable —
465    /// the count is bounded by the AST's node count — and is the right
466    /// answer if it ever were: a pinned `usize::MAX` is a visibly broken
467    /// LLOC, where a wrap to 0 reads as a legitimately empty space.
468    #[inline]
469    fn count_logical_line(&mut self) {
470        self.logical_lines = self.logical_lines.saturating_add(1);
471    }
472
473    /// Folds `other` into `self`, summing statement counts and updating min/max.
474    #[inline]
475    pub fn merge(&mut self, other: &Lloc) {
476        // Merge lloc lines
477        self.logical_lines = self.logical_lines.saturating_add(other.logical_lines);
478        // Fold the child's own min/max so nested spaces propagate (#437).
479        self.lloc_min = self.lloc_min.min(other.lloc_min);
480        self.lloc_max = self.lloc_max.max(other.lloc_max);
481    }
482
483    #[inline]
484    pub(crate) fn compute_minmax(&mut self) {
485        // Fold this space's own value unconditionally so containers
486        // participate, matching the sibling metrics' convention (#437).
487        self.lloc_min = self.lloc_min.min(self.lloc() as usize);
488        self.lloc_max = self.lloc_max.max(self.lloc() as usize);
489    }
490}
491
492/// The `Loc` metric suite.
493#[derive(Debug, Clone, PartialEq)]
494#[non_exhaustive]
495pub struct Stats {
496    sloc: Sloc,
497    ploc: Ploc,
498    cloc: Cloc,
499    lloc: Lloc,
500    space_count: usize,
501    blank_min: usize,
502    blank_max: usize,
503}
504
505impl Default for Stats {
506    fn default() -> Self {
507        Self {
508            sloc: Sloc::default(),
509            ploc: Ploc::default(),
510            cloc: Cloc::default(),
511            lloc: Lloc::default(),
512            space_count: 1,
513            blank_min: usize::MAX,
514            blank_max: 0,
515        }
516    }
517}
518
519impl fmt::Display for Stats {
520    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
521        write!(
522            f,
523            "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: {}",
524            self.sloc(),
525            self.ploc(),
526            self.lloc(),
527            self.cloc(),
528            self.blank(),
529            self.sloc_average(),
530            self.ploc_average(),
531            self.lloc_average(),
532            self.cloc_average(),
533            self.blank_average(),
534            self.sloc_min(),
535            self.sloc_max(),
536            self.cloc_min(),
537            self.cloc_max(),
538            self.ploc_min(),
539            self.ploc_max(),
540            self.lloc_min(),
541            self.lloc_max(),
542            self.blank_min(),
543            self.blank_max(),
544        )
545    }
546}
547
548impl Stats {
549    /// Test-only constructor that forces a degenerate `cloc`/`sloc`
550    /// pair (here `cloc > sloc`) so callers in other metric modules can
551    /// exercise downstream clamps without depending on the parsing
552    /// pipeline. `cloc <= sloc` always holds for parsed input after
553    /// issue #461, so this state is unreachable through normal use.
554    #[cfg(test)]
555    pub(crate) fn with_cloc_sloc(code_comment_lines: usize, sloc_end_row: usize) -> Self {
556        let mut stats = Stats::default();
557        stats.sloc.start = 0;
558        // `end_row + 1`: the synthetic span models a real one ending
559        // mid-line, so the final row counts and `sloc == sloc_end_row + 1`.
560        stats.sloc.end_line = sloc_end_row.saturating_add(1);
561        // Inject `code_comment_lines` distinct synthetic code-comment
562        // rows. An offset past `sloc_end_row` keeps them disjoint from
563        // any real span row, so `cloc()` (the set's cardinality) equals
564        // the requested count without colliding with sloc attribution.
565        if code_comment_lines > 0 {
566            let synthetic_base = sloc_end_row.saturating_add(1);
567            // Explicit rather than leaning on `insert_range`'s inverted-span
568            // guard: that guard exists to survive a bug, not to serve as a
569            // caller's empty case. The `- 1` is exact under the `> 0` test
570            // above, which is also what makes the inclusive end well-formed.
571            let synthetic_end = synthetic_base
572                .saturating_add(code_comment_lines)
573                .saturating_sub(1);
574            stats
575                .cloc
576                .code_comment_line_starts
577                .insert_range(synthetic_base, synthetic_end);
578        }
579        stats
580    }
581
582    /// Merges a second `Loc` metric suite into the first one
583    pub fn merge(&mut self, other: &Stats) {
584        self.sloc.merge(&other.sloc);
585        self.ploc.merge(&other.ploc);
586        self.cloc.merge(&other.cloc);
587        self.lloc.merge(&other.lloc);
588
589        // Count spaces
590        self.space_count = self.space_count.saturating_add(other.space_count);
591
592        // Fold the child's own min/max so nested spaces propagate (#437).
593        self.blank_min = self.blank_min.min(other.blank_min);
594        self.blank_max = self.blank_max.max(other.blank_max);
595    }
596
597    /// Records an `exclude_tests`-pruned subtree's span so this space's
598    /// `sloc()` excludes those physical lines, matching the
599    /// node-accumulated loc sub-metrics that the pruning already drops
600    /// (issue #722). Called from the walker for the space enclosing each
601    /// skipped subtree.
602    #[inline]
603    pub(crate) fn exclude_test_span(&mut self, start_row: usize, end_line: usize) {
604        self.sloc.exclude_span(start_row, end_line);
605    }
606
607    /// The `Sloc` metric.
608    ///
609    /// Counts the number of lines in a scope
610    #[inline]
611    #[must_use]
612    pub fn sloc(&self) -> u64 {
613        self.sloc.sloc()
614    }
615
616    /// The `Ploc` metric.
617    ///
618    /// Counts the number of instruction lines in a scope
619    #[inline]
620    #[must_use]
621    pub fn ploc(&self) -> u64 {
622        self.ploc.ploc()
623    }
624
625    /// The `Lloc` metric.
626    ///
627    /// Counts the number of statements in a scope
628    #[inline]
629    #[must_use]
630    pub fn lloc(&self) -> u64 {
631        self.lloc.lloc()
632    }
633
634    /// The `Cloc` metric.
635    ///
636    /// Counts the number of comments in a scope
637    #[inline]
638    #[must_use]
639    pub fn cloc(&self) -> u64 {
640        self.cloc.cloc()
641    }
642
643    /// The `Blank` metric.
644    ///
645    /// Counts the number of blank lines in a scope
646    #[inline]
647    #[must_use]
648    pub fn blank(&self) -> u64 {
649        // `sloc - ploc - only_comment_lines` can go negative when a space's
650        // physical and comment line attribution overlaps the span row count
651        // (e.g. single-line bodies). `saturating_sub` clamps at 0 (matching
652        // the prior `.max(0.0)` on the f64 form) so the serialized value is
653        // never negative (#437).
654        self.sloc()
655            .saturating_sub(self.ploc())
656            .saturating_sub(self.cloc.only_comment_line_starts.len() as u64)
657    }
658
659    /// The `Sloc` metric average value.
660    ///
661    /// This value is computed dividing the `Sloc` value for the number of spaces
662    #[inline]
663    #[must_use]
664    pub fn sloc_average(&self) -> f64 {
665        crate::metrics::average(self.sloc() as f64, self.space_count)
666    }
667
668    /// The `Ploc` metric average value.
669    ///
670    /// This value is computed dividing the `Ploc` value for the number of spaces
671    #[inline]
672    #[must_use]
673    pub fn ploc_average(&self) -> f64 {
674        crate::metrics::average(self.ploc() as f64, self.space_count)
675    }
676
677    /// The `Lloc` metric average value.
678    ///
679    /// This value is computed dividing the `Lloc` value for the number of spaces
680    #[inline]
681    #[must_use]
682    pub fn lloc_average(&self) -> f64 {
683        crate::metrics::average(self.lloc() as f64, self.space_count)
684    }
685
686    /// The `Cloc` metric average value.
687    ///
688    /// This value is computed dividing the `Cloc` value for the number of spaces
689    #[inline]
690    #[must_use]
691    pub fn cloc_average(&self) -> f64 {
692        crate::metrics::average(self.cloc() as f64, self.space_count)
693    }
694
695    /// The `Blank` metric average value.
696    ///
697    /// This value is computed dividing the `Blank` value for the number of spaces
698    #[inline]
699    #[must_use]
700    pub fn blank_average(&self) -> f64 {
701        crate::metrics::average(self.blank() as f64, self.space_count)
702    }
703
704    /// The `Sloc` metric minimum value.
705    #[inline]
706    #[must_use]
707    pub fn sloc_min(&self) -> u64 {
708        self.sloc.sloc_min()
709    }
710
711    /// The `Sloc` metric maximum value.
712    #[inline]
713    #[must_use]
714    pub fn sloc_max(&self) -> u64 {
715        self.sloc.sloc_max()
716    }
717
718    /// The `Cloc` metric minimum value.
719    #[inline]
720    #[must_use]
721    pub fn cloc_min(&self) -> u64 {
722        self.cloc.cloc_min()
723    }
724
725    /// The `Cloc` metric maximum value.
726    #[inline]
727    #[must_use]
728    pub fn cloc_max(&self) -> u64 {
729        self.cloc.cloc_max()
730    }
731
732    /// The `Ploc` metric minimum value.
733    #[inline]
734    #[must_use]
735    pub fn ploc_min(&self) -> u64 {
736        self.ploc.ploc_min()
737    }
738
739    /// The `Ploc` metric maximum value.
740    #[inline]
741    #[must_use]
742    pub fn ploc_max(&self) -> u64 {
743        self.ploc.ploc_max()
744    }
745
746    /// The `Lloc` metric minimum value.
747    #[inline]
748    #[must_use]
749    pub fn lloc_min(&self) -> u64 {
750        self.lloc.lloc_min()
751    }
752
753    /// The `Lloc` metric maximum value.
754    #[inline]
755    #[must_use]
756    pub fn lloc_max(&self) -> u64 {
757        self.lloc.lloc_max()
758    }
759
760    /// The `Blank` metric minimum value. See `min_or_zero` for the
761    /// `usize::MAX` sentinel guard.
762    #[inline]
763    #[must_use]
764    pub fn blank_min(&self) -> u64 {
765        min_or_zero(self.blank_min)
766    }
767
768    /// The `Blank` metric maximum value.
769    #[inline]
770    #[must_use]
771    pub fn blank_max(&self) -> u64 {
772        self.blank_max as u64
773    }
774
775    #[inline]
776    pub(crate) fn compute_minmax(&mut self) {
777        self.sloc.compute_minmax();
778        self.ploc.compute_minmax();
779        self.cloc.compute_minmax();
780        self.lloc.compute_minmax();
781
782        // Fold this space's own blank value unconditionally so containers
783        // participate, matching the sibling metrics' convention (#437).
784        // `blank()` returns a `u64` already clamped at 0 by `saturating_sub`,
785        // so the widening `as usize` cast is lossless (64-bit) and cannot
786        // introduce a spurious value here. Bound once: `blank()` popcounts
787        // two word arrays since #1109.
788        let blank = self.blank() as usize;
789        self.blank_min = self.blank_min.min(blank);
790        self.blank_max = self.blank_max.max(blank);
791    }
792
793    pub(crate) fn init_unit_span(&mut self, start: usize, end_line: usize) {
794        self.sloc.start = start;
795        self.sloc.end_line = end_line;
796    }
797
798    /// Drops every recorded PLOC / CLOC row that lies outside this
799    /// space's own `sloc` row span.
800    ///
801    /// That bounds `ploc` and `cloc` by the span, which is what the
802    /// `debug_assert!` below states. It is *not* the same as pinning
803    /// the public `ploc() <= sloc()`: `sloc()` subtracts
804    /// `exclude_tests`-pruned rows, and can still come out below `ploc`
805    /// — see the assertion's comment and #1417.
806    ///
807    /// A grammar's error recovery synthesises **zero-width tokens one
808    /// row past the last row the file has**: an unterminated Bash
809    /// heredoc gets a `heredoc_end` at `(3, 1)..(3, 1)` in a two-row
810    /// file, an unterminated Elixir / Lua / Groovy string gets its
811    /// closing quote the same way. Those tokens are childless, so they
812    /// reach the leaf branch of a language's catch-all arm, which
813    /// inserts a raw `start` row — and here that `start` *is* the
814    /// phantom row. [`Node::end_line`] already encodes "a node whose
815    /// end column is 0 does not occupy the row it ends on"; nothing
816    /// encoded the same rule for a node that *begins* past the span, nor
817    /// applied it to the raw end row `init` hands the comment arms — so a
818    /// comment ending at column 0 of the row past the last one (Perl POD
819    /// running to end-of-file without `=cut`) counted that row as a
820    /// comment. The clamp removes both.
821    ///
822    /// `ploc > sloc` is a contract violation rather than a rounding
823    /// artifact: [`Stats::blank`] saturates at 0, so the clamp there
824    /// hides it while a consumer computing `ploc / sloc` gets a ratio
825    /// above 1. Ten of the twenty-odd languages reproduced it on
826    /// truncated input before #1398 — a per-arm skip would have been
827    /// ten edits that still could not cover a grammar whose recovery
828    /// shape nobody sampled, so the rule lives once, here, keyed on the
829    /// span every language already reports.
830    ///
831    /// **Input whose tree stays inside the file's rows is untouched, and
832    /// a clean parse does not guarantee that**: Perl POD at end of file
833    /// and Ruby's unterminated `<<~` heredoc both parse without an error
834    /// node and still move. Over the `pdf.js`, `DeepSpeech` and
835    /// `serde` corpora exactly one of 1,876 files moves, and it is a
836    /// real bug fixed rather than a real row lost:
837    /// `DeepSpeech/parse_valgrind_suppressions.sh` leaves a MISSING `}`
838    /// at `(58, 1)` of a 57-row file, so its `ploc` drops 36 → 35 and
839    /// `blank` rises 6 → 7. No snapshot covers it — `snapshots/` holds
840    /// only the C-family files — so the integration suite is silent
841    /// about the one case that proves the fix reaches ordinary trees.
842    /// Do not read a green snapshot run as evidence of no effect.
843    ///
844    /// The clamp's one latent hazard is the mirror image: a unit whose
845    /// span is *shorter* than the file. That happens today for a file
846    /// ending in a blank row (`"package m\n\n"` reports `unit 1..1`),
847    /// and is harmless only because a trailing blank row reaches
848    /// neither line set. Nothing pins `unit.end_line >= line_count` —
849    /// `tests/parity/space_span_containment.rs` asserts
850    /// `unit == (1, line_count)`, but none of its fixtures ends in a
851    /// blank row — so a grammar that ever credited such a row to PLOC
852    /// would have it deleted here silently.
853    pub(crate) fn clamp_line_sets_to_span(&mut self) {
854        let start = self.sloc.start;
855        let span = span_rows(start, self.sloc.end_line);
856        // `end_line` is the 1-based inclusive last row of the span, so
857        // the last 0-based row it covers is `end_line - 1` — exact
858        // wherever the span is non-empty, which is what `span == 0`
859        // selects against. `(1, 0)` is the inverted range
860        // `LineSet::retain_range` reads as "keep nothing", and it is
861        // what the empty file's `0..0` needs: retaining `0..=0` there
862        // would keep a row the file does not have.
863        //
864        // That last branch is defensive rather than live. The three
865        // walks that reach `span == 0` (an empty file, and the two
866        // whitespace-only root contracts) all arrive with both line
867        // sets already empty, so `(0, 0)` here is byte-identical over
868        // every corpus input — measured. `a_zero_span_keeps_no_row`
869        // below is therefore a direct `Stats` test rather than a
870        // fixture, per `.claude/rules/testing.md`: it is the only shape
871        // that can tell the two spellings apart.
872        //
873        // The corpus measurement is the weaker half of that claim, and
874        // `line_set`'s module header retracts one of exactly this shape
875        // as having been wrong *and* load-bearing (#1398). The stronger
876        // half is structural: `span == 0` needs a space opened on a
877        // genuinely zero-width node at column 0, and every language's
878        // `is_func_space` matches compound productions — never a raw
879        // token, never `is_error` / `is_missing` — while a MISSING node
880        // is always a single terminal.
881        //
882        // Either way the absence is not what should hold the branch,
883        // because `retain_range(1, 0)` *clears* both line sets: whatever
884        // arrived is gone, and the parent never recovers it, since a
885        // child is clamped before `Ploc::merge` lifts it. That clear is
886        // also what makes the two assertions at the end of this function
887        // vacuous here — they compare against `span`, so both read
888        // `0 <= 0` no matter what was destroyed. Assert before the
889        // clear, where the question can still be answered.
890        let (first, last) = if span == 0 {
891            debug_assert!(
892                self.ploc() == 0 && self.cloc() == 0,
893                "zero-span space reached the clamp holding {} ploc / {} cloc \
894                 row(s); clearing them here would lose them silently",
895                self.ploc(),
896                self.cloc()
897            );
898            (1, 0)
899        } else {
900            (start, self.sloc.end_line.saturating_sub(1))
901        };
902        self.ploc.lines.retain_range(first, last);
903        self.cloc.only_comment_line_starts.retain_range(first, last);
904        self.cloc.code_comment_line_starts.retain_range(first, last);
905        // The fourth set is the `exclude_tests`-pruned rows, and it is
906        // clamped for the same reason as the other three rather than
907        // because anything is known to put a row outside the span
908        // there: a pruned node lies inside the space that recorded it,
909        // so this is expected to be a no-op. What it buys is the
910        // premise `X ⊆ span` that makes `settle_excluded_rows`'s
911        // `ploc() <= sloc()` a theorem rather than an observation —
912        // without it a stray row would turn `sloc()` negative-by-
913        // saturation and fire an assertion in the wrong pass (#1417).
914        self.sloc.excluded_rows.retain_range(first, last);
915
916        // The invariant the clamp establishes, asserted on the path
917        // every walk takes for every space rather than only on the
918        // fixtures the regression tests name — 821 of the workspace's
919        // tests reach it.
920        //
921        // Against the span, deliberately, and kept that way now that
922        // [`Stats::settle_excluded_rows`] asserts the stronger
923        // `ploc() <= sloc()` form (#1417). The two say different
924        // things: this pair localises a failure to the clamp itself,
925        // where `sloc()`'s subtrahend is not yet settled and could not
926        // be blamed.
927        //
928        // `ploc()` and `cloc()` popcount their word arrays (#1109):
929        // O(words) per space, the same order as the `compute_minmax` that
930        // follows, where per node it would be the quadratic shape #1122
931        // removed. The message repeats the call, but an assertion's
932        // message arguments are evaluated only on the failing branch.
933        debug_assert!(
934            self.ploc() <= span as u64,
935            "ploc {} exceeds the {span} row span it was clamped to",
936            self.ploc()
937        );
938        debug_assert!(
939            self.cloc() <= span as u64,
940            "cloc {} exceeds the {span} row span it was clamped to",
941            self.cloc()
942        );
943    }
944
945    /// Removes from the `exclude_tests`-pruned row set every row that
946    /// retained code or comments also occupy, so `sloc()` subtracts only
947    /// the rows the prune genuinely took (#1417).
948    ///
949    /// `Sloc::exclude_span` records a pruned subtree's whole span, and
950    /// its first or last physical row can carry a retained sibling —
951    /// `fn a() {} #[cfg(test)] mod t { … }` is one row that stays. The
952    /// walk cannot know that when it prunes, because the retained rows
953    /// of the enclosing space are still arriving; this pass runs once
954    /// per space at finalization, when they are all in.
955    ///
956    /// All three retained sets are subtracted, but only two of them can
957    /// move the answer, and that is worth stating rather than hedging:
958    /// `code_comment_line_starts` is a subset of `ploc.lines` by
959    /// construction. Its only writers are the two `add_cloc_lines` arms,
960    /// both gated on `ploc.lines.contains(start)` already being true,
961    /// and `check_comment_ends_on_code_line`, whose per-language call
962    /// sites every one insert that same row into `ploc.lines` on the
963    /// next statement. `Stats::merge` unions both sets and
964    /// [`Stats::clamp_line_sets_to_span`] retains both over one range,
965    /// so no later pass can separate them. Deleting the third
966    /// subtraction therefore fails no test and *cannot* — measured at 0
967    /// of 3,390 lib tests, where dropping either of the other two fails
968    /// 3 and 1. It stays as defence against a future call site that
969    /// forgets the PLOC insert, and the `debug_assert_eq!` opening the
970    /// body is what keeps that defence honest: without it the line is
971    /// unreachable weight whose loss nothing would ever notice.
972    ///
973    /// Ordered after [`Stats::clamp_line_sets_to_span`], which
974    /// establishes the premise: with `P`, `O`, `C` and the pruned set
975    /// `X` all inside the span `S`, the residual `X' = X \ (P ∪ O ∪ C)`
976    /// is disjoint from `P ∪ O ∪ C`, so `|X'| + |P ∪ O ∪ C| <= |S|` and
977    /// `sloc() = |S| - |X'| >= max(ploc(), cloc())`. That is what the
978    /// assertions below pin — on every space of every walk, so they are
979    /// the real evidence the fix holds across all twenty languages
980    /// rather than only on the fixtures the regression tests name.
981    ///
982    /// [`Stats::blank`] keeps its `saturating_sub`: `P` and `O ∪ C` may
983    /// overlap on a code-and-comment row, so no lower bound on
984    /// `sloc - ploc - |O|` follows from the above.
985    pub(crate) fn settle_excluded_rows(&mut self) {
986        // The premise that makes the third subtraction below redundant,
987        // pinned on every space of every walk because it is a property
988        // of all the `check_comment_ends_on_code_line` call sites at
989        // once and no single fixture can reach them.
990        debug_assert_eq!(
991            self.ploc
992                .lines
993                .union_len(&self.cloc.code_comment_line_starts),
994            self.ploc.lines.len(),
995            "a row carrying both code and a comment must also be a code row"
996        );
997
998        // Disjoint field paths, so the shorthand borrows cleanly.
999        let excluded = &mut self.sloc.excluded_rows;
1000        excluded.subtract(&self.ploc.lines);
1001        excluded.subtract(&self.cloc.only_comment_line_starts);
1002        excluded.subtract(&self.cloc.code_comment_line_starts);
1003
1004        debug_assert!(
1005            self.ploc() <= self.sloc(),
1006            "ploc {} exceeds sloc {} after settling",
1007            self.ploc(),
1008            self.sloc()
1009        );
1010        debug_assert!(
1011            self.cloc() <= self.sloc(),
1012            "cloc {} exceeds sloc {} after settling",
1013            self.cloc(),
1014            self.sloc()
1015        );
1016    }
1017}
1018
1019#[doc(hidden)]
1020/// Per-language computation of the lines-of-code metrics.
1021pub(crate) trait Loc
1022where
1023    Self: Checker,
1024{
1025    /// Walk `node` and update `stats` with this metric for the language
1026    /// implementing the trait.
1027    /// `ancestors` is the chain the walker descended through: the
1028    /// C-family and JVM-family arms read it to tell a declaration in a
1029    /// loop header from one in the loop body (#1084).
1030    fn compute(node: &Node, ancestors: Ancestors<'_, '_>, stats: &mut Stats, is_func_space: bool);
1031}
1032
1033mod shared;
1034pub(crate) use shared::*;
1035
1036// Real defaults — Loc counts on these "languages" would conflate
1037// comments / preproc directives with executable code; treating them
1038// as 0 is the documented behaviour. Audited in #188.
1039implement_metric_trait!(Loc, PreprocCode, CcommentCode);
1040
1041mod bash;
1042mod c;
1043mod cpp;
1044mod csharp;
1045mod elixir;
1046mod go;
1047mod groovy;
1048mod irules;
1049mod java;
1050mod javascript;
1051mod kotlin;
1052mod lua;
1053mod mozcpp;
1054mod mozjs;
1055mod objc;
1056mod perl;
1057mod php;
1058mod python;
1059mod ruby;
1060mod rust;
1061mod tcl;
1062mod tsx;
1063mod typescript;
1064
1065#[cfg(test)]
1066#[allow(
1067    clippy::float_cmp,
1068    clippy::cast_precision_loss,
1069    clippy::cast_possible_truncation,
1070    clippy::cast_sign_loss,
1071    clippy::similar_names,
1072    clippy::doc_markdown,
1073    clippy::needless_raw_string_hashes,
1074    clippy::too_many_lines
1075)]
1076mod tests {
1077    use crate::test_support::{check_metrics_only_shim, metrics_verbatim, space_verbatim};
1078
1079    use super::*;
1080
1081    check_metrics_only_shim!(check_metrics, Loc);
1082
1083    /// A `Stats::default()` that never sees an observation must not leak
1084    /// the `usize::MAX` sentinel for any of the LOC `_min` accumulators
1085    /// (`sloc_min`, `ploc_min`, `lloc_min`, `cloc_min`, `blank_min`).
1086    /// The getters collapse the sentinel to `0.0` so JSON never emits
1087    /// `1.8446744e19`.
1088    #[test]
1089    fn loc_empty_file_min_is_zero() {
1090        let stats = Stats::default();
1091        assert_eq!(stats.sloc_min(), 0);
1092        assert_eq!(stats.ploc_min(), 0);
1093        assert_eq!(stats.lloc_min(), 0);
1094        assert_eq!(stats.cloc_min(), 0);
1095        assert_eq!(stats.blank_min(), 0);
1096    }
1097
1098    /// Parses `source` with `PerlParser` and asserts the resulting tree has
1099    /// no `ERROR` nodes. Use alongside metric assertions whose expected
1100    /// values would happen to match what an error tree produces — a parse
1101    /// regression in tree-sitter-perl could otherwise leave such tests
1102    /// silently green.
1103    #[cfg(feature = "perl")]
1104    #[cfg(test)]
1105    fn assert_perl_parses_cleanly(source: &str) {
1106        use crate::traits::ParserTrait;
1107        // Mirror the trailing-newline normalisation `check_func_space` does
1108        // before handing input to the parser, so this helper sees the same
1109        // bytes the metric tests do.
1110        let path = std::path::PathBuf::from("foo.pl");
1111        let mut bytes = source.trim_end_matches('\n').as_bytes().to_vec();
1112        bytes.push(b'\n');
1113        let parser = PerlParser::new(bytes, &path, None);
1114        assert!(
1115            !parser.root().has_error(),
1116            "tree-sitter-perl returned an error tree for snippet:\n{source}"
1117        );
1118    }
1119
1120    #[cfg(feature = "python")]
1121    #[test]
1122    fn python_sloc() {
1123        check_metrics::<PythonParser>(
1124            "
1125
1126            a = 42
1127
1128            ",
1129            "foo.py",
1130            |metric| {
1131                // Spaces: 1
1132                insta::assert_json_snapshot!(
1133                    metric.loc,
1134                    @r#"
1135                {
1136                  "sloc": 1,
1137                  "ploc": 1,
1138                  "lloc": 1,
1139                  "cloc": 0,
1140                  "blank": 0,
1141                  "sloc_average": 1.0,
1142                  "ploc_average": 1.0,
1143                  "lloc_average": 1.0,
1144                  "cloc_average": 0.0,
1145                  "blank_average": 0.0,
1146                  "sloc_min": 1,
1147                  "sloc_max": 1,
1148                  "cloc_min": 0,
1149                  "cloc_max": 0,
1150                  "ploc_min": 1,
1151                  "ploc_max": 1,
1152                  "lloc_min": 1,
1153                  "lloc_max": 1,
1154                  "blank_min": 0,
1155                  "blank_max": 0
1156                }
1157                "#
1158                );
1159            },
1160        );
1161    }
1162
1163    #[cfg(feature = "python")]
1164    #[test]
1165    fn python_blank() {
1166        check_metrics::<PythonParser>(
1167            "
1168            a = 42
1169
1170            b = 43
1171
1172            ",
1173            "foo.py",
1174            |metric| {
1175                // Spaces: 1
1176                insta::assert_json_snapshot!(
1177                    metric.loc,
1178                    @r#"
1179                {
1180                  "sloc": 3,
1181                  "ploc": 2,
1182                  "lloc": 2,
1183                  "cloc": 0,
1184                  "blank": 1,
1185                  "sloc_average": 3.0,
1186                  "ploc_average": 2.0,
1187                  "lloc_average": 2.0,
1188                  "cloc_average": 0.0,
1189                  "blank_average": 1.0,
1190                  "sloc_min": 3,
1191                  "sloc_max": 3,
1192                  "cloc_min": 0,
1193                  "cloc_max": 0,
1194                  "ploc_min": 2,
1195                  "ploc_max": 2,
1196                  "lloc_min": 2,
1197                  "lloc_max": 2,
1198                  "blank_min": 1,
1199                  "blank_max": 1
1200                }
1201                "#
1202                );
1203            },
1204        );
1205    }
1206
1207    #[cfg(feature = "rust")]
1208    #[test]
1209    fn rust_blank() {
1210        check_metrics::<RustParser>(
1211            "
1212
1213            let a = 42;
1214
1215            let b = 43;
1216
1217            ",
1218            "foo.rs",
1219            |metric| {
1220                // Spaces: 1
1221                insta::assert_json_snapshot!(
1222                    metric.loc,
1223                    @r#"
1224                {
1225                  "sloc": 3,
1226                  "ploc": 2,
1227                  "lloc": 2,
1228                  "cloc": 0,
1229                  "blank": 1,
1230                  "sloc_average": 3.0,
1231                  "ploc_average": 2.0,
1232                  "lloc_average": 2.0,
1233                  "cloc_average": 0.0,
1234                  "blank_average": 1.0,
1235                  "sloc_min": 3,
1236                  "sloc_max": 3,
1237                  "cloc_min": 0,
1238                  "cloc_max": 0,
1239                  "ploc_min": 2,
1240                  "ploc_max": 2,
1241                  "lloc_min": 2,
1242                  "lloc_max": 2,
1243                  "blank_min": 1,
1244                  "blank_max": 1
1245                }
1246                "#
1247                );
1248            },
1249        );
1250
1251        check_metrics::<RustParser>("fn func() { /* comment */ }", "foo.rs", |metric| {
1252            // Spaces: 2
1253            insta::assert_json_snapshot!(
1254                metric.loc,
1255                @r#"
1256            {
1257              "sloc": 1,
1258              "ploc": 1,
1259              "lloc": 0,
1260              "cloc": 1,
1261              "blank": 0,
1262              "sloc_average": 0.5,
1263              "ploc_average": 0.5,
1264              "lloc_average": 0.0,
1265              "cloc_average": 0.5,
1266              "blank_average": 0.0,
1267              "sloc_min": 1,
1268              "sloc_max": 1,
1269              "cloc_min": 1,
1270              "cloc_max": 1,
1271              "ploc_min": 1,
1272              "ploc_max": 1,
1273              "lloc_min": 0,
1274              "lloc_max": 0,
1275              "blank_min": 0,
1276              "blank_max": 0
1277            }
1278            "#
1279            );
1280        });
1281    }
1282
1283    #[cfg(feature = "c")]
1284    #[test]
1285    fn c_blank() {
1286        check_metrics::<CParser>(
1287            "
1288
1289            int a = 42;
1290
1291            int b = 43;
1292
1293            ",
1294            "foo.c",
1295            |metric| {
1296                // Spaces: 1
1297                insta::assert_json_snapshot!(
1298                    metric.loc,
1299                    @r#"
1300                {
1301                  "sloc": 3,
1302                  "ploc": 2,
1303                  "lloc": 2,
1304                  "cloc": 0,
1305                  "blank": 1,
1306                  "sloc_average": 3.0,
1307                  "ploc_average": 2.0,
1308                  "lloc_average": 2.0,
1309                  "cloc_average": 0.0,
1310                  "blank_average": 1.0,
1311                  "sloc_min": 3,
1312                  "sloc_max": 3,
1313                  "cloc_min": 0,
1314                  "cloc_max": 0,
1315                  "ploc_min": 2,
1316                  "ploc_max": 2,
1317                  "lloc_min": 2,
1318                  "lloc_max": 2,
1319                  "blank_min": 1,
1320                  "blank_max": 1
1321                }
1322                "#
1323                );
1324            },
1325        );
1326    }
1327
1328    #[cfg(feature = "python")]
1329    #[test]
1330    fn python_no_zero_blank() {
1331        // Checks that the blank metric is not equal to 0 when there are some
1332        // comments next to code lines.
1333        check_metrics::<PythonParser>(
1334            "def ConnectToUpdateServer():
1335                 pool = 4
1336
1337                 updateServer = -42
1338                 isConnected = False
1339                 currTry = 0
1340                 numRetries = 10 # Number of IPC connection retries before
1341                                 # giving up.
1342                 numTries = 20 # Number of IPC connection tries before
1343                               # giving up.",
1344            "foo.py",
1345            |metric| {
1346                // Spaces: 2
1347                insta::assert_json_snapshot!(
1348                    metric.loc,
1349                    @r#"
1350                {
1351                  "sloc": 10,
1352                  "ploc": 7,
1353                  "lloc": 6,
1354                  "cloc": 4,
1355                  "blank": 1,
1356                  "sloc_average": 5.0,
1357                  "ploc_average": 3.5,
1358                  "lloc_average": 3.0,
1359                  "cloc_average": 2.0,
1360                  "blank_average": 0.5,
1361                  "sloc_min": 10,
1362                  "sloc_max": 10,
1363                  "cloc_min": 4,
1364                  "cloc_max": 4,
1365                  "ploc_min": 7,
1366                  "ploc_max": 7,
1367                  "lloc_min": 6,
1368                  "lloc_max": 6,
1369                  "blank_min": 1,
1370                  "blank_max": 1
1371                }
1372                "#
1373                );
1374            },
1375        );
1376    }
1377
1378    #[cfg(feature = "python")]
1379    #[test]
1380    fn python_no_blank() {
1381        // Checks that the blank metric is equal to 0 when there are no blank
1382        // lines and there are comments next to code lines.
1383        check_metrics::<PythonParser>(
1384            "def ConnectToUpdateServer():
1385                 pool = 4
1386                 updateServer = -42
1387                 isConnected = False
1388                 currTry = 0
1389                 numRetries = 10 # Number of IPC connection retries before
1390                                 # giving up.
1391                 numTries = 20 # Number of IPC connection tries before
1392                               # giving up.",
1393            "foo.py",
1394            |metric| {
1395                // Spaces: 2
1396                insta::assert_json_snapshot!(
1397                    metric.loc,
1398                    @r#"
1399                {
1400                  "sloc": 9,
1401                  "ploc": 7,
1402                  "lloc": 6,
1403                  "cloc": 4,
1404                  "blank": 0,
1405                  "sloc_average": 4.5,
1406                  "ploc_average": 3.5,
1407                  "lloc_average": 3.0,
1408                  "cloc_average": 2.0,
1409                  "blank_average": 0.0,
1410                  "sloc_min": 9,
1411                  "sloc_max": 9,
1412                  "cloc_min": 4,
1413                  "cloc_max": 4,
1414                  "ploc_min": 7,
1415                  "ploc_max": 7,
1416                  "lloc_min": 6,
1417                  "lloc_max": 6,
1418                  "blank_min": 0,
1419                  "blank_max": 0
1420                }
1421                "#
1422                );
1423            },
1424        );
1425    }
1426
1427    #[cfg(feature = "python")]
1428    #[test]
1429    fn python_no_zero_blank_more_comments() {
1430        // Checks that the blank metric is not equal to 0 when there are more
1431        // comments next to code lines compared to the previous tests.
1432        check_metrics::<PythonParser>(
1433            "def ConnectToUpdateServer():
1434                 pool = 4
1435
1436                 updateServer = -42
1437                 isConnected = False
1438                 currTry = 0 # Set this variable to 0
1439                 numRetries = 10 # Number of IPC connection retries before
1440                                 # giving up.
1441                 numTries = 20 # Number of IPC connection tries before
1442                               # giving up.",
1443            "foo.py",
1444            |metric| {
1445                // Spaces: 2
1446                insta::assert_json_snapshot!(
1447                    metric.loc,
1448                    @r#"
1449                {
1450                  "sloc": 10,
1451                  "ploc": 7,
1452                  "lloc": 6,
1453                  "cloc": 5,
1454                  "blank": 1,
1455                  "sloc_average": 5.0,
1456                  "ploc_average": 3.5,
1457                  "lloc_average": 3.0,
1458                  "cloc_average": 2.5,
1459                  "blank_average": 0.5,
1460                  "sloc_min": 10,
1461                  "sloc_max": 10,
1462                  "cloc_min": 5,
1463                  "cloc_max": 5,
1464                  "ploc_min": 7,
1465                  "ploc_max": 7,
1466                  "lloc_min": 6,
1467                  "lloc_max": 6,
1468                  "blank_min": 1,
1469                  "blank_max": 1
1470                }
1471                "#
1472                );
1473            },
1474        );
1475    }
1476
1477    #[cfg(feature = "rust")]
1478    #[test]
1479    fn rust_no_zero_blank() {
1480        // Checks that the blank metric is not equal to 0 when there are some
1481        // comments next to code lines.
1482        check_metrics::<RustParser>(
1483            "fn ConnectToUpdateServer() {
1484              let pool = 0;
1485
1486              let updateServer = -42;
1487              let isConnected = false;
1488              let currTry = 0;
1489              let numRetries = 10;  // Number of IPC connection retries before
1490                                    // giving up.
1491              let numTries = 20;    // Number of IPC connection tries before
1492                                    // giving up.
1493            }",
1494            "foo.rs",
1495            |metric| {
1496                // Spaces: 2
1497                insta::assert_json_snapshot!(
1498                    metric.loc,
1499                    @r#"
1500                {
1501                  "sloc": 11,
1502                  "ploc": 8,
1503                  "lloc": 6,
1504                  "cloc": 4,
1505                  "blank": 1,
1506                  "sloc_average": 5.5,
1507                  "ploc_average": 4.0,
1508                  "lloc_average": 3.0,
1509                  "cloc_average": 2.0,
1510                  "blank_average": 0.5,
1511                  "sloc_min": 11,
1512                  "sloc_max": 11,
1513                  "cloc_min": 4,
1514                  "cloc_max": 4,
1515                  "ploc_min": 8,
1516                  "ploc_max": 8,
1517                  "lloc_min": 6,
1518                  "lloc_max": 6,
1519                  "blank_min": 1,
1520                  "blank_max": 1
1521                }
1522                "#
1523                );
1524            },
1525        );
1526    }
1527
1528    #[cfg(feature = "javascript")]
1529    #[test]
1530    fn javascript_no_zero_blank() {
1531        // Checks that the blank metric is not equal to 0 when there are some
1532        // comments next to code lines.
1533        check_metrics::<JavascriptParser>(
1534            "function ConnectToUpdateServer() {
1535              var pool = 0;
1536
1537              var updateServer = -42;
1538              var isConnected = false;
1539              var currTry = 0;
1540              var numRetries = 10;  // Number of IPC connection retries before
1541                                    // giving up.
1542              var numTries = 20;    // Number of IPC connection tries before
1543                                    // giving up.
1544            }",
1545            "foo.js",
1546            |metric| {
1547                // Spaces: 2
1548                insta::assert_json_snapshot!(
1549                    metric.loc,
1550                    @r#"
1551                {
1552                  "sloc": 11,
1553                  "ploc": 8,
1554                  "lloc": 6,
1555                  "cloc": 4,
1556                  "blank": 1,
1557                  "sloc_average": 5.5,
1558                  "ploc_average": 4.0,
1559                  "lloc_average": 3.0,
1560                  "cloc_average": 2.0,
1561                  "blank_average": 0.5,
1562                  "sloc_min": 11,
1563                  "sloc_max": 11,
1564                  "cloc_min": 4,
1565                  "cloc_max": 4,
1566                  "ploc_min": 8,
1567                  "ploc_max": 8,
1568                  "lloc_min": 6,
1569                  "lloc_max": 6,
1570                  "blank_min": 1,
1571                  "blank_max": 1
1572                }
1573                "#
1574                );
1575            },
1576        );
1577    }
1578
1579    #[cfg(feature = "cpp")]
1580    #[test]
1581    fn cpp_no_zero_blank() {
1582        // Checks that the blank metric is not equal to 0 when there are some
1583        // comments next to code lines.
1584        check_metrics::<CppParser>(
1585            "void ConnectToUpdateServer() {
1586              int pool;
1587
1588              int updateServer = -42;
1589              bool isConnected = false;
1590              int currTry = 0;
1591              const int numRetries = 10; // Number of IPC connection retries before
1592                                         // giving up.
1593              const int numTries = 20; // Number of IPC connection tries before
1594                                       // giving up.
1595            }",
1596            "foo.cpp",
1597            |metric| {
1598                // Spaces: 2
1599                insta::assert_json_snapshot!(
1600                    metric.loc,
1601                    @r#"
1602                {
1603                  "sloc": 11,
1604                  "ploc": 8,
1605                  "lloc": 6,
1606                  "cloc": 4,
1607                  "blank": 1,
1608                  "sloc_average": 5.5,
1609                  "ploc_average": 4.0,
1610                  "lloc_average": 3.0,
1611                  "cloc_average": 2.0,
1612                  "blank_average": 0.5,
1613                  "sloc_min": 11,
1614                  "sloc_max": 11,
1615                  "cloc_min": 4,
1616                  "cloc_max": 4,
1617                  "ploc_min": 8,
1618                  "ploc_max": 8,
1619                  "lloc_min": 6,
1620                  "lloc_max": 6,
1621                  "blank_min": 1,
1622                  "blank_max": 1
1623                }
1624                "#
1625                );
1626            },
1627        );
1628    }
1629
1630    #[cfg(feature = "cpp")]
1631    #[test]
1632    fn cpp_code_line_start_block_blank() {
1633        // Checks that the blank metric is equal to 1 when there are
1634        // block comments starting next to code lines.
1635        check_metrics::<CppParser>(
1636            "void ConnectToUpdateServer() {
1637              int pool;
1638
1639              int updateServer = -42;
1640              bool isConnected = false;
1641              int currTry = 0;
1642              const int numRetries = 10; /* Number of IPC connection retries
1643              before
1644              giving up. */
1645              const int numTries = 20; // Number of IPC connection tries before
1646                                       // giving up.
1647            }",
1648            "foo.cpp",
1649            |metric| {
1650                // Spaces: 2
1651                insta::assert_json_snapshot!(
1652                    metric.loc,
1653                    @r#"
1654                {
1655                  "sloc": 12,
1656                  "ploc": 8,
1657                  "lloc": 6,
1658                  "cloc": 5,
1659                  "blank": 1,
1660                  "sloc_average": 6.0,
1661                  "ploc_average": 4.0,
1662                  "lloc_average": 3.0,
1663                  "cloc_average": 2.5,
1664                  "blank_average": 0.5,
1665                  "sloc_min": 12,
1666                  "sloc_max": 12,
1667                  "cloc_min": 5,
1668                  "cloc_max": 5,
1669                  "ploc_min": 8,
1670                  "ploc_max": 8,
1671                  "lloc_min": 6,
1672                  "lloc_max": 6,
1673                  "blank_min": 1,
1674                  "blank_max": 1
1675                }
1676                "#
1677                );
1678            },
1679        );
1680    }
1681
1682    #[cfg(feature = "cpp")]
1683    #[test]
1684    fn cpp_block_comment_blank() {
1685        // Checks that the blank metric is equal to 1 when there are
1686        // block comments on independent lines.
1687        check_metrics::<CppParser>(
1688            "void ConnectToUpdateServer() {
1689              int pool;
1690
1691              int updateServer = -42;
1692              bool isConnected = false;
1693              int currTry = 0;
1694              /* Number of IPC connection retries
1695              before
1696              giving up. */
1697              const int numRetries = 10;
1698              const int numTries = 20; // Number of IPC connection tries before
1699                                       // giving up.
1700            }",
1701            "foo.cpp",
1702            |metric| {
1703                // Spaces: 2
1704                insta::assert_json_snapshot!(
1705                    metric.loc,
1706                    @r#"
1707                {
1708                  "sloc": 13,
1709                  "ploc": 8,
1710                  "lloc": 6,
1711                  "cloc": 5,
1712                  "blank": 1,
1713                  "sloc_average": 6.5,
1714                  "ploc_average": 4.0,
1715                  "lloc_average": 3.0,
1716                  "cloc_average": 2.5,
1717                  "blank_average": 0.5,
1718                  "sloc_min": 13,
1719                  "sloc_max": 13,
1720                  "cloc_min": 5,
1721                  "cloc_max": 5,
1722                  "ploc_min": 8,
1723                  "ploc_max": 8,
1724                  "lloc_min": 6,
1725                  "lloc_max": 6,
1726                  "blank_min": 1,
1727                  "blank_max": 1
1728                }
1729                "#
1730                );
1731            },
1732        );
1733    }
1734
1735    #[cfg(feature = "cpp")]
1736    #[test]
1737    fn cpp_code_line_block_one_line_blank() {
1738        // Checks that the blank metric is equal to 1 when there are
1739        // block comments before the same code line.
1740        check_metrics::<CppParser>(
1741            "void ConnectToUpdateServer() {
1742              int pool;
1743
1744              int updateServer = -42;
1745              bool isConnected = false;
1746              int currTry = 0;
1747              /* Number of IPC connection retries before giving up. */ const int numRetries = 10;
1748              const int numTries = 20; // Number of IPC connection tries before
1749                                       // giving up.
1750            }",
1751            "foo.cpp",
1752            |metric| {
1753                // Spaces: 2
1754                insta::assert_json_snapshot!(
1755                    metric.loc,
1756                    @r#"
1757                {
1758                  "sloc": 10,
1759                  "ploc": 8,
1760                  "lloc": 6,
1761                  "cloc": 3,
1762                  "blank": 1,
1763                  "sloc_average": 5.0,
1764                  "ploc_average": 4.0,
1765                  "lloc_average": 3.0,
1766                  "cloc_average": 1.5,
1767                  "blank_average": 0.5,
1768                  "sloc_min": 10,
1769                  "sloc_max": 10,
1770                  "cloc_min": 3,
1771                  "cloc_max": 3,
1772                  "ploc_min": 8,
1773                  "ploc_max": 8,
1774                  "lloc_min": 6,
1775                  "lloc_max": 6,
1776                  "blank_min": 1,
1777                  "blank_max": 1
1778                }
1779                "#
1780                );
1781            },
1782        );
1783    }
1784
1785    #[cfg(feature = "cpp")]
1786    #[test]
1787    fn cpp_code_line_end_block_blank() {
1788        // Checks that the blank metric is equal to 1 when there are
1789        // block comments ending next to code lines.
1790        check_metrics::<CppParser>(
1791            "void ConnectToUpdateServer() {
1792              int pool;
1793
1794              int updateServer = -42;
1795              bool isConnected = false;
1796              int currTry = 0;
1797              /* Number of IPC connection retries
1798              before
1799              giving up. */ const int numRetries = 10;
1800              const int numTries = 20; // Number of IPC connection tries before
1801                                       // giving up.
1802            }",
1803            "foo.cpp",
1804            |metric| {
1805                // Spaces: 2
1806                insta::assert_json_snapshot!(
1807                    metric.loc,
1808                    @r#"
1809                {
1810                  "sloc": 12,
1811                  "ploc": 8,
1812                  "lloc": 6,
1813                  "cloc": 5,
1814                  "blank": 1,
1815                  "sloc_average": 6.0,
1816                  "ploc_average": 4.0,
1817                  "lloc_average": 3.0,
1818                  "cloc_average": 2.5,
1819                  "blank_average": 0.5,
1820                  "sloc_min": 12,
1821                  "sloc_max": 12,
1822                  "cloc_min": 5,
1823                  "cloc_max": 5,
1824                  "ploc_min": 8,
1825                  "ploc_max": 8,
1826                  "lloc_min": 6,
1827                  "lloc_max": 6,
1828                  "blank_min": 1,
1829                  "blank_max": 1
1830                }
1831                "#
1832                );
1833            },
1834        );
1835    }
1836
1837    #[cfg(feature = "python")]
1838    #[test]
1839    fn python_cloc() {
1840        check_metrics::<PythonParser>(
1841            "\"\"\"Block comment
1842            Block comment
1843            \"\"\"
1844            # Line Comment
1845            a = 42 # Line Comment",
1846            "foo.py",
1847            |metric| {
1848                // Spaces: 1
1849                insta::assert_json_snapshot!(
1850                    metric.loc,
1851                    @r#"
1852                {
1853                  "sloc": 5,
1854                  "ploc": 1,
1855                  "lloc": 2,
1856                  "cloc": 5,
1857                  "blank": 0,
1858                  "sloc_average": 5.0,
1859                  "ploc_average": 1.0,
1860                  "lloc_average": 2.0,
1861                  "cloc_average": 5.0,
1862                  "blank_average": 0.0,
1863                  "sloc_min": 5,
1864                  "sloc_max": 5,
1865                  "cloc_min": 5,
1866                  "cloc_max": 5,
1867                  "ploc_min": 1,
1868                  "ploc_max": 1,
1869                  "lloc_min": 2,
1870                  "lloc_max": 2,
1871                  "blank_min": 0,
1872                  "blank_max": 0
1873                }
1874                "#
1875                );
1876            },
1877        );
1878    }
1879
1880    #[cfg(feature = "rust")]
1881    #[test]
1882    fn rust_cloc() {
1883        check_metrics::<RustParser>(
1884            "/*Block comment
1885            Block Comment*/
1886            //Line Comment
1887            /*Block Comment*/ let a = 42; // Line Comment",
1888            "foo.rs",
1889            |metric| {
1890                // Spaces: 1
1891                // expected: cloc = 4 — the 2-line block (lines 1-2) and
1892                // the standalone `//Line Comment` (line 3) give 3
1893                // only-comment lines; line 4 carries a leading block
1894                // comment AND a trailing line comment but is one
1895                // physical code line, so it adds a single code-comment
1896                // line, not two (issue #461). Pre-fix this reported 5,
1897                // violating cloc <= sloc (sloc = 4).
1898                insta::assert_json_snapshot!(
1899                    metric.loc,
1900                    @r#"
1901                {
1902                  "sloc": 4,
1903                  "ploc": 1,
1904                  "lloc": 1,
1905                  "cloc": 4,
1906                  "blank": 0,
1907                  "sloc_average": 4.0,
1908                  "ploc_average": 1.0,
1909                  "lloc_average": 1.0,
1910                  "cloc_average": 4.0,
1911                  "blank_average": 0.0,
1912                  "sloc_min": 4,
1913                  "sloc_max": 4,
1914                  "cloc_min": 4,
1915                  "cloc_max": 4,
1916                  "ploc_min": 1,
1917                  "ploc_max": 1,
1918                  "lloc_min": 1,
1919                  "lloc_max": 1,
1920                  "blank_min": 0,
1921                  "blank_max": 0
1922                }
1923                "#
1924                );
1925            },
1926        );
1927    }
1928
1929    #[cfg(feature = "c")]
1930    #[test]
1931    fn c_cloc() {
1932        check_metrics::<CParser>(
1933            "/*Block comment
1934            Block Comment*/
1935            //Line Comment
1936            /*Block Comment*/ int a = 42; // Line Comment",
1937            "foo.c",
1938            |metric| {
1939                // Spaces: 1
1940                // expected: cloc = 4 — see `rust_cloc`; line 4's leading
1941                // block comment and trailing line comment share one
1942                // physical code line and add a single code-comment line
1943                // (issue #461). Pre-fix reported 5 (cloc > sloc = 4).
1944                insta::assert_json_snapshot!(
1945                    metric.loc,
1946                    @r#"
1947                {
1948                  "sloc": 4,
1949                  "ploc": 1,
1950                  "lloc": 1,
1951                  "cloc": 4,
1952                  "blank": 0,
1953                  "sloc_average": 4.0,
1954                  "ploc_average": 1.0,
1955                  "lloc_average": 1.0,
1956                  "cloc_average": 4.0,
1957                  "blank_average": 0.0,
1958                  "sloc_min": 4,
1959                  "sloc_max": 4,
1960                  "cloc_min": 4,
1961                  "cloc_max": 4,
1962                  "ploc_min": 1,
1963                  "ploc_max": 1,
1964                  "lloc_min": 1,
1965                  "lloc_max": 1,
1966                  "blank_min": 0,
1967                  "blank_max": 0
1968                }
1969                "#
1970                );
1971            },
1972        );
1973    }
1974
1975    #[cfg(feature = "python")]
1976    #[test]
1977    fn python_lloc() {
1978        check_metrics::<PythonParser>(
1979            "for x in range(0,42):
1980                if x % 2 == 0:
1981                    print(x)",
1982            "foo.py",
1983            |metric| {
1984                // Spaces: 1
1985                insta::assert_json_snapshot!(
1986                    metric.loc,
1987                    @r#"
1988                {
1989                  "sloc": 3,
1990                  "ploc": 3,
1991                  "lloc": 3,
1992                  "cloc": 0,
1993                  "blank": 0,
1994                  "sloc_average": 3.0,
1995                  "ploc_average": 3.0,
1996                  "lloc_average": 3.0,
1997                  "cloc_average": 0.0,
1998                  "blank_average": 0.0,
1999                  "sloc_min": 3,
2000                  "sloc_max": 3,
2001                  "cloc_min": 0,
2002                  "cloc_max": 0,
2003                  "ploc_min": 3,
2004                  "ploc_max": 3,
2005                  "lloc_min": 3,
2006                  "lloc_max": 3,
2007                  "blank_min": 0,
2008                  "blank_max": 0
2009                }
2010                "#
2011                );
2012            },
2013        );
2014    }
2015
2016    #[cfg(feature = "rust")]
2017    #[test]
2018    fn rust_lloc() {
2019        check_metrics::<RustParser>(
2020            "for x in 0..42 {
2021                if x % 2 == 0 {
2022                    println!(\"{}\", x);
2023                }
2024             }",
2025            "foo.rs",
2026            |metric| {
2027                // Spaces: 1
2028                insta::assert_json_snapshot!(
2029                    metric.loc,
2030                    @r#"
2031                {
2032                  "sloc": 5,
2033                  "ploc": 5,
2034                  "lloc": 3,
2035                  "cloc": 0,
2036                  "blank": 0,
2037                  "sloc_average": 5.0,
2038                  "ploc_average": 5.0,
2039                  "lloc_average": 3.0,
2040                  "cloc_average": 0.0,
2041                  "blank_average": 0.0,
2042                  "sloc_min": 5,
2043                  "sloc_max": 5,
2044                  "cloc_min": 0,
2045                  "cloc_max": 0,
2046                  "ploc_min": 5,
2047                  "ploc_max": 5,
2048                  "lloc_min": 3,
2049                  "lloc_max": 3,
2050                  "blank_min": 0,
2051                  "blank_max": 0
2052                }
2053                "#
2054                );
2055            },
2056        );
2057
2058        // LLOC returns three because there is an empty Rust statement
2059        check_metrics::<RustParser>(
2060            "let a = 42;
2061             if true {
2062                42
2063             } else {
2064                43
2065             };",
2066            "foo.rs",
2067            |metric| {
2068                // Spaces: 1
2069                insta::assert_json_snapshot!(
2070                    metric.loc,
2071                    @r#"
2072                {
2073                  "sloc": 6,
2074                  "ploc": 6,
2075                  "lloc": 3,
2076                  "cloc": 0,
2077                  "blank": 0,
2078                  "sloc_average": 6.0,
2079                  "ploc_average": 6.0,
2080                  "lloc_average": 3.0,
2081                  "cloc_average": 0.0,
2082                  "blank_average": 0.0,
2083                  "sloc_min": 6,
2084                  "sloc_max": 6,
2085                  "cloc_min": 0,
2086                  "cloc_max": 0,
2087                  "ploc_min": 6,
2088                  "ploc_max": 6,
2089                  "lloc_min": 3,
2090                  "lloc_max": 3,
2091                  "blank_min": 0,
2092                  "blank_max": 0
2093                }
2094                "#
2095                );
2096            },
2097        );
2098    }
2099
2100    #[cfg(feature = "c")]
2101    #[test]
2102    fn c_lloc() {
2103        check_metrics::<CParser>(
2104            "for (;;)
2105                break;",
2106            "foo.c",
2107            |metric| {
2108                // Spaces: 1
2109                insta::assert_json_snapshot!(
2110                    metric.loc,
2111                    @r#"
2112                {
2113                  "sloc": 2,
2114                  "ploc": 2,
2115                  "lloc": 2,
2116                  "cloc": 0,
2117                  "blank": 0,
2118                  "sloc_average": 2.0,
2119                  "ploc_average": 2.0,
2120                  "lloc_average": 2.0,
2121                  "cloc_average": 0.0,
2122                  "blank_average": 0.0,
2123                  "sloc_min": 2,
2124                  "sloc_max": 2,
2125                  "cloc_min": 0,
2126                  "cloc_max": 0,
2127                  "ploc_min": 2,
2128                  "ploc_max": 2,
2129                  "lloc_min": 2,
2130                  "lloc_max": 2,
2131                  "blank_min": 0,
2132                  "blank_max": 0
2133                }
2134                "#
2135                );
2136            },
2137        );
2138    }
2139
2140    #[cfg(feature = "cpp")]
2141    #[test]
2142    fn cpp_lloc() {
2143        check_metrics::<CppParser>(
2144            "nsTArray<xpcGCCallback> callbacks(extraGCCallbacks.Clone());
2145             for (uint32_t i = 0; i < callbacks.Length(); ++i) {
2146                 callbacks[i](status);
2147             }",
2148            "foo.cpp",
2149            |metric| {
2150                // Spaces: 1
2151                // lloc: nsTArray, for, callbacks
2152                insta::assert_json_snapshot!(
2153                    metric.loc,
2154                    @r#"
2155                {
2156                  "sloc": 4,
2157                  "ploc": 4,
2158                  "lloc": 3,
2159                  "cloc": 0,
2160                  "blank": 0,
2161                  "sloc_average": 4.0,
2162                  "ploc_average": 4.0,
2163                  "lloc_average": 3.0,
2164                  "cloc_average": 0.0,
2165                  "blank_average": 0.0,
2166                  "sloc_min": 4,
2167                  "sloc_max": 4,
2168                  "cloc_min": 0,
2169                  "cloc_max": 0,
2170                  "ploc_min": 4,
2171                  "ploc_max": 4,
2172                  "lloc_min": 3,
2173                  "lloc_max": 3,
2174                  "blank_min": 0,
2175                  "blank_max": 0
2176                }
2177                "#
2178                );
2179            },
2180        );
2181    }
2182
2183    #[cfg(feature = "cpp")]
2184    #[test]
2185    fn cpp_return_lloc() {
2186        check_metrics::<CppParser>(
2187            "uint8_t* pixel_data = frame.GetFrameDataAtPos(DesktopVector(x, y));
2188             return RgbaColor(pixel_data) == blank_pixel_;",
2189            "foo.cpp",
2190            |metric| {
2191                // Spaces: 1
2192                // lloc: pixel_data, return
2193                insta::assert_json_snapshot!(
2194                    metric.loc,
2195                    @r#"
2196                {
2197                  "sloc": 2,
2198                  "ploc": 2,
2199                  "lloc": 2,
2200                  "cloc": 0,
2201                  "blank": 0,
2202                  "sloc_average": 2.0,
2203                  "ploc_average": 2.0,
2204                  "lloc_average": 2.0,
2205                  "cloc_average": 0.0,
2206                  "blank_average": 0.0,
2207                  "sloc_min": 2,
2208                  "sloc_max": 2,
2209                  "cloc_min": 0,
2210                  "cloc_max": 0,
2211                  "ploc_min": 2,
2212                  "ploc_max": 2,
2213                  "lloc_min": 2,
2214                  "lloc_max": 2,
2215                  "blank_min": 0,
2216                  "blank_max": 0
2217                }
2218                "#
2219                );
2220            },
2221        );
2222    }
2223
2224    #[cfg(feature = "cpp")]
2225    #[test]
2226    fn cpp_for_lloc() {
2227        check_metrics::<CppParser>(
2228            "for (; start != end; ++start) {
2229                 const unsigned char idx = *start;
2230                 if (idx > 127 || !kValidTokenMap[idx]) return false;
2231             }",
2232            "foo.cpp",
2233            |metric| {
2234                // Spaces: 1
2235                // lloc: for, idx, if, return
2236                insta::assert_json_snapshot!(
2237                    metric.loc,
2238                    @r#"
2239                {
2240                  "sloc": 4,
2241                  "ploc": 4,
2242                  "lloc": 4,
2243                  "cloc": 0,
2244                  "blank": 0,
2245                  "sloc_average": 4.0,
2246                  "ploc_average": 4.0,
2247                  "lloc_average": 4.0,
2248                  "cloc_average": 0.0,
2249                  "blank_average": 0.0,
2250                  "sloc_min": 4,
2251                  "sloc_max": 4,
2252                  "cloc_min": 0,
2253                  "cloc_max": 0,
2254                  "ploc_min": 4,
2255                  "ploc_max": 4,
2256                  "lloc_min": 4,
2257                  "lloc_max": 4,
2258                  "blank_min": 0,
2259                  "blank_max": 0
2260                }
2261                "#
2262                );
2263            },
2264        );
2265    }
2266
2267    #[cfg(feature = "cpp")]
2268    #[test]
2269    fn cpp_while_lloc() {
2270        check_metrics::<CppParser>(
2271            "while (sHeapAtoms) {
2272                 HttpHeapAtom* next = sHeapAtoms->next;
2273                 free(sHeapAtoms);
2274            }",
2275            "foo.cpp",
2276            |metric| {
2277                // Spaces: 1
2278                // lloc: while, next, free
2279                insta::assert_json_snapshot!(
2280                    metric.loc,
2281                    @r#"
2282                {
2283                  "sloc": 4,
2284                  "ploc": 4,
2285                  "lloc": 3,
2286                  "cloc": 0,
2287                  "blank": 0,
2288                  "sloc_average": 4.0,
2289                  "ploc_average": 4.0,
2290                  "lloc_average": 3.0,
2291                  "cloc_average": 0.0,
2292                  "blank_average": 0.0,
2293                  "sloc_min": 4,
2294                  "sloc_max": 4,
2295                  "cloc_min": 0,
2296                  "cloc_max": 0,
2297                  "ploc_min": 4,
2298                  "ploc_max": 4,
2299                  "lloc_min": 3,
2300                  "lloc_max": 3,
2301                  "blank_min": 0,
2302                  "blank_max": 0
2303                }
2304                "#
2305                );
2306            },
2307        );
2308    }
2309
2310    #[cfg(feature = "python")]
2311    #[test]
2312    fn python_string_on_new_line() {
2313        // More lines of the same instruction were counted as blank lines
2314        check_metrics::<PythonParser>(
2315            "capabilities[\"goog:chromeOptions\"][\"androidPackage\"] = \\
2316                \"org.chromium.weblayer.shell\"",
2317            "foo.py",
2318            |metric| {
2319                // Spaces: 1
2320                insta::assert_json_snapshot!(
2321                    metric.loc,
2322                    @r#"
2323                {
2324                  "sloc": 2,
2325                  "ploc": 2,
2326                  "lloc": 1,
2327                  "cloc": 0,
2328                  "blank": 0,
2329                  "sloc_average": 2.0,
2330                  "ploc_average": 2.0,
2331                  "lloc_average": 1.0,
2332                  "cloc_average": 0.0,
2333                  "blank_average": 0.0,
2334                  "sloc_min": 2,
2335                  "sloc_max": 2,
2336                  "cloc_min": 0,
2337                  "cloc_max": 0,
2338                  "ploc_min": 2,
2339                  "ploc_max": 2,
2340                  "lloc_min": 1,
2341                  "lloc_max": 1,
2342                  "blank_min": 0,
2343                  "blank_max": 0
2344                }
2345                "#
2346                );
2347            },
2348        );
2349    }
2350
2351    #[cfg(feature = "python")]
2352    #[test]
2353    fn python_multiline_string_assignment_ploc() {
2354        // Regression test for issue #415: interior rows of a multi-line,
2355        // non-docstring string literal were mis-counted as blank lines.
2356        check_metrics::<PythonParser>(
2357            "QUERY = \"\"\"
2358SELECT id, name
2359FROM users
2360WHERE active = 1
2361ORDER BY name
2362\"\"\"",
2363            "foo.py",
2364            |metric| {
2365                // Spaces: 1. Six physical lines, all code, no blanks.
2366                assert_eq!(metric.loc.sloc(), 6);
2367                assert_eq!(metric.loc.ploc(), 6);
2368                assert_eq!(metric.loc.cloc(), 0);
2369                assert_eq!(metric.loc.blank(), 0);
2370                insta::assert_json_snapshot!(
2371                    metric.loc,
2372                    @r#"
2373                {
2374                  "sloc": 6,
2375                  "ploc": 6,
2376                  "lloc": 1,
2377                  "cloc": 0,
2378                  "blank": 0,
2379                  "sloc_average": 6.0,
2380                  "ploc_average": 6.0,
2381                  "lloc_average": 1.0,
2382                  "cloc_average": 0.0,
2383                  "blank_average": 0.0,
2384                  "sloc_min": 6,
2385                  "sloc_max": 6,
2386                  "cloc_min": 0,
2387                  "cloc_max": 0,
2388                  "ploc_min": 6,
2389                  "ploc_max": 6,
2390                  "lloc_min": 1,
2391                  "lloc_max": 1,
2392                  "blank_min": 0,
2393                  "blank_max": 0
2394                }
2395                "#
2396                );
2397            },
2398        );
2399    }
2400
2401    #[cfg(feature = "python")]
2402    #[test]
2403    fn python_multiline_string_argument_ploc() {
2404        // Regression test for issue #415: a multi-line string passed as a call
2405        // argument must have all its rows counted as code, not blank.
2406        check_metrics::<PythonParser>(
2407            "print(\"\"\"
2408line one
2409line two
2410\"\"\")",
2411            "foo.py",
2412            |metric| {
2413                // Spaces: 1. Four physical lines, all code, no blanks.
2414                assert_eq!(metric.loc.sloc(), 4);
2415                assert_eq!(metric.loc.ploc(), 4);
2416                assert_eq!(metric.loc.cloc(), 0);
2417                assert_eq!(metric.loc.blank(), 0);
2418            },
2419        );
2420    }
2421
2422    #[cfg(feature = "python")]
2423    #[test]
2424    fn python_single_line_string_assignment_ploc() {
2425        // Single-line, non-docstring string: behaviour must be unchanged by
2426        // the issue #415 fix (start == end means no extra rows are inserted).
2427        check_metrics::<PythonParser>("QUERY = \"SELECT 1\"", "foo.py", |metric| {
2428            // Spaces: 1.
2429            assert_eq!(metric.loc.sloc(), 1);
2430            assert_eq!(metric.loc.ploc(), 1);
2431            assert_eq!(metric.loc.cloc(), 0);
2432            assert_eq!(metric.loc.blank(), 0);
2433        });
2434    }
2435
2436    #[cfg(feature = "python")]
2437    #[test]
2438    fn python_multiline_docstring_still_cloc() {
2439        // The fix for issue #415 must leave docstring classification unchanged:
2440        // a bare triple-quoted string statement is still counted as comments.
2441        check_metrics::<PythonParser>(
2442            "def f():
2443    \"\"\"Docstring line one
2444    Docstring line two
2445    \"\"\"
2446    return 1",
2447            "foo.py",
2448            |metric| {
2449                // Spaces: 2 (module + function). The three docstring rows are
2450                // comment lines, not code or blank lines.
2451                assert_eq!(metric.loc.cloc(), 3);
2452                assert_eq!(metric.loc.blank(), 0);
2453            },
2454        );
2455    }
2456
2457    #[cfg(feature = "rust")]
2458    #[test]
2459    fn rust_no_field_expression_lloc() {
2460        check_metrics::<RustParser>(
2461            "struct Foo {
2462                field: usize,
2463             }
2464             let foo = Foo { 42 };
2465             foo.field;",
2466            "foo.rs",
2467            |metric| {
2468                // Spaces: 1
2469                insta::assert_json_snapshot!(
2470                    metric.loc,
2471                    @r#"
2472                {
2473                  "sloc": 5,
2474                  "ploc": 5,
2475                  "lloc": 2,
2476                  "cloc": 0,
2477                  "blank": 0,
2478                  "sloc_average": 5.0,
2479                  "ploc_average": 5.0,
2480                  "lloc_average": 2.0,
2481                  "cloc_average": 0.0,
2482                  "blank_average": 0.0,
2483                  "sloc_min": 5,
2484                  "sloc_max": 5,
2485                  "cloc_min": 0,
2486                  "cloc_max": 0,
2487                  "ploc_min": 5,
2488                  "ploc_max": 5,
2489                  "lloc_min": 2,
2490                  "lloc_max": 2,
2491                  "blank_min": 0,
2492                  "blank_max": 0
2493                }
2494                "#
2495                );
2496            },
2497        );
2498    }
2499
2500    #[cfg(feature = "rust")]
2501    #[test]
2502    fn rust_no_parenthesized_expression_lloc() {
2503        check_metrics::<RustParser>("let a = (42 + 0);", "foo.rs", |metric| {
2504            // Spaces: 1
2505            insta::assert_json_snapshot!(
2506                metric.loc,
2507                @r#"
2508            {
2509              "sloc": 1,
2510              "ploc": 1,
2511              "lloc": 1,
2512              "cloc": 0,
2513              "blank": 0,
2514              "sloc_average": 1.0,
2515              "ploc_average": 1.0,
2516              "lloc_average": 1.0,
2517              "cloc_average": 0.0,
2518              "blank_average": 0.0,
2519              "sloc_min": 1,
2520              "sloc_max": 1,
2521              "cloc_min": 0,
2522              "cloc_max": 0,
2523              "ploc_min": 1,
2524              "ploc_max": 1,
2525              "lloc_min": 1,
2526              "lloc_max": 1,
2527              "blank_min": 0,
2528              "blank_max": 0
2529            }
2530            "#
2531            );
2532        });
2533    }
2534
2535    #[cfg(feature = "rust")]
2536    #[test]
2537    fn rust_no_array_expression_lloc() {
2538        check_metrics::<RustParser>("let a = [0; 42];", "foo.rs", |metric| {
2539            // Spaces: 1
2540            insta::assert_json_snapshot!(
2541                metric.loc,
2542                @r#"
2543            {
2544              "sloc": 1,
2545              "ploc": 1,
2546              "lloc": 1,
2547              "cloc": 0,
2548              "blank": 0,
2549              "sloc_average": 1.0,
2550              "ploc_average": 1.0,
2551              "lloc_average": 1.0,
2552              "cloc_average": 0.0,
2553              "blank_average": 0.0,
2554              "sloc_min": 1,
2555              "sloc_max": 1,
2556              "cloc_min": 0,
2557              "cloc_max": 0,
2558              "ploc_min": 1,
2559              "ploc_max": 1,
2560              "lloc_min": 1,
2561              "lloc_max": 1,
2562              "blank_min": 0,
2563              "blank_max": 0
2564            }
2565            "#
2566            );
2567        });
2568    }
2569
2570    #[cfg(feature = "rust")]
2571    #[test]
2572    fn rust_no_tuple_expression_lloc() {
2573        check_metrics::<RustParser>("let a = (0, 42);", "foo.rs", |metric| {
2574            // Spaces: 1
2575            insta::assert_json_snapshot!(
2576                metric.loc,
2577                @r#"
2578            {
2579              "sloc": 1,
2580              "ploc": 1,
2581              "lloc": 1,
2582              "cloc": 0,
2583              "blank": 0,
2584              "sloc_average": 1.0,
2585              "ploc_average": 1.0,
2586              "lloc_average": 1.0,
2587              "cloc_average": 0.0,
2588              "blank_average": 0.0,
2589              "sloc_min": 1,
2590              "sloc_max": 1,
2591              "cloc_min": 0,
2592              "cloc_max": 0,
2593              "ploc_min": 1,
2594              "ploc_max": 1,
2595              "lloc_min": 1,
2596              "lloc_max": 1,
2597              "blank_min": 0,
2598              "blank_max": 0
2599            }
2600            "#
2601            );
2602        });
2603    }
2604
2605    #[cfg(feature = "rust")]
2606    #[test]
2607    fn rust_no_unit_expression_lloc() {
2608        check_metrics::<RustParser>("let a = ();", "foo.rs", |metric| {
2609            // Spaces: 1
2610            insta::assert_json_snapshot!(
2611                metric.loc,
2612                @r#"
2613            {
2614              "sloc": 1,
2615              "ploc": 1,
2616              "lloc": 1,
2617              "cloc": 0,
2618              "blank": 0,
2619              "sloc_average": 1.0,
2620              "ploc_average": 1.0,
2621              "lloc_average": 1.0,
2622              "cloc_average": 0.0,
2623              "blank_average": 0.0,
2624              "sloc_min": 1,
2625              "sloc_max": 1,
2626              "cloc_min": 0,
2627              "cloc_max": 0,
2628              "ploc_min": 1,
2629              "ploc_max": 1,
2630              "lloc_min": 1,
2631              "lloc_max": 1,
2632              "blank_min": 0,
2633              "blank_max": 0
2634            }
2635            "#
2636            );
2637        });
2638    }
2639
2640    #[cfg(feature = "rust")]
2641    #[test]
2642    fn rust_call_function_lloc() {
2643        check_metrics::<RustParser>(
2644            "let a = foo(); // +1
2645             foo(); // +1
2646             k!(foo()); // +1",
2647            "foo.rs",
2648            |metric| {
2649                // Spaces: 1
2650                insta::assert_json_snapshot!(
2651                    metric.loc,
2652                    @r#"
2653                {
2654                  "sloc": 3,
2655                  "ploc": 3,
2656                  "lloc": 3,
2657                  "cloc": 3,
2658                  "blank": 0,
2659                  "sloc_average": 3.0,
2660                  "ploc_average": 3.0,
2661                  "lloc_average": 3.0,
2662                  "cloc_average": 3.0,
2663                  "blank_average": 0.0,
2664                  "sloc_min": 3,
2665                  "sloc_max": 3,
2666                  "cloc_min": 3,
2667                  "cloc_max": 3,
2668                  "ploc_min": 3,
2669                  "ploc_max": 3,
2670                  "lloc_min": 3,
2671                  "lloc_max": 3,
2672                  "blank_min": 0,
2673                  "blank_max": 0
2674                }
2675                "#
2676                );
2677            },
2678        );
2679    }
2680
2681    #[cfg(feature = "rust")]
2682    #[test]
2683    fn rust_macro_invocation_lloc() {
2684        check_metrics::<RustParser>(
2685            "let a = foo!(); // +1
2686             foo!(); // +1
2687             k(foo!()); // +1",
2688            "foo.rs",
2689            |metric| {
2690                // Spaces: 1
2691                insta::assert_json_snapshot!(
2692                    metric.loc,
2693                    @r#"
2694                {
2695                  "sloc": 3,
2696                  "ploc": 3,
2697                  "lloc": 3,
2698                  "cloc": 3,
2699                  "blank": 0,
2700                  "sloc_average": 3.0,
2701                  "ploc_average": 3.0,
2702                  "lloc_average": 3.0,
2703                  "cloc_average": 3.0,
2704                  "blank_average": 0.0,
2705                  "sloc_min": 3,
2706                  "sloc_max": 3,
2707                  "cloc_min": 3,
2708                  "cloc_max": 3,
2709                  "ploc_min": 3,
2710                  "ploc_max": 3,
2711                  "lloc_min": 3,
2712                  "lloc_max": 3,
2713                  "blank_min": 0,
2714                  "blank_max": 0
2715                }
2716                "#
2717                );
2718            },
2719        );
2720    }
2721
2722    #[cfg(feature = "rust")]
2723    #[test]
2724    fn rust_function_in_loop_lloc() {
2725        check_metrics::<RustParser>(
2726            "for (a, b) in c.iter().enumerate() {} // +1
2727             while (a, b) in c.iter().enumerate() {} // +1
2728             while let Some(a) = c.strip_prefix(\"hi\") {} // +1",
2729            "foo.rs",
2730            |metric| {
2731                // Spaces: 1
2732                insta::assert_json_snapshot!(
2733                    metric.loc,
2734                    @r#"
2735                {
2736                  "sloc": 3,
2737                  "ploc": 3,
2738                  "lloc": 3,
2739                  "cloc": 3,
2740                  "blank": 0,
2741                  "sloc_average": 3.0,
2742                  "ploc_average": 3.0,
2743                  "lloc_average": 3.0,
2744                  "cloc_average": 3.0,
2745                  "blank_average": 0.0,
2746                  "sloc_min": 3,
2747                  "sloc_max": 3,
2748                  "cloc_min": 3,
2749                  "cloc_max": 3,
2750                  "ploc_min": 3,
2751                  "ploc_max": 3,
2752                  "lloc_min": 3,
2753                  "lloc_max": 3,
2754                  "blank_min": 0,
2755                  "blank_max": 0
2756                }
2757                "#
2758                );
2759            },
2760        );
2761    }
2762
2763    #[cfg(feature = "rust")]
2764    #[test]
2765    fn rust_function_in_if_lloc() {
2766        check_metrics::<RustParser>(
2767            "if foo() {} // +1
2768             if let Some(a) = foo() {} // +1",
2769            "foo.rs",
2770            |metric| {
2771                // Spaces: 1
2772                insta::assert_json_snapshot!(
2773                    metric.loc,
2774                    @r#"
2775                {
2776                  "sloc": 2,
2777                  "ploc": 2,
2778                  "lloc": 2,
2779                  "cloc": 2,
2780                  "blank": 0,
2781                  "sloc_average": 2.0,
2782                  "ploc_average": 2.0,
2783                  "lloc_average": 2.0,
2784                  "cloc_average": 2.0,
2785                  "blank_average": 0.0,
2786                  "sloc_min": 2,
2787                  "sloc_max": 2,
2788                  "cloc_min": 2,
2789                  "cloc_max": 2,
2790                  "ploc_min": 2,
2791                  "ploc_max": 2,
2792                  "lloc_min": 2,
2793                  "lloc_max": 2,
2794                  "blank_min": 0,
2795                  "blank_max": 0
2796                }
2797                "#
2798                );
2799            },
2800        );
2801    }
2802
2803    #[cfg(feature = "rust")]
2804    #[test]
2805    fn rust_function_in_return_lloc() {
2806        check_metrics::<RustParser>(
2807            "return foo();
2808             await foo();",
2809            "foo.rs",
2810            |metric| {
2811                // Spaces: 1
2812                insta::assert_json_snapshot!(
2813                    metric.loc,
2814                    @r#"
2815                {
2816                  "sloc": 2,
2817                  "ploc": 2,
2818                  "lloc": 2,
2819                  "cloc": 0,
2820                  "blank": 0,
2821                  "sloc_average": 2.0,
2822                  "ploc_average": 2.0,
2823                  "lloc_average": 2.0,
2824                  "cloc_average": 0.0,
2825                  "blank_average": 0.0,
2826                  "sloc_min": 2,
2827                  "sloc_max": 2,
2828                  "cloc_min": 0,
2829                  "cloc_max": 0,
2830                  "ploc_min": 2,
2831                  "ploc_max": 2,
2832                  "lloc_min": 2,
2833                  "lloc_max": 2,
2834                  "blank_min": 0,
2835                  "blank_max": 0
2836                }
2837                "#
2838                );
2839            },
2840        );
2841    }
2842
2843    #[cfg(feature = "rust")]
2844    #[test]
2845    fn rust_closure_expression_lloc() {
2846        check_metrics::<RustParser>(
2847            "let a = |i: i32| -> i32 { i + 1 }; // +1
2848             a(42); // +1
2849             k(b.iter().map(|n| n.parse.ok().unwrap_or(42))); // +1",
2850            "foo.rs",
2851            |metric| {
2852                // Spaces: 3
2853                insta::assert_json_snapshot!(
2854                    metric.loc,
2855                    @r#"
2856                {
2857                  "sloc": 3,
2858                  "ploc": 3,
2859                  "lloc": 3,
2860                  "cloc": 3,
2861                  "blank": 0,
2862                  "sloc_average": 1.0,
2863                  "ploc_average": 1.0,
2864                  "lloc_average": 1.0,
2865                  "cloc_average": 1.0,
2866                  "blank_average": 0.0,
2867                  "sloc_min": 1,
2868                  "sloc_max": 3,
2869                  "cloc_min": 0,
2870                  "cloc_max": 3,
2871                  "ploc_min": 1,
2872                  "ploc_max": 3,
2873                  "lloc_min": 0,
2874                  "lloc_max": 3,
2875                  "blank_min": 0,
2876                  "blank_max": 0
2877                }
2878                "#
2879                );
2880            },
2881        );
2882    }
2883
2884    #[cfg(feature = "python")]
2885    #[test]
2886    fn python_general_loc() {
2887        check_metrics::<PythonParser>(
2888            "def func(a,
2889                      b,
2890                      c):
2891                 print(a)
2892                 print(b)
2893                 print(c)",
2894            "foo.py",
2895            |metric| {
2896                // Spaces: 2
2897                insta::assert_json_snapshot!(
2898                    metric.loc,
2899                    @r#"
2900                {
2901                  "sloc": 6,
2902                  "ploc": 6,
2903                  "lloc": 3,
2904                  "cloc": 0,
2905                  "blank": 0,
2906                  "sloc_average": 3.0,
2907                  "ploc_average": 3.0,
2908                  "lloc_average": 1.5,
2909                  "cloc_average": 0.0,
2910                  "blank_average": 0.0,
2911                  "sloc_min": 6,
2912                  "sloc_max": 6,
2913                  "cloc_min": 0,
2914                  "cloc_max": 0,
2915                  "ploc_min": 6,
2916                  "ploc_max": 6,
2917                  "lloc_min": 3,
2918                  "lloc_max": 3,
2919                  "blank_min": 0,
2920                  "blank_max": 0
2921                }
2922                "#
2923                );
2924            },
2925        );
2926    }
2927
2928    #[cfg(feature = "python")]
2929    #[test]
2930    fn python_real_loc() {
2931        check_metrics::<PythonParser>(
2932            "def web_socket_transfer_data(request):
2933                while True:
2934                    line = request.ws_stream.receive_message()
2935                    if line is None:
2936                        return
2937                    code, reason = line.split(' ', 1)
2938                    if code is None or reason is None:
2939                        return
2940                    request.ws_stream.close_connection(int(code), reason)
2941                    # close_connection() initiates closing handshake. It validates code
2942                    # and reason. If you want to send a broken close frame for a test,
2943                    # following code will be useful.
2944                    # > data = struct.pack('!H', int(code)) + reason.encode('UTF-8')
2945                    # > request.connection.write(stream.create_close_frame(data))
2946                    # > # Suppress to re-respond client responding close frame.
2947                    # > raise Exception(\"customized server initiated closing handshake\")",
2948            "foo.py",
2949            |metric| {
2950                // Spaces: 2
2951                insta::assert_json_snapshot!(
2952                    metric.loc,
2953                    @r#"
2954                {
2955                  "sloc": 16,
2956                  "ploc": 9,
2957                  "lloc": 8,
2958                  "cloc": 7,
2959                  "blank": 0,
2960                  "sloc_average": 8.0,
2961                  "ploc_average": 4.5,
2962                  "lloc_average": 4.0,
2963                  "cloc_average": 3.5,
2964                  "blank_average": 0.0,
2965                  "sloc_min": 16,
2966                  "sloc_max": 16,
2967                  "cloc_min": 7,
2968                  "cloc_max": 7,
2969                  "ploc_min": 9,
2970                  "ploc_max": 9,
2971                  "lloc_min": 8,
2972                  "lloc_max": 8,
2973                  "blank_min": 0,
2974                  "blank_max": 0
2975                }
2976                "#
2977                );
2978            },
2979        );
2980    }
2981
2982    #[cfg(feature = "javascript")]
2983    #[test]
2984    fn javascript_real_loc() {
2985        check_metrics::<JavascriptParser>(
2986            "assert.throws(Test262Error, function() {
2987               for (let { poisoned: x = ++initEvalCount } = poisonedProperty; ; ) {
2988                 return;
2989               }
2990             });",
2991            "foo.js",
2992            |metric| {
2993                // Spaces: 2
2994                insta::assert_json_snapshot!(
2995                    metric.loc,
2996                    @r#"
2997                {
2998                  "sloc": 5,
2999                  "ploc": 5,
3000                  "lloc": 4,
3001                  "cloc": 0,
3002                  "blank": 0,
3003                  "sloc_average": 2.5,
3004                  "ploc_average": 2.5,
3005                  "lloc_average": 2.0,
3006                  "cloc_average": 0.0,
3007                  "blank_average": 0.0,
3008                  "sloc_min": 5,
3009                  "sloc_max": 5,
3010                  "cloc_min": 0,
3011                  "cloc_max": 0,
3012                  "ploc_min": 5,
3013                  "ploc_max": 5,
3014                  "lloc_min": 3,
3015                  "lloc_max": 4,
3016                  "blank_min": 0,
3017                  "blank_max": 0
3018                }
3019                "#
3020                );
3021            },
3022        );
3023    }
3024
3025    #[cfg(feature = "mozjs")]
3026    #[test]
3027    fn mozjs_real_loc() {
3028        check_metrics::<MozjsParser>(
3029            "assert.throws(Test262Error, function() {
3030               for (let { poisoned: x = ++initEvalCount } = poisonedProperty; ; ) {
3031                 return;
3032               }
3033             });",
3034            "foo.js",
3035            |metric| {
3036                // Spaces: 2
3037                insta::assert_json_snapshot!(
3038                    metric.loc,
3039                    @r#"
3040                {
3041                  "sloc": 5,
3042                  "ploc": 5,
3043                  "lloc": 4,
3044                  "cloc": 0,
3045                  "blank": 0,
3046                  "sloc_average": 2.5,
3047                  "ploc_average": 2.5,
3048                  "lloc_average": 2.0,
3049                  "cloc_average": 0.0,
3050                  "blank_average": 0.0,
3051                  "sloc_min": 5,
3052                  "sloc_max": 5,
3053                  "cloc_min": 0,
3054                  "cloc_max": 0,
3055                  "ploc_min": 5,
3056                  "ploc_max": 5,
3057                  "lloc_min": 3,
3058                  "lloc_max": 4,
3059                  "blank_min": 0,
3060                  "blank_max": 0
3061                }
3062                "#
3063                );
3064            },
3065        );
3066    }
3067
3068    #[cfg(feature = "mozjs")]
3069    #[test]
3070    fn mozjs_blank_and_comment_loc() {
3071        check_metrics::<MozjsParser>(
3072            "// a comment
3073             function f() {
3074
3075                 var x = 1;
3076
3077             }",
3078            "foo.js",
3079            |metric| {
3080                insta::assert_json_snapshot!(
3081                    metric.loc,
3082                    @r#"
3083                {
3084                  "sloc": 6,
3085                  "ploc": 3,
3086                  "lloc": 1,
3087                  "cloc": 1,
3088                  "blank": 2,
3089                  "sloc_average": 3.0,
3090                  "ploc_average": 1.5,
3091                  "lloc_average": 0.5,
3092                  "cloc_average": 0.5,
3093                  "blank_average": 1.0,
3094                  "sloc_min": 5,
3095                  "sloc_max": 6,
3096                  "cloc_min": 0,
3097                  "cloc_max": 1,
3098                  "ploc_min": 3,
3099                  "ploc_max": 3,
3100                  "lloc_min": 1,
3101                  "lloc_max": 1,
3102                  "blank_min": 2,
3103                  "blank_max": 2
3104                }
3105                "#
3106                );
3107            },
3108        );
3109    }
3110
3111    #[cfg(feature = "cpp")]
3112    #[test]
3113    fn cpp_namespace_loc() {
3114        check_metrics::<CppParser>(
3115            "namespace mozilla::dom::quota {} // namespace mozilla::dom::quota",
3116            "foo.cpp",
3117            |metric| {
3118                // Spaces: 2
3119                insta::assert_json_snapshot!(
3120                    metric.loc,
3121                    @r#"
3122                {
3123                  "sloc": 1,
3124                  "ploc": 1,
3125                  "lloc": 0,
3126                  "cloc": 1,
3127                  "blank": 0,
3128                  "sloc_average": 0.5,
3129                  "ploc_average": 0.5,
3130                  "lloc_average": 0.0,
3131                  "cloc_average": 0.5,
3132                  "blank_average": 0.0,
3133                  "sloc_min": 1,
3134                  "sloc_max": 1,
3135                  "cloc_min": 0,
3136                  "cloc_max": 1,
3137                  "ploc_min": 1,
3138                  "ploc_max": 1,
3139                  "lloc_min": 0,
3140                  "lloc_max": 0,
3141                  "blank_min": 0,
3142                  "blank_max": 0
3143                }
3144                "#
3145                );
3146            },
3147        );
3148    }
3149
3150    #[cfg(feature = "java")]
3151    #[test]
3152    fn java_comments() {
3153        check_metrics::<JavaParser>(
3154            "for (int i = 0; i < 100; i++) { \
3155               // Print hello
3156               System.out.println(\"hello\"); \
3157               // Print world
3158               System.out.println(\"hello\"); \
3159             }",
3160            "foo.java",
3161            |metric| {
3162                // Spaces: 1
3163                insta::assert_json_snapshot!(
3164                    metric.loc,
3165                    @r#"
3166                {
3167                  "sloc": 3,
3168                  "ploc": 3,
3169                  "lloc": 3,
3170                  "cloc": 2,
3171                  "blank": 0,
3172                  "sloc_average": 3.0,
3173                  "ploc_average": 3.0,
3174                  "lloc_average": 3.0,
3175                  "cloc_average": 2.0,
3176                  "blank_average": 0.0,
3177                  "sloc_min": 3,
3178                  "sloc_max": 3,
3179                  "cloc_min": 2,
3180                  "cloc_max": 2,
3181                  "ploc_min": 3,
3182                  "ploc_max": 3,
3183                  "lloc_min": 3,
3184                  "lloc_max": 3,
3185                  "blank_min": 0,
3186                  "blank_max": 0
3187                }
3188                "#
3189                );
3190            },
3191        );
3192    }
3193
3194    #[cfg(feature = "java")]
3195    #[test]
3196    fn java_blank() {
3197        check_metrics::<JavaParser>(
3198            "int x = 1;
3199
3200
3201            int y = 2;",
3202            "foo.java",
3203            |metric| {
3204                // Spaces: 1
3205                insta::assert_json_snapshot!(
3206                    metric.loc,
3207                    @r#"
3208                {
3209                  "sloc": 4,
3210                  "ploc": 2,
3211                  "lloc": 2,
3212                  "cloc": 0,
3213                  "blank": 2,
3214                  "sloc_average": 4.0,
3215                  "ploc_average": 2.0,
3216                  "lloc_average": 2.0,
3217                  "cloc_average": 0.0,
3218                  "blank_average": 2.0,
3219                  "sloc_min": 4,
3220                  "sloc_max": 4,
3221                  "cloc_min": 0,
3222                  "cloc_max": 0,
3223                  "ploc_min": 2,
3224                  "ploc_max": 2,
3225                  "lloc_min": 2,
3226                  "lloc_max": 2,
3227                  "blank_min": 2,
3228                  "blank_max": 2
3229                }
3230                "#
3231                );
3232            },
3233        );
3234    }
3235
3236    #[cfg(feature = "java")]
3237    #[test]
3238    fn java_sloc() {
3239        check_metrics::<JavaParser>(
3240            "for (int i = 0; i < 100; i++) {
3241               System.out.println(i);
3242             }",
3243            "foo.java",
3244            |metric| {
3245                // Spaces: 1
3246                insta::assert_json_snapshot!(
3247                    metric.loc,
3248                    @r#"
3249                {
3250                  "sloc": 3,
3251                  "ploc": 3,
3252                  "lloc": 2,
3253                  "cloc": 0,
3254                  "blank": 0,
3255                  "sloc_average": 3.0,
3256                  "ploc_average": 3.0,
3257                  "lloc_average": 2.0,
3258                  "cloc_average": 0.0,
3259                  "blank_average": 0.0,
3260                  "sloc_min": 3,
3261                  "sloc_max": 3,
3262                  "cloc_min": 0,
3263                  "cloc_max": 0,
3264                  "ploc_min": 3,
3265                  "ploc_max": 3,
3266                  "lloc_min": 2,
3267                  "lloc_max": 2,
3268                  "blank_min": 0,
3269                  "blank_max": 0
3270                }
3271                "#
3272                );
3273            },
3274        );
3275    }
3276
3277    #[cfg(feature = "java")]
3278    #[test]
3279    fn java_module_sloc() {
3280        check_metrics::<JavaParser>(
3281            "module helloworld{
3282              exports com.test;
3283            }",
3284            "foo.java",
3285            |metric| {
3286                // Spaces: 1
3287                insta::assert_json_snapshot!(
3288                    metric.loc,
3289                    @r#"
3290                {
3291                  "sloc": 3,
3292                  "ploc": 3,
3293                  "lloc": 0,
3294                  "cloc": 0,
3295                  "blank": 0,
3296                  "sloc_average": 3.0,
3297                  "ploc_average": 3.0,
3298                  "lloc_average": 0.0,
3299                  "cloc_average": 0.0,
3300                  "blank_average": 0.0,
3301                  "sloc_min": 3,
3302                  "sloc_max": 3,
3303                  "cloc_min": 0,
3304                  "cloc_max": 0,
3305                  "ploc_min": 3,
3306                  "ploc_max": 3,
3307                  "lloc_min": 0,
3308                  "lloc_max": 0,
3309                  "blank_min": 0,
3310                  "blank_max": 0
3311                }
3312                "#
3313                );
3314            },
3315        );
3316    }
3317
3318    #[cfg(feature = "java")]
3319    #[test]
3320    fn java_single_ploc() {
3321        check_metrics::<JavaParser>("int x = 1;", "foo.java", |metric| {
3322            // Spaces: 1
3323            insta::assert_json_snapshot!(
3324                metric.loc,
3325                @r#"
3326            {
3327              "sloc": 1,
3328              "ploc": 1,
3329              "lloc": 1,
3330              "cloc": 0,
3331              "blank": 0,
3332              "sloc_average": 1.0,
3333              "ploc_average": 1.0,
3334              "lloc_average": 1.0,
3335              "cloc_average": 0.0,
3336              "blank_average": 0.0,
3337              "sloc_min": 1,
3338              "sloc_max": 1,
3339              "cloc_min": 0,
3340              "cloc_max": 0,
3341              "ploc_min": 1,
3342              "ploc_max": 1,
3343              "lloc_min": 1,
3344              "lloc_max": 1,
3345              "blank_min": 0,
3346              "blank_max": 0
3347            }
3348            "#
3349            );
3350        });
3351    }
3352
3353    #[cfg(feature = "java")]
3354    #[test]
3355    fn java_simple_ploc() {
3356        check_metrics::<JavaParser>(
3357            "for (int i = 0; i < 100; i = i++) {
3358               System.out.println(i);
3359             }",
3360            "foo.java",
3361            |metric| {
3362                // Spaces: 1
3363                insta::assert_json_snapshot!(
3364                    metric.loc,
3365                    @r#"
3366                {
3367                  "sloc": 3,
3368                  "ploc": 3,
3369                  "lloc": 2,
3370                  "cloc": 0,
3371                  "blank": 0,
3372                  "sloc_average": 3.0,
3373                  "ploc_average": 3.0,
3374                  "lloc_average": 2.0,
3375                  "cloc_average": 0.0,
3376                  "blank_average": 0.0,
3377                  "sloc_min": 3,
3378                  "sloc_max": 3,
3379                  "cloc_min": 0,
3380                  "cloc_max": 0,
3381                  "ploc_min": 3,
3382                  "ploc_max": 3,
3383                  "lloc_min": 2,
3384                  "lloc_max": 2,
3385                  "blank_min": 0,
3386                  "blank_max": 0
3387                }
3388                "#
3389                );
3390            },
3391        );
3392    }
3393
3394    #[cfg(feature = "java")]
3395    #[test]
3396    fn java_multi_ploc() {
3397        check_metrics::<JavaParser>(
3398            "int x = 1;
3399            for (int i = 0; i < 100; i++) {
3400               System.out.println(i);
3401             }",
3402            "foo.java",
3403            |metric| {
3404                // Spaces: 1
3405                insta::assert_json_snapshot!(
3406                    metric.loc,
3407                    @r#"
3408                {
3409                  "sloc": 4,
3410                  "ploc": 4,
3411                  "lloc": 3,
3412                  "cloc": 0,
3413                  "blank": 0,
3414                  "sloc_average": 4.0,
3415                  "ploc_average": 4.0,
3416                  "lloc_average": 3.0,
3417                  "cloc_average": 0.0,
3418                  "blank_average": 0.0,
3419                  "sloc_min": 4,
3420                  "sloc_max": 4,
3421                  "cloc_min": 0,
3422                  "cloc_max": 0,
3423                  "ploc_min": 4,
3424                  "ploc_max": 4,
3425                  "lloc_min": 3,
3426                  "lloc_max": 3,
3427                  "blank_min": 0,
3428                  "blank_max": 0
3429                }
3430                "#
3431                );
3432            },
3433        );
3434    }
3435
3436    #[cfg(feature = "java")]
3437    #[test]
3438    fn java_single_statement_lloc() {
3439        check_metrics::<JavaParser>("int max = 10;", "foo.java", |metric| {
3440            // Spaces: 1
3441            insta::assert_json_snapshot!(
3442                metric.loc,
3443                @r#"
3444            {
3445              "sloc": 1,
3446              "ploc": 1,
3447              "lloc": 1,
3448              "cloc": 0,
3449              "blank": 0,
3450              "sloc_average": 1.0,
3451              "ploc_average": 1.0,
3452              "lloc_average": 1.0,
3453              "cloc_average": 0.0,
3454              "blank_average": 0.0,
3455              "sloc_min": 1,
3456              "sloc_max": 1,
3457              "cloc_min": 0,
3458              "cloc_max": 0,
3459              "ploc_min": 1,
3460              "ploc_max": 1,
3461              "lloc_min": 1,
3462              "lloc_max": 1,
3463              "blank_min": 0,
3464              "blank_max": 0
3465            }
3466            "#
3467            );
3468        });
3469    }
3470
3471    #[cfg(feature = "java")]
3472    #[test]
3473    fn java_for_lloc() {
3474        check_metrics::<JavaParser>(
3475            "for (int i = 0; i < 100; i++) { // + 1
3476               System.out.println(i); // + 1
3477             }",
3478            "foo.java",
3479            |metric| {
3480                // Spaces: 1
3481                insta::assert_json_snapshot!(
3482                    metric.loc,
3483                    @r#"
3484                {
3485                  "sloc": 3,
3486                  "ploc": 3,
3487                  "lloc": 2,
3488                  "cloc": 2,
3489                  "blank": 0,
3490                  "sloc_average": 3.0,
3491                  "ploc_average": 3.0,
3492                  "lloc_average": 2.0,
3493                  "cloc_average": 2.0,
3494                  "blank_average": 0.0,
3495                  "sloc_min": 3,
3496                  "sloc_max": 3,
3497                  "cloc_min": 2,
3498                  "cloc_max": 2,
3499                  "ploc_min": 3,
3500                  "ploc_max": 3,
3501                  "lloc_min": 2,
3502                  "lloc_max": 2,
3503                  "blank_min": 0,
3504                  "blank_max": 0
3505                }
3506                "#
3507                );
3508            },
3509        );
3510    }
3511
3512    #[cfg(feature = "java")]
3513    #[test]
3514    fn java_foreach_lloc() {
3515        check_metrics::<JavaParser>(
3516            "
3517            int arr[]={12,13,14,44}; // +1
3518            for (int i:arr) { // +1
3519               System.out.println(i); // +1
3520             }",
3521            "foo.java",
3522            |metric| {
3523                // Spaces: 1
3524                insta::assert_json_snapshot!(
3525                    metric.loc,
3526                    @r#"
3527                {
3528                  "sloc": 4,
3529                  "ploc": 4,
3530                  "lloc": 3,
3531                  "cloc": 3,
3532                  "blank": 0,
3533                  "sloc_average": 4.0,
3534                  "ploc_average": 4.0,
3535                  "lloc_average": 3.0,
3536                  "cloc_average": 3.0,
3537                  "blank_average": 0.0,
3538                  "sloc_min": 4,
3539                  "sloc_max": 4,
3540                  "cloc_min": 3,
3541                  "cloc_max": 3,
3542                  "ploc_min": 4,
3543                  "ploc_max": 4,
3544                  "lloc_min": 3,
3545                  "lloc_max": 3,
3546                  "blank_min": 0,
3547                  "blank_max": 0
3548                }
3549                "#
3550                );
3551            },
3552        );
3553    }
3554
3555    #[cfg(feature = "java")]
3556    #[test]
3557    fn java_while_lloc() {
3558        check_metrics::<JavaParser>(
3559            "
3560            int i=0; // +1
3561            while(i < 10) { // +1
3562                i++; // +1
3563                System.out.println(i); // +1
3564             }",
3565            "foo.java",
3566            |metric| {
3567                // Spaces: 1
3568                insta::assert_json_snapshot!(
3569                    metric.loc,
3570                    @r#"
3571                {
3572                  "sloc": 5,
3573                  "ploc": 5,
3574                  "lloc": 4,
3575                  "cloc": 4,
3576                  "blank": 0,
3577                  "sloc_average": 5.0,
3578                  "ploc_average": 5.0,
3579                  "lloc_average": 4.0,
3580                  "cloc_average": 4.0,
3581                  "blank_average": 0.0,
3582                  "sloc_min": 5,
3583                  "sloc_max": 5,
3584                  "cloc_min": 4,
3585                  "cloc_max": 4,
3586                  "ploc_min": 5,
3587                  "ploc_max": 5,
3588                  "lloc_min": 4,
3589                  "lloc_max": 4,
3590                  "blank_min": 0,
3591                  "blank_max": 0
3592                }
3593                "#
3594                );
3595            },
3596        );
3597    }
3598
3599    #[cfg(feature = "java")]
3600    #[test]
3601    fn java_do_while_lloc() {
3602        check_metrics::<JavaParser>(
3603            "
3604            int i=0; // +1
3605            do { // +1
3606                i++; // +1
3607                System.out.println(i); // +1
3608             } while(i < 10)",
3609            "foo.java",
3610            |metric| {
3611                // Spaces: 1
3612                insta::assert_json_snapshot!(
3613                    metric.loc,
3614                    @r#"
3615                {
3616                  "sloc": 5,
3617                  "ploc": 5,
3618                  "lloc": 4,
3619                  "cloc": 4,
3620                  "blank": 0,
3621                  "sloc_average": 5.0,
3622                  "ploc_average": 5.0,
3623                  "lloc_average": 4.0,
3624                  "cloc_average": 4.0,
3625                  "blank_average": 0.0,
3626                  "sloc_min": 5,
3627                  "sloc_max": 5,
3628                  "cloc_min": 4,
3629                  "cloc_max": 4,
3630                  "ploc_min": 5,
3631                  "ploc_max": 5,
3632                  "lloc_min": 4,
3633                  "lloc_max": 4,
3634                  "blank_min": 0,
3635                  "blank_max": 0
3636                }
3637                "#
3638                );
3639            },
3640        );
3641    }
3642
3643    #[cfg(feature = "java")]
3644    #[test]
3645    fn java_switch_lloc() {
3646        check_metrics::<JavaParser>(
3647            "switch(grade) { // +1
3648                case 'A' :
3649                   System.out.println(\"Pass with distinction\"); // +1
3650                   break; // +1
3651                case 'B' :
3652                case 'C' :
3653                   System.out.println(\"Pass\"); // +1
3654                   break; // +1
3655                case 'D' :
3656                   System.out.println(\"At risk\"); // +1
3657                case 'F' :
3658                   System.out.println(\"Fail\"); // +1
3659                   break; // +1
3660                default :
3661                   System.out.println(\"Invalid grade\"); // +1
3662             }",
3663            "foo.java",
3664            |metric| {
3665                // Spaces: 1
3666                insta::assert_json_snapshot!(
3667                    metric.loc,
3668                    @r#"
3669                {
3670                  "sloc": 16,
3671                  "ploc": 16,
3672                  "lloc": 9,
3673                  "cloc": 9,
3674                  "blank": 0,
3675                  "sloc_average": 16.0,
3676                  "ploc_average": 16.0,
3677                  "lloc_average": 9.0,
3678                  "cloc_average": 9.0,
3679                  "blank_average": 0.0,
3680                  "sloc_min": 16,
3681                  "sloc_max": 16,
3682                  "cloc_min": 9,
3683                  "cloc_max": 9,
3684                  "ploc_min": 16,
3685                  "ploc_max": 16,
3686                  "lloc_min": 9,
3687                  "lloc_max": 9,
3688                  "blank_min": 0,
3689                  "blank_max": 0
3690                }
3691                "#
3692                );
3693            },
3694        );
3695    }
3696
3697    #[cfg(feature = "java")]
3698    #[test]
3699    fn java_continue_lloc() {
3700        check_metrics::<JavaParser>(
3701            "int max = 10; // +1
3702
3703            for (int i = 0; i < max; i++) { // +1
3704                if(i % 2 == 0) { continue;} + 2
3705                System.out.println(i); // +1
3706             }",
3707            "foo.java",
3708            |metric| {
3709                // Spaces: 1
3710                insta::assert_json_snapshot!(
3711                    metric.loc,
3712                    @r#"
3713                {
3714                  "sloc": 6,
3715                  "ploc": 5,
3716                  "lloc": 5,
3717                  "cloc": 3,
3718                  "blank": 1,
3719                  "sloc_average": 6.0,
3720                  "ploc_average": 5.0,
3721                  "lloc_average": 5.0,
3722                  "cloc_average": 3.0,
3723                  "blank_average": 1.0,
3724                  "sloc_min": 6,
3725                  "sloc_max": 6,
3726                  "cloc_min": 3,
3727                  "cloc_max": 3,
3728                  "ploc_min": 5,
3729                  "ploc_max": 5,
3730                  "lloc_min": 5,
3731                  "lloc_max": 5,
3732                  "blank_min": 1,
3733                  "blank_max": 1
3734                }
3735                "#
3736                );
3737            },
3738        );
3739    }
3740
3741    #[cfg(feature = "java")]
3742    #[test]
3743    fn java_try_lloc() {
3744        check_metrics::<JavaParser>(
3745            "try { // +1
3746                int[] myNumbers = {1, 2, 3}; // +1
3747                System.out.println(myNumbers[10]); // +1
3748              } catch (Exception e) {
3749                System.out.println(e.getMessage()); // +1
3750                throw e; // +1
3751              }",
3752            "foo.java",
3753            |metric| {
3754                // Spaces: 1
3755                insta::assert_json_snapshot!(
3756                    metric.loc,
3757                    @r#"
3758                {
3759                  "sloc": 7,
3760                  "ploc": 7,
3761                  "lloc": 5,
3762                  "cloc": 5,
3763                  "blank": 0,
3764                  "sloc_average": 7.0,
3765                  "ploc_average": 7.0,
3766                  "lloc_average": 5.0,
3767                  "cloc_average": 5.0,
3768                  "blank_average": 0.0,
3769                  "sloc_min": 7,
3770                  "sloc_max": 7,
3771                  "cloc_min": 5,
3772                  "cloc_max": 5,
3773                  "ploc_min": 7,
3774                  "ploc_max": 7,
3775                  "lloc_min": 5,
3776                  "lloc_max": 5,
3777                  "blank_min": 0,
3778                  "blank_max": 0
3779                }
3780                "#
3781                );
3782            },
3783        );
3784    }
3785
3786    #[cfg(feature = "java")]
3787    #[test]
3788    fn java_class_loc() {
3789        check_metrics::<JavaParser>(
3790            "
3791            public class Person {
3792              private String name;
3793              public Person(String name){
3794                this.name = name; // +1
3795              }
3796              public String getName() {
3797                return name; // +1
3798              }
3799            }",
3800            "foo.java",
3801            |metric| {
3802                // Spaces: 4
3803                insta::assert_json_snapshot!(
3804                    metric.loc,
3805                    @r#"
3806                {
3807                  "sloc": 9,
3808                  "ploc": 9,
3809                  "lloc": 2,
3810                  "cloc": 2,
3811                  "blank": 0,
3812                  "sloc_average": 2.25,
3813                  "ploc_average": 2.25,
3814                  "lloc_average": 0.5,
3815                  "cloc_average": 0.5,
3816                  "blank_average": 0.0,
3817                  "sloc_min": 3,
3818                  "sloc_max": 9,
3819                  "cloc_min": 1,
3820                  "cloc_max": 2,
3821                  "ploc_min": 3,
3822                  "ploc_max": 9,
3823                  "lloc_min": 1,
3824                  "lloc_max": 2,
3825                  "blank_min": 0,
3826                  "blank_max": 0
3827                }
3828                "#
3829                );
3830            },
3831        );
3832    }
3833
3834    #[cfg(feature = "java")]
3835    #[test]
3836    fn java_expressions_lloc() {
3837        check_metrics::<JavaParser>(
3838            "int x = 10;                                                            // +1 local var declaration
3839            x=+89;                                                                  // +1 expression statement
3840            int y = x * 2;                                                          // +1 local var declaration
3841            IntFunction double = (n) -> n*2;                                        // +1 local var declaration
3842            int y2 = double(x);                                                     // +1 local var declaration
3843            System.out.println(\"double \" + x + \" = \" + y2);                     // +1 expression statement
3844            String message = (x % 2) == 0 ? \"Evenly done.\" : \"Oddly done.\";     // +1 local var declaration
3845            Object done = (Runnable) () -> { System.out.println(\"Done!\"); };      // +2 local var declaration + expression statement
3846            String s = \"string\";                                                  // +1 local var declaration
3847            boolean isS = (s instanceof String);                                    // +1 local var declaration
3848            done.run();                                                             // +1 expression statement
3849            ",
3850            "foo.java",
3851            |metric| {
3852                // Spaces: 1
3853                insta::assert_json_snapshot!(
3854                    metric.loc,
3855                    @r#"
3856                {
3857                  "sloc": 11,
3858                  "ploc": 11,
3859                  "lloc": 12,
3860                  "cloc": 11,
3861                  "blank": 0,
3862                  "sloc_average": 11.0,
3863                  "ploc_average": 11.0,
3864                  "lloc_average": 12.0,
3865                  "cloc_average": 11.0,
3866                  "blank_average": 0.0,
3867                  "sloc_min": 11,
3868                  "sloc_max": 11,
3869                  "cloc_min": 11,
3870                  "cloc_max": 11,
3871                  "ploc_min": 11,
3872                  "ploc_max": 11,
3873                  "lloc_min": 12,
3874                  "lloc_max": 12,
3875                  "blank_min": 0,
3876                  "blank_max": 0
3877                }
3878                "#
3879                );
3880            },
3881        );
3882    }
3883
3884    #[cfg(feature = "java")]
3885    #[test]
3886    fn java_statement_inline_loc() {
3887        check_metrics::<JavaParser>(
3888            "for (int i = 0; i < 100; i++) { System.out.println(\"hello\"); }",
3889            "foo.java",
3890            |metric| {
3891                // Spaces: 1
3892                insta::assert_json_snapshot!(
3893                    metric.loc,
3894                    @r#"
3895                {
3896                  "sloc": 1,
3897                  "ploc": 1,
3898                  "lloc": 2,
3899                  "cloc": 0,
3900                  "blank": 0,
3901                  "sloc_average": 1.0,
3902                  "ploc_average": 1.0,
3903                  "lloc_average": 2.0,
3904                  "cloc_average": 0.0,
3905                  "blank_average": 0.0,
3906                  "sloc_min": 1,
3907                  "sloc_max": 1,
3908                  "cloc_min": 0,
3909                  "cloc_max": 0,
3910                  "ploc_min": 1,
3911                  "ploc_max": 1,
3912                  "lloc_min": 2,
3913                  "lloc_max": 2,
3914                  "blank_min": 0,
3915                  "blank_max": 0
3916                }
3917                "#
3918                );
3919            },
3920        );
3921    }
3922
3923    #[cfg(feature = "java")]
3924    #[test]
3925    fn java_general_loc() {
3926        check_metrics::<JavaParser>(
3927            "int max = 100;
3928
3929            /*
3930              Loop through and print
3931                from: 0
3932                to: max
3933            */
3934            for (int i = 0; i < max; i++) {
3935               // Print the value
3936               System.out.println(i);
3937             }",
3938            "foo.java",
3939            |metric| {
3940                // Spaces: 1
3941                insta::assert_json_snapshot!(
3942                    metric.loc,
3943                    @r#"
3944                {
3945                  "sloc": 11,
3946                  "ploc": 4,
3947                  "lloc": 3,
3948                  "cloc": 6,
3949                  "blank": 1,
3950                  "sloc_average": 11.0,
3951                  "ploc_average": 4.0,
3952                  "lloc_average": 3.0,
3953                  "cloc_average": 6.0,
3954                  "blank_average": 1.0,
3955                  "sloc_min": 11,
3956                  "sloc_max": 11,
3957                  "cloc_min": 6,
3958                  "cloc_max": 6,
3959                  "ploc_min": 4,
3960                  "ploc_max": 4,
3961                  "lloc_min": 3,
3962                  "lloc_max": 3,
3963                  "blank_min": 1,
3964                  "blank_max": 1
3965                }
3966                "#
3967                );
3968            },
3969        );
3970    }
3971
3972    #[cfg(feature = "java")]
3973    #[test]
3974    fn java_main_class_loc() {
3975        check_metrics::<JavaParser>(
3976            "package com.company;
3977             /**
3978             * The HelloWorldApp class implements an application that
3979             * simply prints \"Hello World!\" to standard output.
3980             */
3981
3982            class HelloWorldApp {
3983              public void main(String[] args) {
3984                String message = args.length == 0 ? \"Hello empty world\" : \"Hello world\"; // +1 lloc : 1 var assignment
3985                System.out.println(message); // Display the string. +1 lloc
3986              }
3987            }",
3988            "foo.java",
3989            |metric| {
3990                // Spaces: 3
3991                insta::assert_json_snapshot!(
3992                    metric.loc,
3993                    @r#"
3994                {
3995                  "sloc": 12,
3996                  "ploc": 7,
3997                  "lloc": 2,
3998                  "cloc": 6,
3999                  "blank": 1,
4000                  "sloc_average": 4.0,
4001                  "ploc_average": 2.3333333333333335,
4002                  "lloc_average": 0.6666666666666666,
4003                  "cloc_average": 2.0,
4004                  "blank_average": 0.3333333333333333,
4005                  "sloc_min": 4,
4006                  "sloc_max": 12,
4007                  "cloc_min": 2,
4008                  "cloc_max": 6,
4009                  "ploc_min": 4,
4010                  "ploc_max": 7,
4011                  "lloc_min": 2,
4012                  "lloc_max": 2,
4013                  "blank_min": 0,
4014                  "blank_max": 1
4015                }
4016                "#
4017                );
4018            },
4019        );
4020    }
4021
4022    #[cfg(feature = "go")]
4023    #[test]
4024    fn go_general_loc() {
4025        check_metrics::<GoParser>(
4026            "package main
4027
4028            // entrypoint
4029            func main() {
4030                /* loop body */
4031                for i := 0; i < 10; i++ {
4032                    fmt.Println(i)
4033                }
4034            }",
4035            "foo.go",
4036            |metric| {
4037                // Spaces: 2 (unit + main).
4038                // lloc: for_statement (+1), fmt.Println expression (+1).
4039                //       `i := 0` and `i++` inside the for-clause are gated.
4040                // cloc: 2 comments (line + block).
4041                insta::assert_json_snapshot!(
4042                    metric.loc,
4043                    @r#"
4044                {
4045                  "sloc": 9,
4046                  "ploc": 6,
4047                  "lloc": 2,
4048                  "cloc": 2,
4049                  "blank": 1,
4050                  "sloc_average": 4.5,
4051                  "ploc_average": 3.0,
4052                  "lloc_average": 1.0,
4053                  "cloc_average": 1.0,
4054                  "blank_average": 0.5,
4055                  "sloc_min": 6,
4056                  "sloc_max": 9,
4057                  "cloc_min": 1,
4058                  "cloc_max": 2,
4059                  "ploc_min": 5,
4060                  "ploc_max": 6,
4061                  "lloc_min": 2,
4062                  "lloc_max": 2,
4063                  "blank_min": 0,
4064                  "blank_max": 1
4065                }
4066                "#
4067                );
4068            },
4069        );
4070    }
4071
4072    #[cfg(feature = "go")]
4073    #[test]
4074    fn go_for_clause_does_not_double_count_lloc() {
4075        // Bare `for` body has only a return; the `for_statement` itself is the
4076        // single logical line. Confirms ShortVarDeclaration in a for-clause
4077        // does not add an extra lloc.
4078        check_metrics::<GoParser>(
4079            "package main
4080            func f(n int) int {
4081                for i := 0; i < n; i++ {
4082                    return i
4083                }
4084                return 0
4085            }",
4086            "foo.go",
4087            |metric| {
4088                // Expected lloc: for (+1), return (+1), return (+1) = 3.
4089                // Without the gate, ShortVarDeclaration would add an extra (+1).
4090                assert_eq!(metric.loc.lloc(), 3);
4091            },
4092        );
4093    }
4094
4095    #[cfg(feature = "go")]
4096    #[test]
4097    fn go_blank() {
4098        check_metrics::<GoParser>(
4099            "package main
4100
4101            func foo() {
4102                x := 1
4103
4104                y := 2
4105            }",
4106            "foo.go",
4107            |metric| {
4108                // Spaces: 2 (unit + foo).
4109                // blank: 2 (lines 2 and 5 are empty).
4110                insta::assert_json_snapshot!(
4111                    metric.loc,
4112                    @r#"
4113                {
4114                  "sloc": 7,
4115                  "ploc": 5,
4116                  "lloc": 2,
4117                  "cloc": 0,
4118                  "blank": 2,
4119                  "sloc_average": 3.5,
4120                  "ploc_average": 2.5,
4121                  "lloc_average": 1.0,
4122                  "cloc_average": 0.0,
4123                  "blank_average": 1.0,
4124                  "sloc_min": 5,
4125                  "sloc_max": 7,
4126                  "cloc_min": 0,
4127                  "cloc_max": 0,
4128                  "ploc_min": 4,
4129                  "ploc_max": 5,
4130                  "lloc_min": 2,
4131                  "lloc_max": 2,
4132                  "blank_min": 1,
4133                  "blank_max": 2
4134                }
4135                "#
4136                );
4137            },
4138        );
4139    }
4140
4141    #[cfg(feature = "go")]
4142    #[test]
4143    fn go_cloc_line_comments() {
4144        check_metrics::<GoParser>(
4145            "package main
4146
4147            // helper adds two numbers.
4148            // It returns their sum.
4149            func add(a, b int) int {
4150                // compute the result
4151                return a + b
4152            }",
4153            "foo.go",
4154            |metric| {
4155                // Spaces: 2 (unit + add).
4156                // cloc: 3 lines with `//` comments.
4157                insta::assert_json_snapshot!(
4158                    metric.loc,
4159                    @r#"
4160                {
4161                  "sloc": 8,
4162                  "ploc": 4,
4163                  "lloc": 1,
4164                  "cloc": 3,
4165                  "blank": 1,
4166                  "sloc_average": 4.0,
4167                  "ploc_average": 2.0,
4168                  "lloc_average": 0.5,
4169                  "cloc_average": 1.5,
4170                  "blank_average": 0.5,
4171                  "sloc_min": 4,
4172                  "sloc_max": 8,
4173                  "cloc_min": 1,
4174                  "cloc_max": 3,
4175                  "ploc_min": 3,
4176                  "ploc_max": 4,
4177                  "lloc_min": 1,
4178                  "lloc_max": 1,
4179                  "blank_min": 0,
4180                  "blank_max": 1
4181                }
4182                "#
4183                );
4184            },
4185        );
4186    }
4187
4188    #[cfg(feature = "go")]
4189    #[test]
4190    fn go_cloc_block_comments() {
4191        check_metrics::<GoParser>(
4192            "package main
4193
4194            /* block comment
4195               spanning two lines */
4196            func foo() {
4197                x := 1 /* inline block */
4198            }",
4199            "foo.go",
4200            |metric| {
4201                // Spaces: 2 (unit + foo).
4202                // cloc: 2-line block comment + inline block = 3 comment lines.
4203                insta::assert_json_snapshot!(
4204                    metric.loc,
4205                    @r#"
4206                {
4207                  "sloc": 7,
4208                  "ploc": 4,
4209                  "lloc": 1,
4210                  "cloc": 3,
4211                  "blank": 1,
4212                  "sloc_average": 3.5,
4213                  "ploc_average": 2.0,
4214                  "lloc_average": 0.5,
4215                  "cloc_average": 1.5,
4216                  "blank_average": 0.5,
4217                  "sloc_min": 3,
4218                  "sloc_max": 7,
4219                  "cloc_min": 1,
4220                  "cloc_max": 3,
4221                  "ploc_min": 3,
4222                  "ploc_max": 4,
4223                  "lloc_min": 1,
4224                  "lloc_max": 1,
4225                  "blank_min": 0,
4226                  "blank_max": 1
4227                }
4228                "#
4229                );
4230            },
4231        );
4232    }
4233
4234    #[cfg(feature = "go")]
4235    #[test]
4236    fn go_lloc_if_for_switch() {
4237        check_metrics::<GoParser>(
4238            "package main
4239
4240            func foo(n int) int {
4241                if n > 0 {
4242                    for i := 0; i < n; i++ {
4243                        switch i {
4244                        }
4245                    }
4246                }
4247                return n
4248            }",
4249            "foo.go",
4250            |metric| {
4251                // Spaces: 2 (unit + foo).
4252                // lloc: if (+1), for (+1), switch (+1), return (+1) = 4.
4253                insta::assert_json_snapshot!(
4254                    metric.loc,
4255                    @r#"
4256                {
4257                  "sloc": 11,
4258                  "ploc": 10,
4259                  "lloc": 4,
4260                  "cloc": 0,
4261                  "blank": 1,
4262                  "sloc_average": 5.5,
4263                  "ploc_average": 5.0,
4264                  "lloc_average": 2.0,
4265                  "cloc_average": 0.0,
4266                  "blank_average": 0.5,
4267                  "sloc_min": 9,
4268                  "sloc_max": 11,
4269                  "cloc_min": 0,
4270                  "cloc_max": 0,
4271                  "ploc_min": 9,
4272                  "ploc_max": 10,
4273                  "lloc_min": 4,
4274                  "lloc_max": 4,
4275                  "blank_min": 0,
4276                  "blank_max": 1
4277                }
4278                "#
4279                );
4280            },
4281        );
4282    }
4283
4284    #[cfg(feature = "go")]
4285    #[test]
4286    fn go_lloc_go_defer() {
4287        check_metrics::<GoParser>(
4288            "package main
4289
4290            func foo() {
4291                go run()
4292                defer cleanup()
4293            }",
4294            "foo.go",
4295            |metric| {
4296                // Spaces: 2 (unit + foo).
4297                // lloc: go (+1), defer (+1) = 2.
4298                insta::assert_json_snapshot!(
4299                    metric.loc,
4300                    @r#"
4301                {
4302                  "sloc": 6,
4303                  "ploc": 5,
4304                  "lloc": 2,
4305                  "cloc": 0,
4306                  "blank": 1,
4307                  "sloc_average": 3.0,
4308                  "ploc_average": 2.5,
4309                  "lloc_average": 1.0,
4310                  "cloc_average": 0.0,
4311                  "blank_average": 0.5,
4312                  "sloc_min": 4,
4313                  "sloc_max": 6,
4314                  "cloc_min": 0,
4315                  "cloc_max": 0,
4316                  "ploc_min": 4,
4317                  "ploc_max": 5,
4318                  "lloc_min": 2,
4319                  "lloc_max": 2,
4320                  "blank_min": 0,
4321                  "blank_max": 1
4322                }
4323                "#
4324                );
4325            },
4326        );
4327    }
4328
4329    #[cfg(feature = "go")]
4330    #[test]
4331    fn go_lloc_var_const_declarations() {
4332        check_metrics::<GoParser>(
4333            "package main
4334
4335            func foo() {
4336                var x int
4337                var y = 10
4338                const z = 42
4339                a := 3
4340                a = 4
4341            }",
4342            "foo.go",
4343            |metric| {
4344                // Spaces: 2 (unit + foo).
4345                // lloc: var (+1), var (+1), const (+1),
4346                //       short_var_decl (+1), assignment (+1) = 5.
4347                insta::assert_json_snapshot!(
4348                    metric.loc,
4349                    @r#"
4350                {
4351                  "sloc": 9,
4352                  "ploc": 8,
4353                  "lloc": 5,
4354                  "cloc": 0,
4355                  "blank": 1,
4356                  "sloc_average": 4.5,
4357                  "ploc_average": 4.0,
4358                  "lloc_average": 2.5,
4359                  "cloc_average": 0.0,
4360                  "blank_average": 0.5,
4361                  "sloc_min": 7,
4362                  "sloc_max": 9,
4363                  "cloc_min": 0,
4364                  "cloc_max": 0,
4365                  "ploc_min": 7,
4366                  "ploc_max": 8,
4367                  "lloc_min": 5,
4368                  "lloc_max": 5,
4369                  "blank_min": 0,
4370                  "blank_max": 1
4371                }
4372                "#
4373                );
4374            },
4375        );
4376    }
4377
4378    #[cfg(feature = "go")]
4379    #[test]
4380    fn go_lloc_select() {
4381        check_metrics::<GoParser>(
4382            "package main
4383
4384            func foo(ch chan int) {
4385                select {
4386                case v := <-ch:
4387                    _ = v
4388                }
4389            }",
4390            "foo.go",
4391            |metric| {
4392                // Spaces: 2 (unit + foo).
4393                // lloc: select (+1), assignment `_ = v` (+1) = 2.
4394                // `case v := <-ch:` is a receive_statement inside a
4395                // communication_case, not a ShortVarDeclaration.
4396                insta::assert_json_snapshot!(
4397                    metric.loc,
4398                    @r#"
4399                {
4400                  "sloc": 8,
4401                  "ploc": 7,
4402                  "lloc": 2,
4403                  "cloc": 0,
4404                  "blank": 1,
4405                  "sloc_average": 4.0,
4406                  "ploc_average": 3.5,
4407                  "lloc_average": 1.0,
4408                  "cloc_average": 0.0,
4409                  "blank_average": 0.5,
4410                  "sloc_min": 6,
4411                  "sloc_max": 8,
4412                  "cloc_min": 0,
4413                  "cloc_max": 0,
4414                  "ploc_min": 6,
4415                  "ploc_max": 7,
4416                  "lloc_min": 2,
4417                  "lloc_max": 2,
4418                  "blank_min": 0,
4419                  "blank_max": 1
4420                }
4421                "#
4422                );
4423            },
4424        );
4425    }
4426
4427    #[cfg(feature = "go")]
4428    #[test]
4429    fn go_sloc_multiline_function() {
4430        check_metrics::<GoParser>(
4431            "package main
4432
4433            func add(
4434                a int,
4435                b int,
4436            ) int {
4437                return a + b
4438            }",
4439            "foo.go",
4440            |metric| {
4441                // Spaces: 2 (unit + add).
4442                // The multi-line signature should count each line as sloc.
4443                insta::assert_json_snapshot!(
4444                    metric.loc,
4445                    @r#"
4446                {
4447                  "sloc": 8,
4448                  "ploc": 7,
4449                  "lloc": 1,
4450                  "cloc": 0,
4451                  "blank": 1,
4452                  "sloc_average": 4.0,
4453                  "ploc_average": 3.5,
4454                  "lloc_average": 0.5,
4455                  "cloc_average": 0.0,
4456                  "blank_average": 0.5,
4457                  "sloc_min": 6,
4458                  "sloc_max": 8,
4459                  "cloc_min": 0,
4460                  "cloc_max": 0,
4461                  "ploc_min": 6,
4462                  "ploc_max": 7,
4463                  "lloc_min": 1,
4464                  "lloc_max": 1,
4465                  "blank_min": 0,
4466                  "blank_max": 1
4467                }
4468                "#
4469                );
4470            },
4471        );
4472    }
4473
4474    #[cfg(feature = "go")]
4475    #[test]
4476    fn go_code_comment_same_line() {
4477        check_metrics::<GoParser>(
4478            "package main
4479
4480            func foo() {
4481                x := 1 // initialize x
4482                y := 2 // initialize y
4483            }",
4484            "foo.go",
4485            |metric| {
4486                // Spaces: 2 (unit + foo).
4487                // cloc: 2 (inline comments on code lines).
4488                // blank: 1 (line between package and func).
4489                // The code+comment lines should count for both ploc and cloc.
4490                insta::assert_json_snapshot!(
4491                    metric.loc,
4492                    @r#"
4493                {
4494                  "sloc": 6,
4495                  "ploc": 5,
4496                  "lloc": 2,
4497                  "cloc": 2,
4498                  "blank": 1,
4499                  "sloc_average": 3.0,
4500                  "ploc_average": 2.5,
4501                  "lloc_average": 1.0,
4502                  "cloc_average": 1.0,
4503                  "blank_average": 0.5,
4504                  "sloc_min": 4,
4505                  "sloc_max": 6,
4506                  "cloc_min": 2,
4507                  "cloc_max": 2,
4508                  "ploc_min": 4,
4509                  "ploc_max": 5,
4510                  "lloc_min": 2,
4511                  "lloc_max": 2,
4512                  "blank_min": 0,
4513                  "blank_max": 1
4514                }
4515                "#
4516                );
4517            },
4518        );
4519    }
4520
4521    #[cfg(feature = "perl")]
4522    #[test]
4523    fn perl_grammar_smoke() {
4524        // Pin the contract that tree-sitter-perl 1.1.2 cleanly parses every
4525        // Perl construct exercised by the rest of the `perl_*` test suite.
4526        // If a future grammar bump turns one of these into an error tree,
4527        // the metric assertions might still pass numerically by coincidence;
4528        // this test fails loudly instead.
4529        assert_perl_parses_cleanly(
4530            "use strict;
4531use warnings;
4532
4533# line comment
4534
4535=pod
4536multi-line POD
4537=cut
4538
4539sub factorial {
4540    my ($n) = @_;
4541    return 1 if $n <= 1;
4542    return $n * factorial($n - 1);
4543}
4544
4545my @arr = (1, 2, 3);
4546my %hash = (a => 1, b => 2);
4547my $closure = sub { return $_[0] + 1; };
4548
4549for my $i (1..3) {
4550    if ($i % 2 == 0) {
4551        print \"even\\n\";
4552    } elsif ($i == 1) {
4553        print \"one\\n\";
4554    } else {
4555        print \"odd\\n\";
4556    }
4557}
4558
4559while ($x > 0) {
4560    last if $x == 0;
4561    $x--;
4562}
4563
4564unless ($done) {
4565    next;
4566}
4567
4568my $heredoc = <<END;
4569hello
4570END
4571",
4572        );
4573    }
4574
4575    #[cfg(feature = "perl")]
4576    #[test]
4577    fn perl_blank() {
4578        check_metrics::<PerlParser>(
4579            "
4580
4581my $a = 42;
4582
4583my $b = 43;
4584
4585",
4586            "foo.pl",
4587            |metric| {
4588                insta::assert_json_snapshot!(metric.loc, @r#"
4589                {
4590                  "sloc": 3,
4591                  "ploc": 2,
4592                  "lloc": 2,
4593                  "cloc": 0,
4594                  "blank": 1,
4595                  "sloc_average": 3.0,
4596                  "ploc_average": 2.0,
4597                  "lloc_average": 2.0,
4598                  "cloc_average": 0.0,
4599                  "blank_average": 1.0,
4600                  "sloc_min": 3,
4601                  "sloc_max": 3,
4602                  "cloc_min": 0,
4603                  "cloc_max": 0,
4604                  "ploc_min": 2,
4605                  "ploc_max": 2,
4606                  "lloc_min": 2,
4607                  "lloc_max": 2,
4608                  "blank_min": 1,
4609                  "blank_max": 1
4610                }
4611                "#);
4612            },
4613        );
4614    }
4615
4616    #[cfg(feature = "perl")]
4617    #[test]
4618    fn perl_no_zero_blank() {
4619        // Blank line interleaved with code that carries trailing comments —
4620        // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
4621        check_metrics::<PerlParser>(
4622            "my $a = 1;
4623my $b = 2;
4624
4625my $c = 3; # trailing
4626my $d = 4; # trailing
4627my $e = 5;",
4628            "foo.pl",
4629            |metric| {
4630                assert_eq!(metric.loc.sloc(), 6);
4631                assert_eq!(metric.loc.ploc(), 5);
4632                assert_eq!(metric.loc.cloc(), 2);
4633                assert_eq!(metric.loc.blank(), 1);
4634                insta::assert_json_snapshot!(metric.loc);
4635            },
4636        );
4637    }
4638
4639    #[cfg(feature = "perl")]
4640    #[test]
4641    fn perl_blank_zero_sanity() {
4642        // Sanity check: blank must report 0, never go negative, when the
4643        // input has no blank lines.
4644        check_metrics::<PerlParser>(
4645            "my $a = 1;
4646my $b = 2;",
4647            "foo.pl",
4648            |metric| {
4649                assert_eq!(metric.loc.sloc(), 2);
4650                assert_eq!(metric.loc.ploc(), 2);
4651                assert_eq!(metric.loc.lloc(), 2);
4652                assert_eq!(metric.loc.cloc(), 0);
4653                assert_eq!(metric.loc.blank(), 0);
4654            },
4655        );
4656    }
4657
4658    /// expected: row 0 is comment-only, row 1 is code carrying a trailing
4659    /// comment, row 2 is code — so `ploc 2` and `cloc 2`, with row 1 in
4660    /// both tallies. This pinned `ploc 3` until #1137: the `#` token
4661    /// inside the `comments` node reached the PLOC catch-all, which also
4662    /// reclassified row 0 from comment-only to code-and-comment.
4663    #[cfg(feature = "perl")]
4664    #[test]
4665    fn perl_cloc_line_comments() {
4666        check_metrics::<PerlParser>(
4667            "# top comment
4668my $a = 1; # trailing
4669my $b = 2;",
4670            "foo.pl",
4671            |metric| {
4672                insta::assert_json_snapshot!(metric.loc, @r#"
4673                {
4674                  "sloc": 3,
4675                  "ploc": 2,
4676                  "lloc": 2,
4677                  "cloc": 2,
4678                  "blank": 0,
4679                  "sloc_average": 3.0,
4680                  "ploc_average": 2.0,
4681                  "lloc_average": 2.0,
4682                  "cloc_average": 2.0,
4683                  "blank_average": 0.0,
4684                  "sloc_min": 3,
4685                  "sloc_max": 3,
4686                  "cloc_min": 2,
4687                  "cloc_max": 2,
4688                  "ploc_min": 2,
4689                  "ploc_max": 2,
4690                  "lloc_min": 2,
4691                  "lloc_max": 2,
4692                  "blank_min": 0,
4693                  "blank_max": 0
4694                }
4695                "#);
4696            },
4697        );
4698    }
4699
4700    #[cfg(feature = "perl")]
4701    #[test]
4702    fn perl_cloc_pod_block() {
4703        check_metrics::<PerlParser>(
4704            "my $x = 1;
4705=pod
4706multi-line
4707pod block
4708=cut
4709my $y = 2;",
4710            "foo.pl",
4711            |metric| {
4712                insta::assert_json_snapshot!(metric.loc, @r#"
4713                {
4714                  "sloc": 6,
4715                  "ploc": 2,
4716                  "lloc": 2,
4717                  "cloc": 4,
4718                  "blank": 0,
4719                  "sloc_average": 6.0,
4720                  "ploc_average": 2.0,
4721                  "lloc_average": 2.0,
4722                  "cloc_average": 4.0,
4723                  "blank_average": 0.0,
4724                  "sloc_min": 6,
4725                  "sloc_max": 6,
4726                  "cloc_min": 4,
4727                  "cloc_max": 4,
4728                  "ploc_min": 2,
4729                  "ploc_max": 2,
4730                  "lloc_min": 2,
4731                  "lloc_max": 2,
4732                  "blank_min": 0,
4733                  "blank_max": 0
4734                }
4735                "#);
4736            },
4737        );
4738    }
4739
4740    #[cfg(feature = "perl")]
4741    #[test]
4742    fn perl_lloc_simple_statements() {
4743        check_metrics::<PerlParser>(
4744            "my $a = 1;
4745my $b = 2;
4746my $c = 3;",
4747            "foo.pl",
4748            |metric| {
4749                insta::assert_json_snapshot!(metric.loc, @r#"
4750                {
4751                  "sloc": 3,
4752                  "ploc": 3,
4753                  "lloc": 3,
4754                  "cloc": 0,
4755                  "blank": 0,
4756                  "sloc_average": 3.0,
4757                  "ploc_average": 3.0,
4758                  "lloc_average": 3.0,
4759                  "cloc_average": 0.0,
4760                  "blank_average": 0.0,
4761                  "sloc_min": 3,
4762                  "sloc_max": 3,
4763                  "cloc_min": 0,
4764                  "cloc_max": 0,
4765                  "ploc_min": 3,
4766                  "ploc_max": 3,
4767                  "lloc_min": 3,
4768                  "lloc_max": 3,
4769                  "blank_min": 0,
4770                  "blank_max": 0
4771                }
4772                "#);
4773            },
4774        );
4775    }
4776
4777    #[cfg(feature = "perl")]
4778    #[test]
4779    fn perl_lloc_compound_statements() {
4780        check_metrics::<PerlParser>(
4781            "if ($x) {
4782    print 'a';
4783}
4784while ($n > 0) {
4785    $n--;
4786}",
4787            "foo.pl",
4788            |metric| {
4789                insta::assert_json_snapshot!(metric.loc, @r#"
4790                {
4791                  "sloc": 6,
4792                  "ploc": 6,
4793                  "lloc": 4,
4794                  "cloc": 0,
4795                  "blank": 0,
4796                  "sloc_average": 6.0,
4797                  "ploc_average": 6.0,
4798                  "lloc_average": 4.0,
4799                  "cloc_average": 0.0,
4800                  "blank_average": 0.0,
4801                  "sloc_min": 6,
4802                  "sloc_max": 6,
4803                  "cloc_min": 0,
4804                  "cloc_max": 0,
4805                  "ploc_min": 6,
4806                  "ploc_max": 6,
4807                  "lloc_min": 4,
4808                  "lloc_max": 4,
4809                  "blank_min": 0,
4810                  "blank_max": 0
4811                }
4812                "#);
4813            },
4814        );
4815    }
4816
4817    #[cfg(feature = "perl")]
4818    #[test]
4819    fn perl_lloc_postfix_form_counts_once() {
4820        // `do_thing() if cond;` is one logical line — wrapped in
4821        // single_line_statement; the inner if_simple_statement does not
4822        // add a second LLOC.
4823        check_metrics::<PerlParser>(
4824            "sub f {
4825    return 1 if $_[0];
4826}",
4827            "foo.pl",
4828            |metric| {
4829                assert_eq!(metric.loc.lloc(), 1);
4830            },
4831        );
4832    }
4833
4834    #[cfg(feature = "perl")]
4835    #[test]
4836    fn perl_lloc_use_statement() {
4837        check_metrics::<PerlParser>(
4838            "use strict;
4839use warnings;
4840my $x = 1;",
4841            "foo.pl",
4842            |metric| {
4843                insta::assert_json_snapshot!(metric.loc, @r#"
4844                {
4845                  "sloc": 3,
4846                  "ploc": 3,
4847                  "lloc": 3,
4848                  "cloc": 0,
4849                  "blank": 0,
4850                  "sloc_average": 3.0,
4851                  "ploc_average": 3.0,
4852                  "lloc_average": 3.0,
4853                  "cloc_average": 0.0,
4854                  "blank_average": 0.0,
4855                  "sloc_min": 3,
4856                  "sloc_max": 3,
4857                  "cloc_min": 0,
4858                  "cloc_max": 0,
4859                  "ploc_min": 3,
4860                  "ploc_max": 3,
4861                  "lloc_min": 3,
4862                  "lloc_max": 3,
4863                  "blank_min": 0,
4864                  "blank_max": 0
4865                }
4866                "#);
4867            },
4868        );
4869    }
4870
4871    #[cfg(feature = "perl")]
4872    #[test]
4873    fn perl_lloc_for_loop() {
4874        check_metrics::<PerlParser>(
4875            "for my $i (1..3) {
4876    print $i;
4877}",
4878            "foo.pl",
4879            |metric| {
4880                // `for_statement_2` (+1) and `print …;` SEMI in block (+1) → 2
4881                assert_eq!(metric.loc.lloc(), 2);
4882            },
4883        );
4884    }
4885
4886    #[cfg(feature = "perl")]
4887    #[test]
4888    fn perl_lloc_loop_control_statement() {
4889        check_metrics::<PerlParser>(
4890            "while (1) {
4891    last if $done;
4892}",
4893            "foo.pl",
4894            |metric| {
4895                // while_statement (+1) + loop_control_statement (+1) = 2
4896                assert_eq!(metric.loc.lloc(), 2);
4897            },
4898        );
4899    }
4900
4901    #[cfg(feature = "perl")]
4902    #[test]
4903    fn perl_lloc_no_double_count_inside_single_line_statement() {
4904        // SEMI inside a single_line_statement (postfix form) is a child of
4905        // if_simple_statement, not Block — so it must not add a second LLOC.
4906        check_metrics::<PerlParser>(
4907            "sub f {
4908    print 'a' unless $_[0];
4909}",
4910            "foo.pl",
4911            |metric| {
4912                assert_eq!(metric.loc.lloc(), 1);
4913            },
4914        );
4915    }
4916
4917    #[cfg(feature = "perl")]
4918    #[test]
4919    fn perl_lloc_function_definition_not_counted() {
4920        // `sub f { ... }` itself is a function space, not an LLOC; only its
4921        // body statements count.
4922        check_metrics::<PerlParser>(
4923            "sub f {
4924    my $x = 1;
4925}",
4926            "foo.pl",
4927            |metric| {
4928                assert_eq!(metric.loc.lloc(), 1);
4929            },
4930        );
4931    }
4932
4933    #[cfg(feature = "perl")]
4934    #[test]
4935    fn perl_lloc_anonymous_function() {
4936        // `my $f = sub { return 1; };` — the assignment is one LLOC at the
4937        // top level (the SEMI after `};`); the `return 1;` inside the
4938        // anonymous function block is a second LLOC inside the closure.
4939        check_metrics::<PerlParser>("my $f = sub { return 1; };", "foo.pl", |metric| {
4940            assert_eq!(metric.loc.lloc(), 2);
4941        });
4942    }
4943
4944    #[cfg(feature = "perl")]
4945    #[test]
4946    fn perl_multiline_string_assignment_ploc() {
4947        // Regression test for issue #778: interior rows of a multi-line string
4948        // literal are real code, not blank lines, and must be credited to PLOC
4949        // exactly as Python does (#415). Previously Perl no-op'd its string
4950        // kinds, so row 1 reached neither PLOC nor CLOC and `blank =
4951        // sloc - ploc - cloc` mislabelled it as blank (ploc was 2, blank 1).
4952        // Row 0 holds `my $s = "line1`, row 1 `line2`, row 2 `line3";`.
4953        check_metrics::<PerlParser>(
4954            "my $s = \"line1
4955line2
4956line3\";",
4957            "foo.pl",
4958            |metric| {
4959                // Three physical rows, all code, no blanks — matching Python.
4960                assert_eq!(metric.loc.sloc(), 3);
4961                assert_eq!(metric.loc.ploc(), 3);
4962                assert_eq!(metric.loc.cloc(), 0);
4963                assert_eq!(metric.loc.blank(), 0);
4964            },
4965        );
4966    }
4967
4968    // The union of the fixture languages below, so a feature subset that
4969    // enables none of them makes this test *absent* rather than failing
4970    // in a way that reads as a defect in whatever is being changed
4971    // (`.claude/rules/testing.md`). Every call site carries its own
4972    // narrower gate, so if the test exists at least one row runs and it
4973    // cannot go vacuous.
4974    #[cfg(any(
4975        feature = "bash",
4976        feature = "c",
4977        feature = "elixir",
4978        feature = "go",
4979        feature = "irules",
4980        feature = "kotlin",
4981        feature = "mozcpp",
4982        feature = "mozjs",
4983        feature = "perl",
4984        feature = "php",
4985        feature = "python",
4986        feature = "ruby",
4987        feature = "tcl",
4988    ))]
4989    #[test]
4990    fn multiline_string_ploc_consistent_across_languages() {
4991        // Cross-language parity for issue #778: the SAME 3-line string
4992        // assignment must report identical ploc / blank in every language
4993        // that has a multi-line string literal. The canonical value is
4994        // Python's #415 decision: all three rows are code, none are blank.
4995        // `check_metrics` takes a plain `fn(CodeMetrics)`, so the shared
4996        // assertion is a named function rather than a capturing closure and
4997        // must take its argument by value to match that pointer type.
4998        //
4999        // The middle row is **empty**, and that is the point (#1412).
5000        // Until then every fixture here read `line1\nline2\nline3`, which
5001        // a language passes whether or not it credits an interior row
5002        // with no text — so the table could not see either #1396 (PHP) or
5003        // #1412 (Bash), the two defects it is the cross-language home
5004        // for. C is the one language that cannot express the shape: its
5005        // multi-row literal is a backslash-newline continuation, and an
5006        // unescaped empty row ends the string, so its row keeps `line2`.
5007        //
5008        // Gated on its own callers rather than on the test's union: Go is
5009        // in the union and is the one row with different expectations, so
5010        // an ungated helper is dead code in a Go-only build.
5011        #[cfg(any(
5012            feature = "bash",
5013            feature = "c",
5014            feature = "elixir",
5015            feature = "irules",
5016            feature = "kotlin",
5017            feature = "mozcpp",
5018            feature = "mozjs",
5019            feature = "perl",
5020            feature = "php",
5021            feature = "python",
5022            feature = "ruby",
5023            feature = "tcl",
5024        ))]
5025        #[allow(clippy::needless_pass_by_value)]
5026        fn assert_three_code_rows(metric: crate::CodeMetrics) {
5027            assert_eq!(metric.loc.sloc(), 3);
5028            assert_eq!(metric.loc.ploc(), 3);
5029            assert_eq!(metric.loc.cloc(), 0);
5030            assert_eq!(metric.loc.blank(), 0);
5031        }
5032        // The heredoc forms carry their opener and terminator on rows of
5033        // their own, so they are four rows rather than three.
5034        #[cfg(any(feature = "bash", feature = "php"))]
5035        #[allow(clippy::needless_pass_by_value)]
5036        fn assert_four_code_rows(metric: crate::CodeMetrics) {
5037            assert_eq!(metric.loc.sloc(), 4);
5038            assert_eq!(metric.loc.ploc(), 4);
5039            assert_eq!(metric.loc.cloc(), 0);
5040            assert_eq!(metric.loc.blank(), 0);
5041        }
5042        #[cfg(feature = "python")]
5043        check_metrics::<PythonParser>(
5044            "s = \"\"\"line1\n\nline3\"\"\"",
5045            "foo.py",
5046            assert_three_code_rows,
5047        );
5048        #[cfg(feature = "perl")]
5049        check_metrics::<PerlParser>(
5050            "my $s = \"line1\n\nline3\";",
5051            "foo.pl",
5052            assert_three_code_rows,
5053        );
5054        #[cfg(feature = "ruby")]
5055        check_metrics::<RubyParser>("s = \"line1\n\nline3\"", "foo.rb", assert_three_code_rows);
5056        // Go, Kotlin, and Mozilla-C++ reach the same shared
5057        // `add_multiline_string_ploc` helper through their own
5058        // raw-string kinds (`raw_string_literal`,
5059        // `multiline_string_literal`, `raw_string_literal`), and were
5060        // the three call sites of it that no test exercised. Each needs
5061        // its own syntax, so they cannot reuse the quoted form above.
5062        #[cfg(feature = "go")]
5063        check_metrics::<GoParser>(
5064            "package p\n\nvar s = `line1\n\nline3`",
5065            "foo.go",
5066            |metric| {
5067                // Two extra code rows for `package p` and the blank
5068                // between it and the declaration, which Go requires.
5069                assert_eq!(metric.loc.sloc(), 5);
5070                assert_eq!(metric.loc.ploc(), 4);
5071                assert_eq!(metric.loc.cloc(), 0);
5072                assert_eq!(metric.loc.blank(), 1);
5073            },
5074        );
5075        #[cfg(feature = "kotlin")]
5076        check_metrics::<KotlinParser>(
5077            "val s = \"\"\"line1\n\nline3\"\"\"",
5078            "foo.kt",
5079            assert_three_code_rows,
5080        );
5081        #[cfg(feature = "mozcpp")]
5082        check_metrics::<MozcppParser>(
5083            "const char* s = R\"(line1\n\nline3)\";",
5084            "foo.cpp",
5085            assert_three_code_rows,
5086        );
5087        // C has no raw string; tree-sitter-c folds a backslash-newline
5088        // continuation into one `string_literal` spanning every row, and
5089        // that continuation is why the C row below is the one that keeps
5090        // a non-empty middle line.
5091        // Mozjs carries the same `template_string` arm as its upstream
5092        // JS siblings, but only `.jsm` routes to it, so the JavaScript
5093        // and TypeScript template tests never reach this copy.
5094        #[cfg(feature = "c")]
5095        check_metrics::<CParser>(
5096            "const char* s = \"line1\\\nline2\\\nline3\";",
5097            "foo.c",
5098            assert_three_code_rows,
5099        );
5100        #[cfg(feature = "mozjs")]
5101        check_metrics::<MozjsParser>(
5102            "const s = `line1\n\nline3`;",
5103            "foo.jsm",
5104            assert_three_code_rows,
5105        );
5106        // The four languages #778 missed, added by #1260. Bash's shape is
5107        // the single-quoted `raw_string`; Tcl and its iRules dialect spell
5108        // the literal `quoted_word`; Elixir routes the `quoted_content`
5109        // every one of its string forms wraps.
5110        #[cfg(feature = "bash")]
5111        check_metrics::<BashParser>("s='line1\n\nline3'", "foo.sh", assert_three_code_rows);
5112        #[cfg(feature = "tcl")]
5113        check_metrics::<TclParser>(
5114            "set s \"line1\n\nline3\"",
5115            "foo.tcl",
5116            assert_three_code_rows,
5117        );
5118        #[cfg(feature = "irules")]
5119        check_metrics::<IrulesParser>(
5120            "set s \"line1\n\nline3\"",
5121            "foo.irule",
5122            assert_three_code_rows,
5123        );
5124        #[cfg(feature = "elixir")]
5125        check_metrics::<ElixirParser>("s = \"line1\n\nline3\"", "foo.ex", assert_three_code_rows);
5126        // PHP was absent from this table entirely — including for the
5127        // quoted forms #778 routed — which is half of why #1396 survived
5128        // #778.
5129        #[cfg(feature = "php")]
5130        check_metrics::<PhpParser>(
5131            "<?php $s = \"line1\n\nline3\";",
5132            "foo.php",
5133            assert_three_code_rows,
5134        );
5135        // The two heredocs. Neither language's quoted form above can
5136        // reach the defect its heredoc had: PHP's `heredoc` wrapper
5137        // (#1396) and Bash's `heredoc_redirect` (#1412) are separate
5138        // grammar nodes from the quoted literal, and each dropped a body
5139        // row that had no text.
5140        #[cfg(feature = "php")]
5141        check_metrics::<PhpParser>(
5142            "<?php $s = <<<EOT\n\nline3\nEOT;",
5143            "foo.php",
5144            assert_four_code_rows,
5145        );
5146        #[cfg(feature = "bash")]
5147        check_metrics::<BashParser>("cat <<EOT\n\nline3\nEOT", "foo.sh", assert_four_code_rows);
5148    }
5149
5150    #[cfg(any(
5151        feature = "bash",
5152        feature = "elixir",
5153        feature = "irules",
5154        feature = "php",
5155        feature = "tcl",
5156    ))]
5157    #[test]
5158    fn multiline_literal_ending_at_eof_credits_every_row() {
5159        // The #1260 fix lives in exactly the class both metric harnesses
5160        // normalise away: `check_metrics` and the integration
5161        // `read_file_with_eol` path each force a trailing newline, so a
5162        // node ending at EOF is unreachable from either and a test written
5163        // the ordinary way passes against unfixed code (#1051, #1067 —
5164        // `.claude/rules/testing.md`). These fixtures reach the parser
5165        // byte-for-byte through `metrics_verbatim`, with the closing
5166        // delimiter as the last byte of the file.
5167        //
5168        // The interior row is empty in every fixture, for the reason the
5169        // sibling table above records: `line1\nline2\nline3` passes
5170        // whether or not a textless interior row is credited, so the
5171        // table could not see #1396 or #1412.
5172        //
5173        // `(language, source, rows the file has)`. Every row is fully
5174        // code and none blank.
5175        const FIXTURES: &[(crate::LANG, &[u8], u64)] = &[
5176            #[cfg(feature = "bash")]
5177            (crate::LANG::Bash, b"s='line1\n\nline3'", 3),
5178            #[cfg(feature = "tcl")]
5179            (crate::LANG::Tcl, b"set s \"line1\n\nline3\"", 3),
5180            #[cfg(feature = "irules")]
5181            (crate::LANG::Irules, b"set s \"line1\n\nline3\"", 3),
5182            #[cfg(feature = "elixir")]
5183            (crate::LANG::Elixir, b"s = \"line1\n\nline3\"", 3),
5184            #[cfg(feature = "php")]
5185            (crate::LANG::Php, b"<?php $s = \"line1\n\nline3\";", 3),
5186            // The heredocs, whose terminator is the file's last row —
5187            // the one shape a heredoc can end at EOF in.
5188            #[cfg(feature = "php")]
5189            (crate::LANG::Php, b"<?php $s = <<<EOT\n\nline3\nEOT;", 4),
5190            #[cfg(feature = "bash")]
5191            (crate::LANG::Bash, b"cat <<EOT\n\nline3\nEOT", 4),
5192            // Empty throughout, which is the shape whose body node
5193            // collapses to zero width (#1412).
5194            #[cfg(feature = "bash")]
5195            (crate::LANG::Bash, b"cat <<EOT\n\nEOT", 3),
5196        ];
5197
5198        crate::test_support::assert_fixtures_present(FIXTURES);
5199        for &(lang, source, rows) in FIXTURES {
5200            let text = String::from_utf8_lossy(source);
5201            let loc = metrics_verbatim(lang, source, MetricsOptions::default()).loc;
5202            assert_eq!(loc.sloc(), rows, "{lang:?} sloc for {text:?}");
5203            assert_eq!(loc.ploc(), rows, "{lang:?} ploc for {text:?}");
5204            assert_eq!(loc.cloc(), 0, "{lang:?} cloc for {text:?}");
5205            assert_eq!(loc.blank(), 0, "{lang:?} blank for {text:?}");
5206        }
5207    }
5208
5209    #[cfg(feature = "perl")]
5210    #[test]
5211    fn perl_lloc_unless_until() {
5212        check_metrics::<PerlParser>(
5213            "unless ($x) {
5214    print 'a';
5215}
5216until ($n == 0) {
5217    $n--;
5218}",
5219            "foo.pl",
5220            |metric| {
5221                // unless_statement (+1) + print SEMI (+1) + until_statement (+1)
5222                // + $n-- SEMI (+1) = 4
5223                assert_eq!(metric.loc.lloc(), 4);
5224            },
5225        );
5226    }
5227
5228    #[cfg(feature = "perl")]
5229    #[test]
5230    fn perl_lloc_heredoc_body_not_counted() {
5231        // Heredoc body content is data, not code: the body lines should not
5232        // contribute LLOC or PLOC.
5233        check_metrics::<PerlParser>(
5234            "my $s = <<END;
5235line1
5236line2
5237END
5238my $x = 1;",
5239            "foo.pl",
5240            |metric| {
5241                // Two top-level statements: the heredoc-using `my $s = …;`
5242                // and `my $x = 1;`.
5243                assert_eq!(metric.loc.lloc(), 2);
5244            },
5245        );
5246        // Independent confirmation that the snippet is a valid heredoc and
5247        // not silently parsed as an error tree (which could otherwise yield
5248        // the same `lloc == 2.0` and mask a grammar regression).
5249        assert_perl_parses_cleanly(
5250            "my $s = <<END;
5251line1
5252line2
5253END
5254my $x = 1;",
5255        );
5256    }
5257
5258    #[cfg(feature = "perl")]
5259    #[test]
5260    fn perl_lloc_package_and_require() {
5261        check_metrics::<PerlParser>(
5262            "package Foo;
5263require 5.010;
5264my $x = 1;",
5265            "foo.pl",
5266            |metric| {
5267                insta::assert_json_snapshot!(metric.loc, @r#"
5268                {
5269                  "sloc": 3,
5270                  "ploc": 3,
5271                  "lloc": 3,
5272                  "cloc": 0,
5273                  "blank": 0,
5274                  "sloc_average": 3.0,
5275                  "ploc_average": 3.0,
5276                  "lloc_average": 3.0,
5277                  "cloc_average": 0.0,
5278                  "blank_average": 0.0,
5279                  "sloc_min": 3,
5280                  "sloc_max": 3,
5281                  "cloc_min": 0,
5282                  "cloc_max": 0,
5283                  "ploc_min": 3,
5284                  "ploc_max": 3,
5285                  "lloc_min": 3,
5286                  "lloc_max": 3,
5287                  "blank_min": 0,
5288                  "blank_max": 0
5289                }
5290                "#);
5291            },
5292        );
5293    }
5294
5295    #[cfg(feature = "lua")]
5296    #[test]
5297    fn lua_blank() {
5298        check_metrics::<LuaParser>(
5299            "local x = 1
5300
5301local y = 2",
5302            "foo.lua",
5303            |metric| {
5304                assert_eq!(metric.loc.sloc(), 3);
5305                assert_eq!(metric.loc.ploc(), 2);
5306                assert_eq!(metric.loc.lloc(), 2);
5307                assert_eq!(metric.loc.cloc(), 0);
5308                assert_eq!(metric.loc.blank(), 1);
5309                insta::assert_json_snapshot!(metric.loc);
5310            },
5311        );
5312    }
5313
5314    #[cfg(feature = "lua")]
5315    #[test]
5316    fn lua_no_zero_blank() {
5317        // Blank line interleaved with code that carries trailing comments —
5318        // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
5319        check_metrics::<LuaParser>(
5320            "local a = 1
5321local b = 2
5322
5323local c = 3 -- trailing
5324local d = 4 -- trailing
5325local e = 5",
5326            "foo.lua",
5327            |metric| {
5328                assert_eq!(metric.loc.sloc(), 6);
5329                assert_eq!(metric.loc.ploc(), 5);
5330                assert_eq!(metric.loc.cloc(), 2);
5331                assert_eq!(metric.loc.blank(), 1);
5332                insta::assert_json_snapshot!(metric.loc);
5333            },
5334        );
5335    }
5336
5337    #[cfg(feature = "lua")]
5338    #[test]
5339    fn lua_blank_zero_sanity() {
5340        // Sanity check: blank must report 0, never go negative, when the
5341        // input has no blank lines.
5342        check_metrics::<LuaParser>(
5343            "local x = 1
5344local y = 2",
5345            "foo.lua",
5346            |metric| {
5347                assert_eq!(metric.loc.sloc(), 2);
5348                assert_eq!(metric.loc.ploc(), 2);
5349                assert_eq!(metric.loc.lloc(), 2);
5350                assert_eq!(metric.loc.cloc(), 0);
5351                assert_eq!(metric.loc.blank(), 0);
5352            },
5353        );
5354    }
5355
5356    #[cfg(feature = "lua")]
5357    #[test]
5358    fn lua_cloc() {
5359        check_metrics::<LuaParser>(
5360            "-- single line comment
5361local x = 1
5362--[[
5363  block comment
5364  second line
5365]]",
5366            "foo.lua",
5367            |metric| {
5368                assert_eq!(metric.loc.sloc(), 6);
5369                assert_eq!(metric.loc.ploc(), 1);
5370                assert_eq!(metric.loc.lloc(), 1);
5371                assert_eq!(metric.loc.cloc(), 5);
5372                assert_eq!(metric.loc.blank(), 0);
5373                insta::assert_json_snapshot!(metric.loc);
5374            },
5375        );
5376    }
5377
5378    #[cfg(feature = "lua")]
5379    #[test]
5380    fn lua_lloc() {
5381        check_metrics::<LuaParser>(
5382            "local function f(x)
5383  if x > 0 then
5384    local y = x + 1
5385    return y
5386  end
5387  return 0
5388end",
5389            "foo.lua",
5390            |metric| {
5391                assert_eq!(metric.loc.sloc(), 7);
5392                assert_eq!(metric.loc.ploc(), 7);
5393                assert_eq!(metric.loc.lloc(), 5);
5394                assert_eq!(metric.loc.cloc(), 0);
5395                assert_eq!(metric.loc.blank(), 0);
5396                insta::assert_json_snapshot!(metric.loc);
5397            },
5398        );
5399    }
5400
5401    #[cfg(feature = "lua")]
5402    #[test]
5403    fn lua_no_string_lloc() {
5404        // Long strings spanning multiple lines must not inflate lloc.
5405        check_metrics::<LuaParser>(
5406            "local s = [[
5407  line one
5408  line two
5409]]",
5410            "foo.lua",
5411            |metric| {
5412                // #778: a multi-line long-bracket string credits every spanned
5413                // row to PLOC (matching Python's #415 decision), so all four
5414                // rows are code and none are blank. It still contributes a
5415                // single lloc — the assignment statement.
5416                assert_eq!(metric.loc.sloc(), 4);
5417                assert_eq!(metric.loc.ploc(), 4);
5418                assert_eq!(metric.loc.lloc(), 1);
5419                assert_eq!(metric.loc.cloc(), 0);
5420                assert_eq!(metric.loc.blank(), 0);
5421                insta::assert_json_snapshot!(metric.loc);
5422            },
5423        );
5424    }
5425
5426    #[cfg(feature = "lua")]
5427    #[test]
5428    fn lua_no_functiondefinition_lloc() {
5429        // Anonymous function definition is an expression, not a statement.
5430        // The containing variable_declaration counts as lloc; FunctionDefinition must not.
5431        check_metrics::<LuaParser>(
5432            "local f = function(x)
5433  return x + 1
5434end",
5435            "foo.lua",
5436            |metric| {
5437                assert_eq!(metric.loc.sloc(), 3);
5438                assert_eq!(metric.loc.ploc(), 3);
5439                assert_eq!(metric.loc.lloc(), 2);
5440                assert_eq!(metric.loc.cloc(), 0);
5441                assert_eq!(metric.loc.blank(), 0);
5442                insta::assert_json_snapshot!(metric.loc);
5443            },
5444        );
5445    }
5446
5447    #[cfg(feature = "lua")]
5448    #[test]
5449    fn lua_no_elseif_lloc() {
5450        // elseif_statement must not add lloc; only if_statement does.
5451        check_metrics::<LuaParser>(
5452            "local function f(x)
5453  if x > 0 then
5454    return 1
5455  elseif x < 0 then
5456    return -1
5457  else
5458    return 0
5459  end
5460end",
5461            "foo.lua",
5462            |metric| {
5463                assert_eq!(metric.loc.sloc(), 9);
5464                assert_eq!(metric.loc.ploc(), 9);
5465                assert_eq!(metric.loc.lloc(), 5);
5466                assert_eq!(metric.loc.cloc(), 0);
5467                assert_eq!(metric.loc.blank(), 0);
5468                insta::assert_json_snapshot!(metric.loc);
5469            },
5470        );
5471    }
5472
5473    #[cfg(feature = "lua")]
5474    #[test]
5475    fn lua_no_else_lloc() {
5476        // else_statement must not add lloc.
5477        check_metrics::<LuaParser>(
5478            "local function f(x)
5479  if x > 0 then
5480    return 1
5481  else
5482    return 0
5483  end
5484end",
5485            "foo.lua",
5486            |metric| {
5487                assert_eq!(metric.loc.sloc(), 7);
5488                assert_eq!(metric.loc.ploc(), 7);
5489                assert_eq!(metric.loc.lloc(), 4);
5490                assert_eq!(metric.loc.cloc(), 0);
5491                assert_eq!(metric.loc.blank(), 0);
5492                insta::assert_json_snapshot!(metric.loc);
5493            },
5494        );
5495    }
5496
5497    #[cfg(feature = "lua")]
5498    #[test]
5499    fn lua_functiondeclaration_lloc() {
5500        // Named function declaration counts as one lloc.
5501        check_metrics::<LuaParser>(
5502            "function f()
5503  return 1
5504end",
5505            "foo.lua",
5506            |metric| {
5507                assert_eq!(metric.loc.sloc(), 3);
5508                assert_eq!(metric.loc.ploc(), 3);
5509                assert_eq!(metric.loc.lloc(), 2);
5510                assert_eq!(metric.loc.cloc(), 0);
5511                assert_eq!(metric.loc.blank(), 0);
5512                insta::assert_json_snapshot!(metric.loc);
5513            },
5514        );
5515    }
5516
5517    #[cfg(feature = "lua")]
5518    #[test]
5519    fn lua_local_function_lloc() {
5520        // local function declaration is also a function_declaration node → one lloc.
5521        check_metrics::<LuaParser>(
5522            "local function g()
5523  return 2
5524end",
5525            "foo.lua",
5526            |metric| {
5527                assert_eq!(metric.loc.sloc(), 3);
5528                assert_eq!(metric.loc.ploc(), 3);
5529                assert_eq!(metric.loc.lloc(), 2);
5530                assert_eq!(metric.loc.cloc(), 0);
5531                assert_eq!(metric.loc.blank(), 0);
5532                insta::assert_json_snapshot!(metric.loc);
5533            },
5534        );
5535    }
5536
5537    #[cfg(feature = "lua")]
5538    #[test]
5539    fn lua_for_numeric_lloc() {
5540        check_metrics::<LuaParser>(
5541            "for i = 1, 10 do
5542  print(i)
5543end",
5544            "foo.lua",
5545            |metric| {
5546                assert_eq!(metric.loc.sloc(), 3);
5547                assert_eq!(metric.loc.ploc(), 3);
5548                assert_eq!(metric.loc.lloc(), 1);
5549                assert_eq!(metric.loc.cloc(), 0);
5550                assert_eq!(metric.loc.blank(), 0);
5551                insta::assert_json_snapshot!(metric.loc);
5552            },
5553        );
5554    }
5555
5556    #[cfg(feature = "lua")]
5557    #[test]
5558    fn lua_for_generic_lloc() {
5559        check_metrics::<LuaParser>(
5560            "for k, v in pairs(t) do
5561  print(k, v)
5562end",
5563            "foo.lua",
5564            |metric| {
5565                assert_eq!(metric.loc.sloc(), 3);
5566                assert_eq!(metric.loc.ploc(), 3);
5567                assert_eq!(metric.loc.lloc(), 1);
5568                assert_eq!(metric.loc.cloc(), 0);
5569                assert_eq!(metric.loc.blank(), 0);
5570                insta::assert_json_snapshot!(metric.loc);
5571            },
5572        );
5573    }
5574
5575    #[cfg(feature = "lua")]
5576    #[test]
5577    fn lua_repeat_lloc() {
5578        check_metrics::<LuaParser>(
5579            "local i = 0
5580repeat
5581  i = i + 1
5582until i >= 10",
5583            "foo.lua",
5584            |metric| {
5585                assert_eq!(metric.loc.sloc(), 4);
5586                assert_eq!(metric.loc.ploc(), 4);
5587                assert_eq!(metric.loc.lloc(), 3);
5588                assert_eq!(metric.loc.cloc(), 0);
5589                assert_eq!(metric.loc.blank(), 0);
5590                insta::assert_json_snapshot!(metric.loc);
5591            },
5592        );
5593    }
5594
5595    #[cfg(feature = "lua")]
5596    #[test]
5597    fn lua_local_decl_lloc() {
5598        check_metrics::<LuaParser>(
5599            "local x = 1
5600local y, z = 2, 3",
5601            "foo.lua",
5602            |metric| {
5603                assert_eq!(metric.loc.sloc(), 2);
5604                assert_eq!(metric.loc.ploc(), 2);
5605                assert_eq!(metric.loc.lloc(), 2);
5606                assert_eq!(metric.loc.cloc(), 0);
5607                assert_eq!(metric.loc.blank(), 0);
5608                insta::assert_json_snapshot!(metric.loc);
5609            },
5610        );
5611    }
5612
5613    #[cfg(feature = "lua")]
5614    #[test]
5615    fn lua_function_call_lloc() {
5616        // Standalone function calls have no expression_statement wrapper in Lua.
5617        // They fall to the `_` branch → counted as ploc, not lloc.
5618        check_metrics::<LuaParser>(
5619            "print(\"hello\")
5620local x = 1",
5621            "foo.lua",
5622            |metric| {
5623                assert_eq!(metric.loc.sloc(), 2);
5624                assert_eq!(metric.loc.ploc(), 2);
5625                assert_eq!(metric.loc.lloc(), 1);
5626                assert_eq!(metric.loc.cloc(), 0);
5627                assert_eq!(metric.loc.blank(), 0);
5628                insta::assert_json_snapshot!(metric.loc);
5629            },
5630        );
5631    }
5632
5633    #[cfg(feature = "lua")]
5634    #[test]
5635    fn lua_toplevel_assignment_lloc() {
5636        // Bare `x = 1` at chunk level: parent is Chunk, not VariableDeclaration,
5637        // so the parent-guard correctly counts it as 1 lloc.
5638        check_metrics::<LuaParser>(
5639            "x = 1
5640y, z = 2, 3",
5641            "foo.lua",
5642            |metric| {
5643                assert_eq!(metric.loc.sloc(), 2);
5644                assert_eq!(metric.loc.ploc(), 2);
5645                assert_eq!(metric.loc.lloc(), 2);
5646                assert_eq!(metric.loc.cloc(), 0);
5647                assert_eq!(metric.loc.blank(), 0);
5648                insta::assert_json_snapshot!(metric.loc);
5649            },
5650        );
5651    }
5652
5653    #[cfg(feature = "typescript")]
5654    #[test]
5655    fn tsx_basic_loc() {
5656        check_metrics::<TsxParser>(
5657            "// A simple utility function
5658            function add(a: number, b: number): number {
5659                /* multi-line
5660                   comment */
5661                return a + b;
5662            }
5663
5664            const greet = (name: string) => {
5665                return `Hello, ${name}`;
5666            };",
5667            "foo.tsx",
5668            |metric| {
5669                insta::assert_json_snapshot!(
5670                    metric.loc,
5671                    @r#"
5672                {
5673                  "sloc": 10,
5674                  "ploc": 6,
5675                  "lloc": 3,
5676                  "cloc": 3,
5677                  "blank": 1,
5678                  "sloc_average": 3.3333333333333335,
5679                  "ploc_average": 2.0,
5680                  "lloc_average": 1.0,
5681                  "cloc_average": 1.0,
5682                  "blank_average": 0.3333333333333333,
5683                  "sloc_min": 3,
5684                  "sloc_max": 10,
5685                  "cloc_min": 0,
5686                  "cloc_max": 3,
5687                  "ploc_min": 3,
5688                  "ploc_max": 6,
5689                  "lloc_min": 1,
5690                  "lloc_max": 3,
5691                  "blank_min": 0,
5692                  "blank_max": 1
5693                }
5694                "#
5695                );
5696            },
5697        );
5698    }
5699
5700    #[cfg(feature = "typescript")]
5701    #[test]
5702    fn typescript_basic_loc() {
5703        check_metrics::<TypescriptParser>(
5704            "// Line comment
5705            /* Block
5706               comment */
5707            function greet(name: string): string {
5708                return `Hello, ${name}`;
5709            }
5710
5711            const add = (a: number, b: number): number => a + b;",
5712            "foo.ts",
5713            |metric| {
5714                insta::assert_json_snapshot!(
5715                    metric.loc,
5716                    @r#"
5717                {
5718                  "sloc": 8,
5719                  "ploc": 4,
5720                  "lloc": 2,
5721                  "cloc": 3,
5722                  "blank": 1,
5723                  "sloc_average": 2.6666666666666665,
5724                  "ploc_average": 1.3333333333333333,
5725                  "lloc_average": 0.6666666666666666,
5726                  "cloc_average": 1.0,
5727                  "blank_average": 0.3333333333333333,
5728                  "sloc_min": 1,
5729                  "sloc_max": 8,
5730                  "cloc_min": 0,
5731                  "cloc_max": 3,
5732                  "ploc_min": 1,
5733                  "ploc_max": 4,
5734                  "lloc_min": 0,
5735                  "lloc_max": 2,
5736                  "blank_min": 0,
5737                  "blank_max": 1
5738                }
5739                "#
5740                );
5741            },
5742        );
5743    }
5744
5745    #[cfg(feature = "csharp")]
5746    #[test]
5747    fn csharp_comments() {
5748        check_metrics::<CsharpParser>(
5749            "for (int i = 0; i < 100; i++) {
5750               // Print hello
5751               System.Console.WriteLine(\"hello\");
5752               /// XML doc comment
5753               System.Console.WriteLine(\"hello\");
5754             }",
5755            "foo.cs",
5756            |metric| {
5757                assert_eq!(metric.loc.sloc(), 6);
5758                assert_eq!(metric.loc.ploc(), 4);
5759                assert_eq!(metric.loc.lloc(), 3);
5760                assert_eq!(metric.loc.cloc(), 2);
5761                assert_eq!(metric.loc.blank(), 0);
5762                insta::assert_json_snapshot!(metric.loc);
5763            },
5764        );
5765    }
5766
5767    #[cfg(feature = "csharp")]
5768    #[test]
5769    fn csharp_blank() {
5770        check_metrics::<CsharpParser>(
5771            "int x = 1;
5772
5773
5774            int y = 2;",
5775            "foo.cs",
5776            |metric| {
5777                assert_eq!(metric.loc.sloc(), 4);
5778                assert_eq!(metric.loc.ploc(), 2);
5779                assert_eq!(metric.loc.lloc(), 2);
5780                assert_eq!(metric.loc.cloc(), 0);
5781                assert_eq!(metric.loc.blank(), 2);
5782                insta::assert_json_snapshot!(metric.loc);
5783            },
5784        );
5785    }
5786
5787    #[cfg(feature = "csharp")]
5788    #[test]
5789    fn csharp_sloc() {
5790        check_metrics::<CsharpParser>(
5791            "for (int i = 0; i < 100; i++) {
5792               System.Console.WriteLine(i);
5793             }",
5794            "foo.cs",
5795            |metric| {
5796                assert_eq!(metric.loc.sloc(), 3);
5797                assert_eq!(metric.loc.ploc(), 3);
5798                assert_eq!(metric.loc.lloc(), 2);
5799                assert_eq!(metric.loc.cloc(), 0);
5800                assert_eq!(metric.loc.blank(), 0);
5801                insta::assert_json_snapshot!(metric.loc);
5802            },
5803        );
5804    }
5805
5806    #[cfg(feature = "csharp")]
5807    #[test]
5808    fn csharp_module_sloc() {
5809        check_metrics::<CsharpParser>(
5810            "namespace HelloWorld {
5811              class Program { }
5812            }",
5813            "foo.cs",
5814            |metric| {
5815                assert_eq!(metric.loc.sloc(), 3);
5816                assert_eq!(metric.loc.ploc(), 3);
5817                assert_eq!(metric.loc.lloc(), 0);
5818                assert_eq!(metric.loc.cloc(), 0);
5819                assert_eq!(metric.loc.blank(), 0);
5820                insta::assert_json_snapshot!(metric.loc);
5821            },
5822        );
5823    }
5824
5825    #[cfg(feature = "csharp")]
5826    #[test]
5827    fn csharp_single_ploc() {
5828        check_metrics::<CsharpParser>("int x = 1;", "foo.cs", |metric| {
5829            assert_eq!(metric.loc.sloc(), 1);
5830            assert_eq!(metric.loc.ploc(), 1);
5831            assert_eq!(metric.loc.lloc(), 1);
5832            assert_eq!(metric.loc.cloc(), 0);
5833            assert_eq!(metric.loc.blank(), 0);
5834            insta::assert_json_snapshot!(metric.loc);
5835        });
5836    }
5837
5838    #[cfg(feature = "csharp")]
5839    #[test]
5840    fn csharp_simple_ploc() {
5841        check_metrics::<CsharpParser>(
5842            "for (int i = 0; i < 100; i++) {
5843               System.Console.WriteLine(i);
5844             }",
5845            "foo.cs",
5846            |metric| {
5847                assert_eq!(metric.loc.sloc(), 3);
5848                assert_eq!(metric.loc.ploc(), 3);
5849                assert_eq!(metric.loc.lloc(), 2);
5850                assert_eq!(metric.loc.cloc(), 0);
5851                assert_eq!(metric.loc.blank(), 0);
5852                insta::assert_json_snapshot!(metric.loc);
5853            },
5854        );
5855    }
5856
5857    #[cfg(feature = "csharp")]
5858    #[test]
5859    fn csharp_multi_ploc() {
5860        check_metrics::<CsharpParser>(
5861            "int x = 1;
5862            for (int i = 0; i < 100; i++) {
5863               System.Console.WriteLine(i);
5864             }",
5865            "foo.cs",
5866            |metric| {
5867                assert_eq!(metric.loc.sloc(), 4);
5868                assert_eq!(metric.loc.ploc(), 4);
5869                assert_eq!(metric.loc.lloc(), 3);
5870                assert_eq!(metric.loc.cloc(), 0);
5871                assert_eq!(metric.loc.blank(), 0);
5872                insta::assert_json_snapshot!(metric.loc);
5873            },
5874        );
5875    }
5876
5877    #[cfg(feature = "csharp")]
5878    #[test]
5879    fn csharp_single_statement_lloc() {
5880        check_metrics::<CsharpParser>("int max = 10;", "foo.cs", |metric| {
5881            assert_eq!(metric.loc.sloc(), 1);
5882            assert_eq!(metric.loc.ploc(), 1);
5883            assert_eq!(metric.loc.lloc(), 1);
5884            assert_eq!(metric.loc.cloc(), 0);
5885            assert_eq!(metric.loc.blank(), 0);
5886            insta::assert_json_snapshot!(metric.loc);
5887        });
5888    }
5889
5890    #[cfg(feature = "csharp")]
5891    #[test]
5892    fn csharp_for_lloc() {
5893        check_metrics::<CsharpParser>(
5894            "for (int i = 0; i < 10; i++) {
5895                System.Console.WriteLine(i);
5896            }",
5897            "foo.cs",
5898            |metric| {
5899                assert_eq!(metric.loc.sloc(), 3);
5900                assert_eq!(metric.loc.ploc(), 3);
5901                assert_eq!(metric.loc.lloc(), 2);
5902                assert_eq!(metric.loc.cloc(), 0);
5903                assert_eq!(metric.loc.blank(), 0);
5904                insta::assert_json_snapshot!(metric.loc);
5905            },
5906        );
5907    }
5908
5909    #[cfg(feature = "csharp")]
5910    #[test]
5911    fn csharp_foreach_lloc() {
5912        check_metrics::<CsharpParser>(
5913            "foreach (var item in items) {
5914                System.Console.WriteLine(item);
5915            }",
5916            "foo.cs",
5917            |metric| {
5918                assert_eq!(metric.loc.sloc(), 3);
5919                assert_eq!(metric.loc.ploc(), 3);
5920                assert_eq!(metric.loc.lloc(), 2);
5921                assert_eq!(metric.loc.cloc(), 0);
5922                assert_eq!(metric.loc.blank(), 0);
5923                insta::assert_json_snapshot!(metric.loc);
5924            },
5925        );
5926    }
5927
5928    #[cfg(feature = "csharp")]
5929    #[test]
5930    fn csharp_while_lloc() {
5931        check_metrics::<CsharpParser>(
5932            "int i = 0;
5933            while (i < 10) {
5934                i++;
5935            }",
5936            "foo.cs",
5937            |metric| {
5938                assert_eq!(metric.loc.sloc(), 4);
5939                assert_eq!(metric.loc.ploc(), 4);
5940                assert_eq!(metric.loc.lloc(), 3);
5941                assert_eq!(metric.loc.cloc(), 0);
5942                assert_eq!(metric.loc.blank(), 0);
5943                insta::assert_json_snapshot!(metric.loc);
5944            },
5945        );
5946    }
5947
5948    #[cfg(feature = "csharp")]
5949    #[test]
5950    fn csharp_do_while_lloc() {
5951        check_metrics::<CsharpParser>(
5952            "int i = 0;
5953            do {
5954                i++;
5955            } while (i < 10);",
5956            "foo.cs",
5957            |metric| {
5958                assert_eq!(metric.loc.sloc(), 4);
5959                assert_eq!(metric.loc.ploc(), 4);
5960                assert_eq!(metric.loc.lloc(), 3);
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    #[cfg(feature = "csharp")]
5969    #[test]
5970    fn csharp_switch_lloc() {
5971        check_metrics::<CsharpParser>(
5972            "switch (x) {
5973                case 1: System.Console.WriteLine(1); break;
5974                case 2: System.Console.WriteLine(2); break;
5975                default: System.Console.WriteLine(0); break;
5976            }
5977            string s = x switch { 1 => \"one\", _ => \"other\" };",
5978            "foo.cs",
5979            |metric| {
5980                assert_eq!(metric.loc.sloc(), 6);
5981                assert_eq!(metric.loc.ploc(), 6);
5982                assert_eq!(metric.loc.lloc(), 8);
5983                assert_eq!(metric.loc.cloc(), 0);
5984                assert_eq!(metric.loc.blank(), 0);
5985                insta::assert_json_snapshot!(metric.loc);
5986            },
5987        );
5988    }
5989
5990    #[cfg(feature = "csharp")]
5991    #[test]
5992    fn csharp_continue_lloc() {
5993        check_metrics::<CsharpParser>(
5994            "for (int i = 0; i < 10; i++) {
5995                if (i == 5) continue;
5996                System.Console.WriteLine(i);
5997            }",
5998            "foo.cs",
5999            |metric| {
6000                assert_eq!(metric.loc.sloc(), 4);
6001                assert_eq!(metric.loc.ploc(), 4);
6002                assert_eq!(metric.loc.lloc(), 4);
6003                assert_eq!(metric.loc.cloc(), 0);
6004                assert_eq!(metric.loc.blank(), 0);
6005                insta::assert_json_snapshot!(metric.loc);
6006            },
6007        );
6008    }
6009
6010    #[cfg(feature = "csharp")]
6011    #[test]
6012    fn csharp_try_lloc() {
6013        check_metrics::<CsharpParser>(
6014            "try {
6015                System.Console.WriteLine(\"try\");
6016            } catch (System.Exception e) {
6017                throw new System.Exception(\"caught\");
6018            } finally {
6019                System.Console.WriteLine(\"done\");
6020            }",
6021            "foo.cs",
6022            |metric| {
6023                assert_eq!(metric.loc.sloc(), 7);
6024                assert_eq!(metric.loc.ploc(), 7);
6025                assert_eq!(metric.loc.lloc(), 4);
6026                assert_eq!(metric.loc.cloc(), 0);
6027                assert_eq!(metric.loc.blank(), 0);
6028                insta::assert_json_snapshot!(metric.loc);
6029            },
6030        );
6031    }
6032
6033    #[cfg(feature = "csharp")]
6034    #[test]
6035    fn csharp_class_loc() {
6036        check_metrics::<CsharpParser>(
6037            "class A {
6038                int x;
6039                public void M() {
6040                    System.Console.WriteLine(x);
6041                }
6042            }",
6043            "foo.cs",
6044            |metric| {
6045                assert_eq!(metric.loc.sloc(), 6);
6046                assert_eq!(metric.loc.ploc(), 6);
6047                assert_eq!(metric.loc.lloc(), 1);
6048                assert_eq!(metric.loc.cloc(), 0);
6049                assert_eq!(metric.loc.blank(), 0);
6050                insta::assert_json_snapshot!(metric.loc);
6051            },
6052        );
6053    }
6054
6055    #[cfg(feature = "csharp")]
6056    #[test]
6057    fn csharp_expressions_lloc() {
6058        check_metrics::<CsharpParser>(
6059            "int a = 1;
6060            int b = 2;
6061            int c = a + b;
6062            System.Console.WriteLine(c);",
6063            "foo.cs",
6064            |metric| {
6065                assert_eq!(metric.loc.sloc(), 4);
6066                assert_eq!(metric.loc.ploc(), 4);
6067                assert_eq!(metric.loc.lloc(), 4);
6068                assert_eq!(metric.loc.cloc(), 0);
6069                assert_eq!(metric.loc.blank(), 0);
6070                insta::assert_json_snapshot!(metric.loc);
6071            },
6072        );
6073    }
6074
6075    #[cfg(feature = "csharp")]
6076    #[test]
6077    fn csharp_statement_inline_loc() {
6078        check_metrics::<CsharpParser>(
6079            "if (x > 0) System.Console.WriteLine(x);",
6080            "foo.cs",
6081            |metric| {
6082                assert_eq!(metric.loc.sloc(), 1);
6083                assert_eq!(metric.loc.ploc(), 1);
6084                assert_eq!(metric.loc.lloc(), 2);
6085                assert_eq!(metric.loc.cloc(), 0);
6086                assert_eq!(metric.loc.blank(), 0);
6087                insta::assert_json_snapshot!(metric.loc);
6088            },
6089        );
6090    }
6091
6092    #[cfg(feature = "csharp")]
6093    #[test]
6094    fn csharp_general_loc() {
6095        check_metrics::<CsharpParser>(
6096            "using System;
6097            namespace Demo {
6098                class A {
6099                    public void M() {
6100                        Console.WriteLine(\"hi\");
6101                    }
6102                }
6103                class B {
6104                    public int N() { return 0; }
6105                }
6106            }",
6107            "foo.cs",
6108            |metric| {
6109                assert_eq!(metric.loc.sloc(), 11);
6110                assert_eq!(metric.loc.ploc(), 11);
6111                assert_eq!(metric.loc.lloc(), 2);
6112                assert_eq!(metric.loc.cloc(), 0);
6113                assert_eq!(metric.loc.blank(), 0);
6114                insta::assert_json_snapshot!(metric.loc);
6115            },
6116        );
6117    }
6118
6119    #[cfg(feature = "csharp")]
6120    #[test]
6121    fn csharp_using_lloc() {
6122        // EC11 — `using_directive` does not bump LLOC; `using_statement`
6123        // (block form) and the C# 8 simple-using local-declaration
6124        // (`using var x = ...;`) both do, the latter via the standard
6125        // `LocalDeclarationStatement` path.
6126        check_metrics::<CsharpParser>(
6127            "using System;
6128            using System.IO;
6129            class A {
6130                public void M() {
6131                    using (var s = File.OpenRead(\"x\")) {
6132                        Console.WriteLine(s);
6133                    }
6134                    using var t = File.OpenRead(\"y\");
6135                    Console.WriteLine(t);
6136                }
6137            }",
6138            "foo.cs",
6139            |metric| {
6140                assert_eq!(metric.loc.sloc(), 11);
6141                assert_eq!(metric.loc.ploc(), 11);
6142                assert_eq!(metric.loc.lloc(), 4);
6143                assert_eq!(metric.loc.cloc(), 0);
6144                assert_eq!(metric.loc.blank(), 0);
6145                insta::assert_json_snapshot!(metric.loc);
6146            },
6147        );
6148    }
6149
6150    #[cfg(feature = "kotlin")]
6151    #[test]
6152    fn kotlin_loc_basic() {
6153        check_metrics::<KotlinParser>(
6154            "// A simple function
6155            fun greet(name: String): String {
6156                val greeting = \"Hello, \" + name
6157                if (name.isEmpty()) {
6158                    return \"Hello, World!\"
6159                }
6160                return greeting
6161            }",
6162            "foo.kt",
6163            |metric| {
6164                insta::assert_json_snapshot!(
6165                    metric.loc,
6166                    @r#"
6167                {
6168                  "sloc": 8,
6169                  "ploc": 7,
6170                  "lloc": 4,
6171                  "cloc": 1,
6172                  "blank": 0,
6173                  "sloc_average": 4.0,
6174                  "ploc_average": 3.5,
6175                  "lloc_average": 2.0,
6176                  "cloc_average": 0.5,
6177                  "blank_average": 0.0,
6178                  "sloc_min": 7,
6179                  "sloc_max": 8,
6180                  "cloc_min": 0,
6181                  "cloc_max": 1,
6182                  "ploc_min": 7,
6183                  "ploc_max": 7,
6184                  "lloc_min": 4,
6185                  "lloc_max": 4,
6186                  "blank_min": 0,
6187                  "blank_max": 0
6188                }
6189                "#
6190                );
6191            },
6192        );
6193    }
6194
6195    #[cfg(feature = "kotlin")]
6196    #[test]
6197    fn kotlin_loc_bare_expression() {
6198        check_metrics::<KotlinParser>(
6199            "fun main() {
6200                val x = 42
6201                println(x)
6202                listOf(1, 2, 3).forEach { println(it) }
6203            }",
6204            "foo.kt",
6205            |metric| {
6206                // lloc should count: val x = 42 (PropertyDeclaration, +1)
6207                // + println(x) (CallExpression, parent=Block, +1)
6208                // + listOf(1, 2, 3).forEach { ... } (CallExpression, parent=Block, +1) = 3
6209                insta::assert_json_snapshot!(
6210                    metric.loc,
6211                    @r#"
6212                {
6213                  "sloc": 5,
6214                  "ploc": 5,
6215                  "lloc": 3,
6216                  "cloc": 0,
6217                  "blank": 0,
6218                  "sloc_average": 2.5,
6219                  "ploc_average": 2.5,
6220                  "lloc_average": 1.5,
6221                  "cloc_average": 0.0,
6222                  "blank_average": 0.0,
6223                  "sloc_min": 5,
6224                  "sloc_max": 5,
6225                  "cloc_min": 0,
6226                  "cloc_max": 0,
6227                  "ploc_min": 5,
6228                  "ploc_max": 5,
6229                  "lloc_min": 3,
6230                  "lloc_max": 3,
6231                  "blank_min": 0,
6232                  "blank_max": 0
6233                }
6234                "#
6235                );
6236            },
6237        );
6238    }
6239
6240    #[cfg(feature = "bash")]
6241    #[test]
6242    fn bash_loc() {
6243        check_metrics::<BashParser>(
6244            "#!/bin/bash
6245# This is a comment
6246f() {
6247    echo 'hello'
6248}
6249
6250# Another comment
6251f",
6252            "foo.sh",
6253            |metric| {
6254                assert_eq!(metric.loc.sloc(), 8);
6255                assert_eq!(metric.loc.ploc(), 4);
6256                assert_eq!(metric.loc.lloc(), 3);
6257                assert_eq!(metric.loc.cloc(), 3);
6258                assert_eq!(metric.loc.blank(), 1);
6259                insta::assert_json_snapshot!(metric.loc);
6260            },
6261        );
6262    }
6263
6264    // CRLF regression tests: metrics must be identical regardless of line ending style.
6265    // These also serve as canaries for tree-sitter row-counting behaviour with \r bytes.
6266
6267    #[cfg(feature = "python")]
6268    #[test]
6269    fn python_cloc_crlf_matches_lf() {
6270        check_metrics::<PythonParser>("# comment\nx = 1", "foo.py", |m| {
6271            assert_eq!(m.loc.cloc(), 1);
6272            assert_eq!(m.loc.ploc(), 1);
6273            assert_eq!(m.loc.sloc(), 2);
6274            assert_eq!(m.loc.blank(), 0);
6275        });
6276        check_metrics::<PythonParser>("# comment\r\nx = 1", "foo.py", |m| {
6277            assert_eq!(m.loc.cloc(), 1);
6278            assert_eq!(m.loc.ploc(), 1);
6279            assert_eq!(m.loc.sloc(), 2);
6280            assert_eq!(m.loc.blank(), 0);
6281        });
6282        // Lone-CR (old Mac line endings) is the true canary: without CR normalisation,
6283        // tree-sitter 0.26.8 only advances its row counter on \n, collapsing all content
6284        // onto row 0 and producing wrong sloc/cloc metrics.
6285        check_metrics::<PythonParser>("# comment\rx = 1", "foo.py", |m| {
6286            assert_eq!(m.loc.cloc(), 1);
6287            assert_eq!(m.loc.ploc(), 1);
6288            assert_eq!(m.loc.sloc(), 2);
6289            assert_eq!(m.loc.blank(), 0);
6290        });
6291    }
6292
6293    #[cfg(feature = "python")]
6294    #[test]
6295    fn python_blank_crlf_matches_lf() {
6296        check_metrics::<PythonParser>("# comment\n\nx = 1", "foo.py", |m| {
6297            assert_eq!(m.loc.blank(), 1);
6298        });
6299        check_metrics::<PythonParser>("# comment\r\n\r\nx = 1", "foo.py", |m| {
6300            assert_eq!(m.loc.blank(), 1);
6301        });
6302        // Lone-CR: without normalisation the blank \r line stays on row 0 and is not counted.
6303        check_metrics::<PythonParser>("# comment\r\rx = 1", "foo.py", |m| {
6304            assert_eq!(m.loc.blank(), 1);
6305        });
6306    }
6307
6308    #[cfg(feature = "rust")]
6309    #[test]
6310    fn rust_cloc_crlf_matches_lf() {
6311        check_metrics::<RustParser>(
6312            "fn f() {\n    // comment\n    let x = 1;\n}",
6313            "foo.rs",
6314            |m| {
6315                assert_eq!(m.loc.cloc(), 1);
6316                assert_eq!(m.loc.sloc(), 4);
6317            },
6318        );
6319        check_metrics::<RustParser>(
6320            "fn f() {\r\n    // comment\r\n    let x = 1;\r\n}",
6321            "foo.rs",
6322            |m| {
6323                assert_eq!(m.loc.cloc(), 1);
6324                assert_eq!(m.loc.sloc(), 4);
6325            },
6326        );
6327        // Lone-CR: without normalisation, tree-sitter 0.26.8 only advances its row counter on
6328        // \n, so all content collapses onto row 0 and sloc becomes 1 instead of 4.
6329        check_metrics::<RustParser>(
6330            "fn f() {\r    // comment\r    let x = 1;\r}",
6331            "foo.rs",
6332            |m| {
6333                assert_eq!(m.loc.cloc(), 1);
6334                assert_eq!(m.loc.sloc(), 4);
6335            },
6336        );
6337    }
6338
6339    #[cfg(feature = "tcl")]
6340    #[test]
6341    fn tcl_blank() {
6342        check_metrics::<TclParser>("set x 1\n\nset y 2", "foo.tcl", |metric| {
6343            assert_eq!(metric.loc.sloc(), 3);
6344            assert_eq!(metric.loc.ploc(), 2);
6345            assert_eq!(metric.loc.lloc(), 2);
6346            assert_eq!(metric.loc.cloc(), 0);
6347            assert_eq!(metric.loc.blank(), 1);
6348            insta::assert_json_snapshot!(metric.loc);
6349        });
6350    }
6351
6352    #[cfg(feature = "tcl")]
6353    #[test]
6354    fn tcl_no_zero_blank() {
6355        // Blank line interleaved with code that carries trailing comments —
6356        // ensures the `blank = sloc - (ploc ∪ cloc lines)` union math holds
6357        // when code and comment lines coincide.
6358        check_metrics::<TclParser>(
6359            "set a 1\nset b 2\n\nset c 3 ;# trailing\nset d 4 ;# trailing\nset e 5",
6360            "foo.tcl",
6361            |metric| {
6362                assert_eq!(metric.loc.sloc(), 6);
6363                assert_eq!(metric.loc.ploc(), 5);
6364                assert_eq!(metric.loc.cloc(), 2);
6365                assert_eq!(metric.loc.blank(), 1);
6366            },
6367        );
6368    }
6369
6370    /// expected: row 0 is comment-only, row 1 is the sole code row — so
6371    /// `cloc 1 + ploc 1 == sloc 2`. This test pinned `ploc == 2` until
6372    /// #1135: the `LF` token terminating the comment row landed in the
6373    /// `_` catch-all and inserted that row into PLOC, which also drove
6374    /// `cloc + ploc` past `sloc`.
6375    #[cfg(feature = "tcl")]
6376    #[test]
6377    fn tcl_cloc() {
6378        check_metrics::<TclParser>("# This is a comment\nset x 1", "foo.tcl", |metric| {
6379            assert_eq!(metric.loc.sloc(), 2);
6380            assert_eq!(metric.loc.ploc(), 1);
6381            assert_eq!(metric.loc.lloc(), 1);
6382            assert_eq!(metric.loc.cloc(), 1);
6383            assert_eq!(metric.loc.blank(), 0);
6384            insta::assert_json_snapshot!(metric.loc);
6385        });
6386    }
6387
6388    #[cfg(feature = "tcl")]
6389    #[test]
6390    fn tcl_lloc() {
6391        check_metrics::<TclParser>(
6392            "proc f {x} {
6393    while {$x > 0} {
6394        if {$x > 10} {
6395            set x [expr {$x - 1}]
6396        }
6397    }
6398}",
6399            "foo.tcl",
6400            |metric| {
6401                assert_eq!(metric.loc.sloc(), 7);
6402                assert_eq!(metric.loc.ploc(), 7);
6403                assert_eq!(metric.loc.lloc(), 4);
6404                assert_eq!(metric.loc.cloc(), 0);
6405                assert_eq!(metric.loc.blank(), 0);
6406                insta::assert_json_snapshot!(metric.loc);
6407            },
6408        );
6409    }
6410
6411    #[cfg(feature = "tcl")]
6412    #[test]
6413    fn tcl_no_command_substitution_lloc() {
6414        // `string toupper` inside [...] is a sub-expression; only `puts` is top-level.
6415        check_metrics::<TclParser>("puts [string toupper x]", "foo.tcl", |metric| {
6416            assert_eq!(metric.loc.sloc(), 1);
6417            assert_eq!(metric.loc.ploc(), 1);
6418            assert_eq!(metric.loc.lloc(), 1);
6419            assert_eq!(metric.loc.cloc(), 0);
6420            assert_eq!(metric.loc.blank(), 0);
6421            insta::assert_json_snapshot!(metric.loc);
6422        });
6423    }
6424
6425    #[cfg(feature = "tcl")]
6426    #[test]
6427    fn tcl_procedure_lloc() {
6428        check_metrics::<TclParser>("proc foo {} {\n    puts hello\n}", "foo.tcl", |metric| {
6429            assert_eq!(metric.loc.sloc(), 3);
6430            assert_eq!(metric.loc.ploc(), 3);
6431            assert_eq!(metric.loc.lloc(), 2);
6432            assert_eq!(metric.loc.cloc(), 0);
6433            assert_eq!(metric.loc.blank(), 0);
6434            insta::assert_json_snapshot!(metric.loc);
6435        });
6436    }
6437
6438    #[cfg(feature = "tcl")]
6439    #[test]
6440    fn tcl_if_lloc() {
6441        check_metrics::<TclParser>("if {1} {\n    puts hello\n}", "foo.tcl", |metric| {
6442            assert_eq!(metric.loc.sloc(), 3);
6443            assert_eq!(metric.loc.ploc(), 3);
6444            assert_eq!(metric.loc.lloc(), 2);
6445            assert_eq!(metric.loc.cloc(), 0);
6446            assert_eq!(metric.loc.blank(), 0);
6447            insta::assert_json_snapshot!(metric.loc);
6448        });
6449    }
6450
6451    #[cfg(feature = "tcl")]
6452    #[test]
6453    fn tcl_elseif_lloc() {
6454        // if=1 lloc, elseif=1 lloc, else adds 0 lloc
6455        check_metrics::<TclParser>(
6456            "if {$x > 10} {
6457    puts big
6458} elseif {$x > 5} {
6459    puts medium
6460} else {
6461    puts small
6462}",
6463            "foo.tcl",
6464            |metric| {
6465                assert_eq!(metric.loc.sloc(), 7);
6466                assert_eq!(metric.loc.ploc(), 7);
6467                assert_eq!(metric.loc.lloc(), 5);
6468                assert_eq!(metric.loc.cloc(), 0);
6469                assert_eq!(metric.loc.blank(), 0);
6470                insta::assert_json_snapshot!(metric.loc);
6471            },
6472        );
6473    }
6474
6475    #[cfg(feature = "tcl")]
6476    #[test]
6477    fn tcl_while_lloc() {
6478        check_metrics::<TclParser>(
6479            "while {$x > 0} {\n    set x [expr {$x - 1}]\n}",
6480            "foo.tcl",
6481            |metric| {
6482                assert_eq!(metric.loc.sloc(), 3);
6483                assert_eq!(metric.loc.ploc(), 3);
6484                assert_eq!(metric.loc.lloc(), 2);
6485                assert_eq!(metric.loc.cloc(), 0);
6486                assert_eq!(metric.loc.blank(), 0);
6487                insta::assert_json_snapshot!(metric.loc);
6488            },
6489        );
6490    }
6491
6492    #[cfg(feature = "tcl")]
6493    #[test]
6494    fn tcl_foreach_lloc() {
6495        check_metrics::<TclParser>(
6496            "foreach item {a b c} {\n    puts $item\n}",
6497            "foo.tcl",
6498            |metric| {
6499                assert_eq!(metric.loc.sloc(), 3);
6500                assert_eq!(metric.loc.ploc(), 3);
6501                assert_eq!(metric.loc.lloc(), 2);
6502                assert_eq!(metric.loc.cloc(), 0);
6503                assert_eq!(metric.loc.blank(), 0);
6504                insta::assert_json_snapshot!(metric.loc);
6505            },
6506        );
6507    }
6508
6509    #[cfg(feature = "tcl")]
6510    #[test]
6511    fn tcl_set_lloc() {
6512        check_metrics::<TclParser>("set x 42", "foo.tcl", |metric| {
6513            assert_eq!(metric.loc.sloc(), 1);
6514            assert_eq!(metric.loc.ploc(), 1);
6515            assert_eq!(metric.loc.lloc(), 1);
6516            assert_eq!(metric.loc.cloc(), 0);
6517            assert_eq!(metric.loc.blank(), 0);
6518            insta::assert_json_snapshot!(metric.loc);
6519        });
6520    }
6521
6522    #[cfg(feature = "tcl")]
6523    #[test]
6524    fn tcl_global_lloc() {
6525        check_metrics::<TclParser>("global x", "foo.tcl", |metric| {
6526            assert_eq!(metric.loc.sloc(), 1);
6527            assert_eq!(metric.loc.ploc(), 1);
6528            assert_eq!(metric.loc.lloc(), 1);
6529            assert_eq!(metric.loc.cloc(), 0);
6530            assert_eq!(metric.loc.blank(), 0);
6531            insta::assert_json_snapshot!(metric.loc);
6532        });
6533    }
6534
6535    #[cfg(feature = "tcl")]
6536    #[test]
6537    fn tcl_try_catch_lloc() {
6538        // try=1 lloc; catch command=1 lloc; commands inside bodies count separately
6539        check_metrics::<TclParser>(
6540            "catch {
6541    set x 1
6542} result
6543try {
6544    set y 2
6545} on error {msg} {
6546    puts $msg
6547}",
6548            "foo.tcl",
6549            |metric| {
6550                assert_eq!(metric.loc.sloc(), 8);
6551                assert_eq!(metric.loc.ploc(), 8);
6552                assert_eq!(metric.loc.lloc(), 5);
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    #[cfg(feature = "tcl")]
6561    #[test]
6562    fn tcl_namespace_lloc() {
6563        check_metrics::<TclParser>(
6564            "namespace eval myns {\n    set x 1\n}",
6565            "foo.tcl",
6566            |metric| {
6567                assert_eq!(metric.loc.sloc(), 3);
6568                assert_eq!(metric.loc.ploc(), 3);
6569                assert_eq!(metric.loc.lloc(), 2);
6570                assert_eq!(metric.loc.cloc(), 0);
6571                assert_eq!(metric.loc.blank(), 0);
6572                insta::assert_json_snapshot!(metric.loc);
6573            },
6574        );
6575    }
6576
6577    #[cfg(feature = "tcl")]
6578    #[test]
6579    fn tcl_regexp_lloc() {
6580        check_metrics::<TclParser>("regexp {^[0-9]+$} $x", "foo.tcl", |metric| {
6581            assert_eq!(metric.loc.sloc(), 1);
6582            assert_eq!(metric.loc.ploc(), 1);
6583            assert_eq!(metric.loc.lloc(), 1);
6584            assert_eq!(metric.loc.cloc(), 0);
6585            assert_eq!(metric.loc.blank(), 0);
6586            insta::assert_json_snapshot!(metric.loc);
6587        });
6588    }
6589
6590    #[cfg(feature = "tcl")]
6591    #[test]
6592    fn tcl_expr_cmd_lloc() {
6593        check_metrics::<TclParser>("expr {1 + 2}", "foo.tcl", |metric| {
6594            assert_eq!(metric.loc.sloc(), 1);
6595            assert_eq!(metric.loc.ploc(), 1);
6596            assert_eq!(metric.loc.lloc(), 1);
6597            assert_eq!(metric.loc.cloc(), 0);
6598            assert_eq!(metric.loc.blank(), 0);
6599            insta::assert_json_snapshot!(metric.loc);
6600        });
6601    }
6602
6603    #[cfg(feature = "tcl")]
6604    #[test]
6605    fn tcl_no_expr_cmd_substitution_lloc() {
6606        // `expr` inside [...] is a sub-expression, not a statement; only `set` counts.
6607        check_metrics::<TclParser>("set x [expr {1 + 2}]", "foo.tcl", |metric| {
6608            assert_eq!(metric.loc.sloc(), 1);
6609            assert_eq!(metric.loc.ploc(), 1);
6610            assert_eq!(metric.loc.lloc(), 1);
6611            assert_eq!(metric.loc.cloc(), 0);
6612            assert_eq!(metric.loc.blank(), 0);
6613            insta::assert_json_snapshot!(metric.loc);
6614        });
6615    }
6616
6617    #[cfg(feature = "tcl")]
6618    #[test]
6619    fn tcl_nested_commands_lloc() {
6620        // Commands inside proc body are recursively parsed; verify each counts.
6621        check_metrics::<TclParser>(
6622            "proc f {x} {
6623    set y [expr {$x * 2}]
6624    puts $y
6625}",
6626            "foo.tcl",
6627            |metric| {
6628                assert_eq!(metric.loc.sloc(), 4);
6629                assert_eq!(metric.loc.ploc(), 4);
6630                assert_eq!(metric.loc.lloc(), 3);
6631                assert_eq!(metric.loc.cloc(), 0);
6632                assert_eq!(metric.loc.blank(), 0);
6633                insta::assert_json_snapshot!(metric.loc);
6634            },
6635        );
6636    }
6637
6638    #[cfg(feature = "tcl")]
6639    #[test]
6640    fn tcl_command_lloc() {
6641        check_metrics::<TclParser>("puts hello", "foo.tcl", |metric| {
6642            assert_eq!(metric.loc.sloc(), 1);
6643            assert_eq!(metric.loc.ploc(), 1);
6644            assert_eq!(metric.loc.lloc(), 1);
6645            assert_eq!(metric.loc.cloc(), 0);
6646            assert_eq!(metric.loc.blank(), 0);
6647            insta::assert_json_snapshot!(metric.loc);
6648        });
6649    }
6650
6651    #[cfg(feature = "tcl")]
6652    #[test]
6653    fn tcl_no_else_lloc() {
6654        // `else` block does not add a logical line.
6655        check_metrics::<TclParser>(
6656            "if {1} {\n    puts yes\n} else {\n    puts no\n}",
6657            "foo.tcl",
6658            |metric| {
6659                assert_eq!(metric.loc.sloc(), 5);
6660                assert_eq!(metric.loc.ploc(), 5);
6661                assert_eq!(metric.loc.lloc(), 3);
6662                assert_eq!(metric.loc.cloc(), 0);
6663                assert_eq!(metric.loc.blank(), 0);
6664                insta::assert_json_snapshot!(metric.loc);
6665            },
6666        );
6667    }
6668
6669    #[cfg(feature = "tcl")]
6670    #[test]
6671    fn tcl_no_finally_lloc() {
6672        // `finally` block, like `else`, does not add a logical line.
6673        // proc(1) + try(1) + puts_hi(1) + puts_done(1) + finally(0) = 4.
6674        check_metrics::<TclParser>(
6675            "proc f {} {\n    try {\n        puts hi\n    } finally {\n        puts done\n    }\n}",
6676            "foo.tcl",
6677            |metric| {
6678                assert_eq!(
6679                    metric.loc.lloc(),
6680                    4,
6681                    "finally adds 0 lloc; would be 5 if finally counted"
6682                );
6683            },
6684        );
6685    }
6686
6687    #[cfg(feature = "tcl")]
6688    #[test]
6689    fn tcl_multiline_block() {
6690        check_metrics::<TclParser>(
6691            "proc f {x} {
6692    set a 1
6693
6694    set b 2
6695    return [expr {$a + $b}]
6696}",
6697            "foo.tcl",
6698            |metric| {
6699                assert_eq!(metric.loc.sloc(), 6);
6700                assert_eq!(metric.loc.ploc(), 5);
6701                assert_eq!(metric.loc.lloc(), 4);
6702                assert_eq!(metric.loc.cloc(), 0);
6703                assert_eq!(metric.loc.blank(), 1);
6704                insta::assert_json_snapshot!(metric.loc);
6705            },
6706        );
6707    }
6708
6709    #[cfg(feature = "tcl")]
6710    #[test]
6711    fn tcl_no_string_lloc() {
6712        // Multi-line double-quoted strings must not inflate lloc — only the
6713        // surrounding command should count. Mirrors lua_no_string_lloc and
6714        // elixir_no_string_content_lloc; pins the heredoc-shaped invariant
6715        // for Tcl quoted_word bodies.
6716        //
6717        // expected: three physical rows (`set s "line one`, `line two`,
6718        // `line three"`), all code — the interior row of a multi-line
6719        // string is PLOC, not blank (#1260, following #778 / #415). This
6720        // test pinned `ploc 2 / blank 1` until #1260; the row `line two`
6721        // held no node at all under the old arm list, so it reached
6722        // neither PLOC nor CLOC and `blank = sloc - ploc - cloc` claimed
6723        // it. `lloc` is unchanged: only the `set` command counts.
6724        check_metrics::<TclParser>(
6725            "set s \"line one\nline two\nline three\"",
6726            "foo.tcl",
6727            |metric| {
6728                assert_eq!(metric.loc.sloc(), 3);
6729                assert_eq!(metric.loc.ploc(), 3);
6730                assert_eq!(metric.loc.lloc(), 1);
6731                assert_eq!(metric.loc.cloc(), 0);
6732                assert_eq!(metric.loc.blank(), 0);
6733                insta::assert_json_snapshot!(metric.loc);
6734            },
6735        );
6736    }
6737
6738    #[cfg(feature = "tcl")]
6739    #[test]
6740    fn tcl_multiline_quoted_word_credits_every_row_to_ploc() {
6741        // Regression test for #1260. A Tcl `quoted_word` carries no child
6742        // per row — only the two `"` tokens — so its interior rows reached
6743        // neither PLOC nor CLOC and `blank = sloc - ploc - cloc` claimed
6744        // them.
6745
6746        // expected: 4 rows — `set msg "line one`, `line two`,
6747        // `line three"`, `puts $msg` — all code, none blank. lloc 2 for the
6748        // `set` and the `puts` command.
6749        check_metrics::<TclParser>(
6750            "set msg \"line one\nline two\nline three\"\nputs $msg",
6751            "foo.tcl",
6752            |metric| {
6753                assert_eq!(metric.loc.sloc(), 4);
6754                assert_eq!(metric.loc.ploc(), 4);
6755                assert_eq!(metric.loc.lloc(), 2);
6756                assert_eq!(metric.loc.cloc(), 0);
6757                assert_eq!(metric.loc.blank(), 0);
6758            },
6759        );
6760
6761        // expected: 3 rows whose middle row is empty *inside* the literal —
6762        // `set msg "one`, ``, `three"`. Code, not blank, matching every
6763        // language that already routes a multi-line literal.
6764        check_metrics::<TclParser>("set msg \"one\n\nthree\"", "foo.tcl", |metric| {
6765            assert_eq!(metric.loc.sloc(), 3);
6766            assert_eq!(metric.loc.ploc(), 3);
6767            assert_eq!(metric.loc.cloc(), 0);
6768            assert_eq!(metric.loc.blank(), 0);
6769        });
6770    }
6771
6772    #[cfg(feature = "tcl")]
6773    #[test]
6774    fn tcl_braced_word_body_blank_rows_stay_blank() {
6775        // The #1260 arm deliberately stops at `quoted_word`. Tcl spells a
6776        // script body and a braced literal with one kind, `braced_word`,
6777        // and the grammar parses both as scripts — `puts {a\n\nb}` yields
6778        // `command` children exactly as a `proc` body does, which is why
6779        // #1318 had to tell the two roles apart out-of-band. Routing it
6780        // would turn every blank line inside every procedure into code.
6781        //
6782        // expected: 5 rows — `proc p {} {`, `    set x 1`, ``,
6783        // `    set y 2`, `}` — with row 3 genuinely blank, so sloc 5,
6784        // ploc 4, blank 1. lloc 3 = the procedure plus two `set` commands.
6785        check_metrics::<TclParser>(
6786            "proc p {} {\n    set x 1\n\n    set y 2\n}",
6787            "foo.tcl",
6788            |metric| {
6789                assert_eq!(metric.loc.sloc(), 5);
6790                assert_eq!(metric.loc.ploc(), 4);
6791                assert_eq!(metric.loc.lloc(), 3);
6792                assert_eq!(metric.loc.cloc(), 0);
6793                assert_eq!(metric.loc.blank(), 1);
6794            },
6795        );
6796    }
6797
6798    #[cfg(feature = "javascript")]
6799    #[test]
6800    fn javascript_blank() {
6801        check_metrics::<JavascriptParser>(
6802            "// header comment
6803        function f() {
6804
6805            var x = 1;
6806
6807            var y = 2;
6808        }",
6809            "foo.js",
6810            |metric| {
6811                assert_eq!(metric.loc.sloc(), 7);
6812                assert_eq!(metric.loc.ploc(), 4);
6813                assert_eq!(metric.loc.lloc(), 2);
6814                assert_eq!(metric.loc.cloc(), 1);
6815                assert_eq!(metric.loc.blank(), 2);
6816                insta::assert_json_snapshot!(metric.loc);
6817            },
6818        );
6819    }
6820
6821    #[cfg(feature = "javascript")]
6822    #[test]
6823    fn javascript_cloc() {
6824        check_metrics::<JavascriptParser>(
6825            "// line comment
6826        /* block
6827           comment */
6828        function f() {
6829            return 1; // inline
6830        }",
6831            "foo.js",
6832            |metric| {
6833                assert_eq!(metric.loc.sloc(), 6);
6834                assert_eq!(metric.loc.ploc(), 3);
6835                assert_eq!(metric.loc.lloc(), 1);
6836                assert_eq!(metric.loc.cloc(), 4);
6837                assert_eq!(metric.loc.blank(), 0);
6838                insta::assert_json_snapshot!(metric.loc);
6839            },
6840        );
6841    }
6842
6843    #[cfg(feature = "javascript")]
6844    #[test]
6845    fn javascript_cloc_html_comment() {
6846        // The Annex-B `<!-- -->` `html_comment` must count as CLOC, not
6847        // fall to the `_` arm and inflate PLOC (#697). Pre-fix this
6848        // fixture measured cloc 0 / ploc 4.
6849        check_metrics::<JavascriptParser>(
6850            "<!-- header comment -->
6851function f() {
6852  return 1;
6853}",
6854            "foo.js",
6855            |metric| {
6856                assert_eq!(metric.loc.sloc(), 4);
6857                assert_eq!(metric.loc.ploc(), 3);
6858                assert_eq!(metric.loc.lloc(), 1);
6859                assert_eq!(metric.loc.cloc(), 1);
6860                assert_eq!(metric.loc.blank(), 0);
6861            },
6862        );
6863    }
6864
6865    #[cfg(feature = "mozjs")]
6866    #[test]
6867    fn mozjs_blank() {
6868        check_metrics::<MozjsParser>(
6869            "function f() {
6870
6871            var x = 1;
6872
6873        }",
6874            "foo.js",
6875            |metric| {
6876                assert_eq!(metric.loc.sloc(), 5);
6877                assert_eq!(metric.loc.ploc(), 3);
6878                assert_eq!(metric.loc.lloc(), 1);
6879                assert_eq!(metric.loc.cloc(), 0);
6880                assert_eq!(metric.loc.blank(), 2);
6881                insta::assert_json_snapshot!(metric.loc);
6882            },
6883        );
6884    }
6885
6886    #[cfg(feature = "mozjs")]
6887    #[test]
6888    fn mozjs_cloc() {
6889        check_metrics::<MozjsParser>(
6890            "// header
6891        /* block comment */
6892        function f() {
6893            return 42;
6894        }",
6895            "foo.js",
6896            |metric| {
6897                assert_eq!(metric.loc.sloc(), 5);
6898                assert_eq!(metric.loc.ploc(), 3);
6899                assert_eq!(metric.loc.lloc(), 1);
6900                assert_eq!(metric.loc.cloc(), 2);
6901                assert_eq!(metric.loc.blank(), 0);
6902                insta::assert_json_snapshot!(metric.loc);
6903            },
6904        );
6905    }
6906
6907    #[cfg(feature = "mozjs")]
6908    #[test]
6909    fn mozjs_no_zero_blank() {
6910        // Blank line interleaved with code that carries trailing comments —
6911        // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
6912        check_metrics::<MozjsParser>(
6913            "function f() {
6914  var a = 1;
6915
6916  var b = 2; // trailing
6917  var c = 3; // trailing
6918}",
6919            "foo.js",
6920            |metric| {
6921                assert_eq!(metric.loc.sloc(), 6);
6922                assert_eq!(metric.loc.ploc(), 5);
6923                assert_eq!(metric.loc.cloc(), 2);
6924                assert_eq!(metric.loc.blank(), 1);
6925                insta::assert_json_snapshot!(metric.loc);
6926            },
6927        );
6928    }
6929
6930    #[cfg(feature = "mozjs")]
6931    #[test]
6932    fn mozjs_arrow_function_loc() {
6933        check_metrics::<MozjsParser>(
6934            "const add = (a, b) => a + b;
6935        const greet = name => {
6936            return 'Hello ' + name;
6937        };",
6938            "foo.js",
6939            |metric| {
6940                assert_eq!(metric.loc.sloc(), 4);
6941                assert_eq!(metric.loc.ploc(), 4);
6942                assert_eq!(metric.loc.lloc(), 3);
6943                assert_eq!(metric.loc.cloc(), 0);
6944                assert_eq!(metric.loc.blank(), 0);
6945                insta::assert_json_snapshot!(metric.loc);
6946            },
6947        );
6948    }
6949
6950    #[cfg(feature = "mozjs")]
6951    #[test]
6952    fn mozjs_multiple_functions_loc() {
6953        check_metrics::<MozjsParser>(
6954            "function f() {
6955            return 1;
6956        }
6957        function g() {
6958            return 2;
6959        }",
6960            "foo.js",
6961            |metric| {
6962                assert_eq!(metric.loc.sloc(), 6);
6963                assert_eq!(metric.loc.ploc(), 6);
6964                assert_eq!(metric.loc.lloc(), 2);
6965                assert_eq!(metric.loc.cloc(), 0);
6966                assert_eq!(metric.loc.blank(), 0);
6967                insta::assert_json_snapshot!(metric.loc);
6968            },
6969        );
6970    }
6971
6972    #[cfg(feature = "mozjs")]
6973    #[test]
6974    fn mozjs_nested_function_loc() {
6975        check_metrics::<MozjsParser>(
6976            "function outer() {
6977            function inner() {
6978                return 1;
6979            }
6980            return inner();
6981        }",
6982            "foo.js",
6983            |metric| {
6984                assert_eq!(metric.loc.sloc(), 6);
6985                assert_eq!(metric.loc.ploc(), 6);
6986                assert_eq!(metric.loc.lloc(), 2);
6987                assert_eq!(metric.loc.cloc(), 0);
6988                assert_eq!(metric.loc.blank(), 0);
6989                insta::assert_json_snapshot!(metric.loc);
6990            },
6991        );
6992    }
6993
6994    #[cfg(feature = "mozjs")]
6995    #[test]
6996    fn mozjs_if_lloc() {
6997        check_metrics::<MozjsParser>(
6998            "function f(x) {
6999            if (x > 0) {
7000                return 1;
7001            } else {
7002                return -1;
7003            }
7004        }",
7005            "foo.js",
7006            |metric| {
7007                assert_eq!(metric.loc.sloc(), 7);
7008                assert_eq!(metric.loc.ploc(), 7);
7009                // lloc = 3: the `if` statement plus the two `return`
7010                // statements. The three `{ … }` brace blocks (function
7011                // body, `if` consequent, `else` alternative) are syntactic
7012                // groupings, not logical statements, and contribute 0 —
7013                // matching C/Rust/Java for the equivalent code (#777).
7014                // Pre-#777 this asserted 6 (every StatementBlock counted).
7015                assert_eq!(metric.loc.lloc(), 3);
7016                assert_eq!(metric.loc.cloc(), 0);
7017                assert_eq!(metric.loc.blank(), 0);
7018                insta::assert_json_snapshot!(metric.loc);
7019            },
7020        );
7021    }
7022
7023    // Cross-language parity (#777): the same if/else function body yields
7024    // identical lloc across the JS family and the C-family / Rust baselines.
7025    // Removing `StatementBlock` from the JS-family lloc arms restored this
7026    // invariant — every brace block now contributes 0 lloc, as it always
7027    // had elsewhere. Pre-#777 the JS variants reported lloc 6 (three brace
7028    // blocks over-counted) against C's and Rust's 3.
7029    #[cfg(all(
7030        feature = "cpp",
7031        feature = "javascript",
7032        feature = "mozjs",
7033        feature = "rust",
7034        feature = "typescript",
7035    ))]
7036    #[test]
7037    fn js_family_if_lloc_matches_c_and_rust() {
7038        #[cfg(any(
7039            feature = "cpp",
7040            feature = "javascript",
7041            feature = "mozjs",
7042            feature = "rust",
7043            feature = "typescript",
7044        ))]
7045        const JS_SRC: &str = "function f(x) {
7046            if (x > 0) {
7047                return 1;
7048            } else {
7049                return -1;
7050            }
7051        }";
7052        #[cfg(any(
7053            feature = "cpp",
7054            feature = "javascript",
7055            feature = "mozjs",
7056            feature = "rust",
7057            feature = "typescript",
7058        ))]
7059        const C_SRC: &str = "int f(int x) {
7060            if (x > 0) {
7061                return 1;
7062            } else {
7063                return -1;
7064            }
7065        }";
7066        #[cfg(any(
7067            feature = "cpp",
7068            feature = "javascript",
7069            feature = "mozjs",
7070            feature = "rust",
7071            feature = "typescript",
7072        ))]
7073        const RUST_SRC: &str = "fn f(x: i32) -> i32 {
7074            if x > 0 {
7075                return 1;
7076            } else {
7077                return -1;
7078            }
7079        }";
7080
7081        // The logical-statement count is grammar-independent: one `if`
7082        // plus two `return`s, regardless of brace style or language.
7083        #[cfg(any(
7084            feature = "cpp",
7085            feature = "javascript",
7086            feature = "mozjs",
7087            feature = "rust",
7088            feature = "typescript",
7089        ))]
7090        const EXPECTED_LLOC: usize = 3;
7091
7092        check_metrics::<CppParser>(C_SRC, "f.c", |m| {
7093            assert_eq!(m.loc.lloc() as usize, EXPECTED_LLOC);
7094        });
7095        check_metrics::<RustParser>(RUST_SRC, "f.rs", |m| {
7096            assert_eq!(m.loc.lloc() as usize, EXPECTED_LLOC);
7097        });
7098        check_metrics::<MozjsParser>(JS_SRC, "f.js", |m| {
7099            assert_eq!(m.loc.lloc() as usize, EXPECTED_LLOC);
7100        });
7101        check_metrics::<JavascriptParser>(JS_SRC, "f.js", |m| {
7102            assert_eq!(m.loc.lloc() as usize, EXPECTED_LLOC);
7103        });
7104        check_metrics::<TypescriptParser>(JS_SRC, "f.ts", |m| {
7105            assert_eq!(m.loc.lloc() as usize, EXPECTED_LLOC);
7106        });
7107        check_metrics::<TsxParser>(JS_SRC, "f.tsx", |m| {
7108            assert_eq!(m.loc.lloc() as usize, EXPECTED_LLOC);
7109        });
7110    }
7111
7112    #[cfg(feature = "mozjs")]
7113    #[test]
7114    fn mozjs_for_lloc() {
7115        check_metrics::<MozjsParser>(
7116            "function f(n) {
7117            var s = 0;
7118            for (var i = 0; i < n; i++) {
7119                s += i;
7120            }
7121            return s;
7122        }",
7123            "foo.js",
7124            |metric| {
7125                assert_eq!(metric.loc.sloc(), 7);
7126                assert_eq!(metric.loc.ploc(), 7);
7127                assert_eq!(metric.loc.lloc(), 4);
7128                assert_eq!(metric.loc.cloc(), 0);
7129                assert_eq!(metric.loc.blank(), 0);
7130                insta::assert_json_snapshot!(metric.loc);
7131            },
7132        );
7133    }
7134
7135    #[cfg(feature = "bash")]
7136    #[test]
7137    fn bash_blank() {
7138        check_metrics::<BashParser>(
7139            "#!/bin/bash
7140
7141        f() {
7142
7143            echo hello
7144
7145        }",
7146            "foo.sh",
7147            |metric| {
7148                assert_eq!(metric.loc.sloc(), 7);
7149                assert_eq!(metric.loc.ploc(), 3);
7150                assert_eq!(metric.loc.lloc(), 2);
7151                assert_eq!(metric.loc.cloc(), 1);
7152                assert_eq!(metric.loc.blank(), 3);
7153                insta::assert_json_snapshot!(metric.loc);
7154            },
7155        );
7156    }
7157
7158    #[cfg(feature = "bash")]
7159    #[test]
7160    fn bash_cloc() {
7161        check_metrics::<BashParser>(
7162            "# header comment
7163        f() {
7164            # body comment
7165            echo hello
7166        }",
7167            "foo.sh",
7168            |metric| {
7169                assert_eq!(metric.loc.sloc(), 5);
7170                assert_eq!(metric.loc.ploc(), 3);
7171                assert_eq!(metric.loc.lloc(), 2);
7172                assert_eq!(metric.loc.cloc(), 2);
7173                assert_eq!(metric.loc.blank(), 0);
7174                insta::assert_json_snapshot!(metric.loc);
7175            },
7176        );
7177    }
7178
7179    #[cfg(feature = "bash")]
7180    #[test]
7181    fn bash_no_zero_blank() {
7182        // Blank line interleaved with code that carries trailing comments —
7183        // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
7184        check_metrics::<BashParser>(
7185            "f() {
7186  echo a
7187
7188  echo b # trailing
7189  echo c # trailing
7190}",
7191            "foo.sh",
7192            |metric| {
7193                assert_eq!(metric.loc.sloc(), 6);
7194                assert_eq!(metric.loc.ploc(), 5);
7195                assert_eq!(metric.loc.cloc(), 2);
7196                assert_eq!(metric.loc.blank(), 1);
7197                insta::assert_json_snapshot!(metric.loc);
7198            },
7199        );
7200    }
7201
7202    #[cfg(feature = "bash")]
7203    #[test]
7204    fn bash_comment_before_code_line_reclassified() {
7205        // Regression for #547: a standalone `#` comment sitting on a line
7206        // that the grammar *also* anchors a zero-width code leaf to (the
7207        // empty `word` tree-sitter-bash emits inside a `$(...)` command
7208        // substitution that contains only a comment) must reclassify that
7209        // row from comment-only to code-comment. The Bash `Loc` leaf arm
7210        // previously omitted `check_comment_ends_on_code_line` (unlike
7211        // Elixir and every other impl), so the row was credited to BOTH
7212        // `ploc` and the comment-only set and `blank` was undercounted by
7213        // one.
7214        //
7215        // Source rows: 0 `echo a`, 1 blank, 2 `echo "$(`, 3 `  # c`,
7216        // 4 `)"`. expected: sloc=5 (every physical row),
7217        // ploc=4 (rows 0/2/3/4 — row 3 carries the phantom code leaf),
7218        // lloc=3, cloc=1 (row 3, now a code-comment line, not comment-only),
7219        // blank=1 (row 1). Without the fix `blank` collapses to 0 because
7220        // row 3 is double-counted. Verified fail-on-revert per
7221        // .claude/rules/testing.md.
7222        check_metrics::<BashParser>("echo a\n\necho \"$(\n  # c\n)\"\n", "foo.sh", |metric| {
7223            assert_eq!(metric.loc.sloc(), 5);
7224            assert_eq!(metric.loc.ploc(), 4);
7225            assert_eq!(metric.loc.lloc(), 3);
7226            assert_eq!(metric.loc.cloc(), 1);
7227            assert_eq!(metric.loc.blank(), 1);
7228            insta::assert_json_snapshot!(
7229                metric.loc,
7230                @r#"
7231                {
7232                  "sloc": 5,
7233                  "ploc": 4,
7234                  "lloc": 3,
7235                  "cloc": 1,
7236                  "blank": 1,
7237                  "sloc_average": 5.0,
7238                  "ploc_average": 4.0,
7239                  "lloc_average": 3.0,
7240                  "cloc_average": 1.0,
7241                  "blank_average": 1.0,
7242                  "sloc_min": 5,
7243                  "sloc_max": 5,
7244                  "cloc_min": 1,
7245                  "cloc_max": 1,
7246                  "ploc_min": 4,
7247                  "ploc_max": 4,
7248                  "lloc_min": 3,
7249                  "lloc_max": 3,
7250                  "blank_min": 1,
7251                  "blank_max": 1
7252                }
7253                "#
7254            );
7255        });
7256    }
7257
7258    #[cfg(feature = "bash")]
7259    #[test]
7260    fn bash_if_lloc() {
7261        check_metrics::<BashParser>(
7262            "f() {
7263            if [ $1 -gt 0 ]; then
7264                echo positive
7265            else
7266                echo negative
7267            fi
7268        }",
7269            "foo.sh",
7270            |metric| {
7271                assert_eq!(metric.loc.sloc(), 7);
7272                assert_eq!(metric.loc.ploc(), 7);
7273                assert_eq!(metric.loc.lloc(), 4);
7274                assert_eq!(metric.loc.cloc(), 0);
7275                assert_eq!(metric.loc.blank(), 0);
7276                insta::assert_json_snapshot!(metric.loc);
7277            },
7278        );
7279    }
7280
7281    #[cfg(feature = "bash")]
7282    #[test]
7283    fn bash_for_lloc() {
7284        check_metrics::<BashParser>(
7285            "f() {
7286            for i in 1 2 3; do
7287                echo $i
7288            done
7289        }",
7290            "foo.sh",
7291            |metric| {
7292                assert_eq!(metric.loc.sloc(), 5);
7293                assert_eq!(metric.loc.ploc(), 5);
7294                assert_eq!(metric.loc.lloc(), 3);
7295                assert_eq!(metric.loc.cloc(), 0);
7296                assert_eq!(metric.loc.blank(), 0);
7297                insta::assert_json_snapshot!(metric.loc);
7298            },
7299        );
7300    }
7301
7302    #[cfg(feature = "bash")]
7303    #[test]
7304    fn bash_while_lloc() {
7305        check_metrics::<BashParser>(
7306            "f() {
7307            local n=5
7308            while [ $n -gt 0 ]; do
7309                echo $n
7310                n=$((n - 1))
7311            done
7312        }",
7313            "foo.sh",
7314            |metric| {
7315                assert_eq!(metric.loc.sloc(), 7);
7316                assert_eq!(metric.loc.ploc(), 7);
7317                // `f()`, `local n=5`, `while`, `echo $n`, and the
7318                // `n=$((n - 1))` assignment. That last one read as zero
7319                // until the LLOC arm learned the `variable_assignment`
7320                // alias the parser actually emits, so this asserted 4.
7321                assert_eq!(metric.loc.lloc(), 5);
7322                assert_eq!(metric.loc.cloc(), 0);
7323                assert_eq!(metric.loc.blank(), 0);
7324                insta::assert_json_snapshot!(metric.loc);
7325            },
7326        );
7327    }
7328
7329    /// A Bash assignment is a logical line where it stands as a
7330    /// statement, and no line of its own where it is part of one.
7331    ///
7332    /// The parser emits `variable_assignment` under the id the enum
7333    /// spells `VariableAssignment2`; the unsuffixed `VariableAssignment`
7334    /// it never emits, so the LLOC arm listing only that scored a bare
7335    /// `a=1` zero (`.claude/rules/grammar-dispatch.md` §1). Adding the
7336    /// alias ungated then double-counted the two positions where the
7337    /// assignment hangs off a node the same arm already counts — a
7338    /// `declaration_command` and a `command`'s environment prefix (§5).
7339    ///
7340    /// Both halves need a row: a table of standalone assignments alone
7341    /// passes with the parent gate deleted, and a table of wrapped ones
7342    /// alone passes with the alias never added.
7343    #[cfg(feature = "bash")]
7344    #[test]
7345    fn bash_assignment_counts_one_logical_line_per_statement() {
7346        for (source, lloc) in [
7347            (&b"a=1\nb=2\nc=3\n"[..], 3),
7348            (&b"local d=4\n"[..], 1),
7349            (&b"export e=5\n"[..], 1),
7350            (&b"declare -a g=(1 2)\n"[..], 1),
7351            (&b"X=1 cmd arg\n"[..], 1),
7352            (&b"(h=6)\n"[..], 1),
7353            (&b"for i in 1; do j=7; done\n"[..], 2),
7354        ] {
7355            let measured = metrics_verbatim(crate::LANG::Bash, source, MetricsOptions::default())
7356                .loc
7357                .lloc();
7358            assert_eq!(measured, lloc, "{:?}", String::from_utf8_lossy(source));
7359        }
7360    }
7361
7362    #[cfg(feature = "bash")]
7363    #[test]
7364    fn bash_case_lloc() {
7365        check_metrics::<BashParser>(
7366            "f() {
7367            case $1 in
7368                start) echo starting ;;
7369                stop)  echo stopping ;;
7370                *)     echo unknown  ;;
7371            esac
7372        }",
7373            "foo.sh",
7374            |metric| {
7375                assert_eq!(metric.loc.sloc(), 7);
7376                assert_eq!(metric.loc.ploc(), 7);
7377                assert_eq!(metric.loc.lloc(), 5);
7378                assert_eq!(metric.loc.cloc(), 0);
7379                assert_eq!(metric.loc.blank(), 0);
7380                insta::assert_json_snapshot!(metric.loc);
7381            },
7382        );
7383    }
7384
7385    #[cfg(feature = "bash")]
7386    #[test]
7387    fn bash_multiple_functions_loc() {
7388        check_metrics::<BashParser>(
7389            "f() {
7390            echo hello
7391        }
7392        g() {
7393            echo world
7394        }",
7395            "foo.sh",
7396            |metric| {
7397                assert_eq!(metric.loc.sloc(), 6);
7398                assert_eq!(metric.loc.ploc(), 6);
7399                assert_eq!(metric.loc.lloc(), 4);
7400                assert_eq!(metric.loc.cloc(), 0);
7401                assert_eq!(metric.loc.blank(), 0);
7402                insta::assert_json_snapshot!(metric.loc);
7403            },
7404        );
7405    }
7406
7407    #[cfg(feature = "bash")]
7408    #[test]
7409    fn bash_nested_function_loc() {
7410        check_metrics::<BashParser>(
7411            "outer() {
7412            inner() {
7413                echo inner
7414            }
7415            inner
7416            echo outer
7417        }",
7418            "foo.sh",
7419            |metric| {
7420                assert_eq!(metric.loc.sloc(), 7);
7421                assert_eq!(metric.loc.ploc(), 7);
7422                assert_eq!(metric.loc.lloc(), 5);
7423                assert_eq!(metric.loc.cloc(), 0);
7424                assert_eq!(metric.loc.blank(), 0);
7425                insta::assert_json_snapshot!(metric.loc);
7426            },
7427        );
7428    }
7429
7430    #[cfg(feature = "bash")]
7431    #[test]
7432    fn bash_heredoc_loc() {
7433        // expected: six physical rows (`f() {`, `cat <<EOF`, `line1`,
7434        // `line2`, `EOF`, `}`), all code and none blank — a heredoc body's
7435        // rows are real source text (#1260, following #778 / #415). This
7436        // test pinned `ploc 5 / blank 1` until #1260: `heredoc_body` is a
7437        // childless node spanning both body rows, so the leaf arm credited
7438        // only `line1` and `line2` reached neither PLOC nor CLOC.
7439        check_metrics::<BashParser>(
7440            "f() {
7441            cat <<EOF
7442line1
7443line2
7444EOF
7445        }",
7446            "foo.sh",
7447            |metric| {
7448                assert_eq!(metric.loc.sloc(), 6);
7449                assert_eq!(metric.loc.ploc(), 6);
7450                assert_eq!(metric.loc.lloc(), 2);
7451                assert_eq!(metric.loc.cloc(), 0);
7452                assert_eq!(metric.loc.blank(), 0);
7453                insta::assert_json_snapshot!(metric.loc);
7454            },
7455        );
7456    }
7457
7458    #[cfg(feature = "bash")]
7459    #[test]
7460    fn bash_multiline_literals_credit_every_row_to_ploc() {
7461        // Regression test for #1260. Bash routed no string kind to a
7462        // PLOC/CLOC bucket, so the interior rows of every multi-row literal
7463        // reached neither and `blank = sloc - ploc - cloc` claimed them.
7464        //
7465        // Each fixture isolates one arm of the new match, so a dropped kind
7466        // fails here rather than being covered by a sibling
7467        // (`.claude/rules/grammar-dispatch.md` section 11): `raw_string`,
7468        // `ansi_c_string` and `heredoc_body` are childless nodes the old
7469        // leaf arm credited only one row of, while `string` emits a
7470        // `string_content` child per row that *has* text and so skipped an
7471        // empty interior row.
7472
7473        // expected: 5 rows — `cat <<DOC`, `line one`, `line two`, `DOC`,
7474        // `echo done` — all code, none blank. lloc 2 for the two commands.
7475        check_metrics::<BashParser>(
7476            "cat <<DOC\nline one\nline two\nDOC\necho done",
7477            "foo.sh",
7478            |metric| {
7479                assert_eq!(metric.loc.sloc(), 5);
7480                assert_eq!(metric.loc.ploc(), 5);
7481                assert_eq!(metric.loc.lloc(), 2);
7482                assert_eq!(metric.loc.cloc(), 0);
7483                assert_eq!(metric.loc.blank(), 0);
7484            },
7485        );
7486
7487        // expected: 3 rows of a single-quoted `raw_string` — `s='one`,
7488        // `two`, `three'` — all code, none blank.
7489        check_metrics::<BashParser>("s='one\ntwo\nthree'", "foo.sh", |metric| {
7490            assert_eq!(metric.loc.sloc(), 3);
7491            assert_eq!(metric.loc.ploc(), 3);
7492            assert_eq!(metric.loc.cloc(), 0);
7493            assert_eq!(metric.loc.blank(), 0);
7494        });
7495
7496        // expected: 2 rows of an `ansi_c_string` (`$'…'`) — `s=$'one` and
7497        // `two'` — both code, neither blank.
7498        check_metrics::<BashParser>("s=$'one\ntwo'", "foo.sh", |metric| {
7499            assert_eq!(metric.loc.sloc(), 2);
7500            assert_eq!(metric.loc.ploc(), 2);
7501            assert_eq!(metric.loc.cloc(), 0);
7502            assert_eq!(metric.loc.blank(), 0);
7503        });
7504
7505        // expected: 3 rows of a double-quoted `string` whose middle row is
7506        // empty — `s="one`, ``, `three"`. The empty row is inside the
7507        // literal, so it is code, not blank: every one of the eighteen
7508        // languages that already route a multi-line literal answers 0 blank
7509        // for this shape, and Bash now agrees.
7510        check_metrics::<BashParser>("s=\"one\n\nthree\"", "foo.sh", |metric| {
7511            assert_eq!(metric.loc.sloc(), 3);
7512            assert_eq!(metric.loc.ploc(), 3);
7513            assert_eq!(metric.loc.cloc(), 0);
7514            assert_eq!(metric.loc.blank(), 0);
7515        });
7516
7517        // expected: 4 rows of a quoted, tab-stripping heredoc — the
7518        // `cat <<-'DOC'` opener, one body row, an empty body row, and the
7519        // `DOC` terminator — all code, none blank. `<<-'DOC'` suppresses
7520        // interpolation, which is a distinct grammar path from the bare
7521        // `<<DOC` above.
7522        check_metrics::<BashParser>("cat <<-'DOC'\n\tline one\n\nDOC", "foo.sh", |metric| {
7523            assert_eq!(metric.loc.sloc(), 4);
7524            assert_eq!(metric.loc.ploc(), 4);
7525            assert_eq!(metric.loc.lloc(), 1);
7526            assert_eq!(metric.loc.cloc(), 0);
7527            assert_eq!(metric.loc.blank(), 0);
7528        });
7529
7530        // expected: 2 rows for a heredoc with an empty body — `cat <<DOC`
7531        // and the `DOC` terminator. The body node is zero-width here, so
7532        // this pins that the new arm adds no phantom row.
7533        check_metrics::<BashParser>("cat <<DOC\nDOC", "foo.sh", |metric| {
7534            assert_eq!(metric.loc.sloc(), 2);
7535            assert_eq!(metric.loc.ploc(), 2);
7536            assert_eq!(metric.loc.cloc(), 0);
7537            assert_eq!(metric.loc.blank(), 0);
7538        });
7539    }
7540
7541    /// Analyses `source` byte-for-byte as Bash, for the #1412 fixtures
7542    /// `check_metrics` cannot carry: that shim trims trailing newlines,
7543    /// and a heredoc whose terminator is the last row is exactly the
7544    /// shape it normalises away (`.claude/rules/testing.md`).
7545    #[cfg(feature = "bash")]
7546    fn bash_loc_verbatim(source: &[u8]) -> Stats {
7547        metrics_verbatim(
7548            crate::LANG::Bash,
7549            source,
7550            crate::MetricsOptions::default().with_only(&[crate::Metric::Loc]),
7551        )
7552        .loc
7553    }
7554
7555    /// #1412: a heredoc body that is empty at the top, or empty
7556    /// throughout, read as blank. `heredoc_body`'s span begins at the
7557    /// first body row that *has* text and collapses to zero width when
7558    /// there is none, so the leading empty rows sat inside no node at
7559    /// all and `blank = sloc - ploc - cloc` claimed them.
7560    ///
7561    /// #1260 routed the body, which fixed only the interior of a body
7562    /// that has text — the shape all of its fixtures had. The arm now
7563    /// routes the `heredoc_redirect` wrapper, the node present for every
7564    /// spelling (`.claude/rules/grammar-dispatch.md` section 6), which
7565    /// is what #1396 did for PHP.
7566    #[cfg(feature = "bash")]
7567    #[test]
7568    fn bash_heredoc_bodies_that_open_or_stay_empty_are_not_blank() {
7569        // `(source, rows the file has, blank reported before #1412)`.
7570        // The last column is what makes each row a regression test
7571        // rather than a restatement of the fixed behaviour.
7572        //
7573        // `sloc` is the second axis the fixtures are anchored on
7574        // (`.claude/rules/testing.md`): trimming the heredoc out of any
7575        // of them leaves a bare `cat` — or, in the last row, a bare
7576        // function — which fails the `sloc` assertion before the `blank`
7577        // one can go vacuous.
7578        const FIXTURES: &[(&[u8], u64, u64)] = &[
7579            // The issue's four reproductions. The fourth is the control:
7580            // it is the shape #1260's own fixtures had and was already
7581            // correct, so a "fix" that simply stopped counting body rows
7582            // moves it off `ploc == rows`.
7583            (b"cat <<EOT\n\nEOT\n", 3, 1),
7584            (b"cat <<EOT\n\n\nEOT\n", 4, 2),
7585            (b"cat <<EOT\n\na\nEOT\n", 4, 1),
7586            (b"cat <<EOT\na\n\nEOT\n", 4, 0),
7587            // The spellings and positions those four do not reach, each
7588            // a different grammar path to the same wrapper: a body empty
7589            // *and* terminated at EOF (`heredoc_end` is the last byte,
7590            // which `check_metrics` cannot express), the tab-stripping
7591            // `<<-` opener, the quoted `<<'EOT'` opener that suppresses
7592            // interpolation, and a heredoc nested in a function body.
7593            (b"cat <<EOT\n\nEOT", 3, 1),
7594            (b"cat <<-EOT\n\n\tx\n\tEOT\n", 4, 1),
7595            (b"cat <<'EOT'\n\nx\nEOT\n", 4, 1),
7596            (b"f() {\n  cat <<EOT\n\n  x\nEOT\n}\n", 6, 1),
7597        ];
7598
7599        for &(source, rows, blank_before) in FIXTURES {
7600            let text = String::from_utf8_lossy(source);
7601            let loc = bash_loc_verbatim(source);
7602            assert_eq!(loc.sloc(), rows, "sloc for {text:?}");
7603            assert_eq!(loc.ploc(), rows, "ploc for {text:?}");
7604            assert_eq!(loc.cloc(), 0, "cloc for {text:?}");
7605            assert_eq!(
7606                loc.blank(),
7607                0,
7608                "blank for {text:?}, which reported {blank_before} before #1412"
7609            );
7610        }
7611    }
7612
7613    /// The wrapper's span must not reach past the heredoc: rows after
7614    /// the terminator are the caller's own, blank ones included. Routing
7615    /// a *container* rather than the body is exactly the change that
7616    /// could over-credit, so this is the upper bound for #1412 the way
7617    /// `a_multiline_literal_credits_no_row_past_its_own_span` is for
7618    /// #1260.
7619    #[cfg(feature = "bash")]
7620    #[test]
7621    fn bash_heredoc_wrapper_credits_no_row_past_its_terminator() {
7622        // rows: 0 `cat <<EOT`, 1 (empty body), 2 `EOT`, 3 (blank),
7623        // 4 `# note`, 5 `echo after`. The blank row and the comment row
7624        // are outside the literal and must stay that way.
7625        let loc = bash_loc_verbatim(b"cat <<EOT\n\nEOT\n\n# note\necho after\n");
7626        assert_eq!(loc.sloc(), 6);
7627        assert_eq!(loc.ploc(), 4);
7628        assert_eq!(loc.cloc(), 1);
7629        assert_eq!(loc.blank(), 1);
7630    }
7631
7632    /// A `\`-continuation row inside a heredoc's command prefix is code
7633    /// (#1443, #1445).
7634    ///
7635    /// `heredoc_redirect` spans the command-line prefix as well as the
7636    /// literal, so crediting its whole interior over-credits a blank or
7637    /// comment-only prefix row. #1443 narrowed the range to the literal's
7638    /// own rows to fix that, and the narrowing was reverted: the shape it
7639    /// fixed needs `bash -n` to reject the file, while the shape it broke
7640    /// — this one — runs.
7641    ///
7642    /// A continuation row holding only `\` carries no leaf for the
7643    /// leaf-gated catch-all to credit, so the wrapper's blanket range is
7644    /// the only thing covering it. Narrowing took it from `ploc 5` to
7645    /// `ploc 4`. The general gap is #1445 (`echo a | \` + `\` +
7646    /// `  grep b` loses the row with no heredoc involved); until that
7647    /// lands, this pins the row the blanket range saves, so a second
7648    /// attempt at the narrowing fails here rather than shipping.
7649    #[cfg(feature = "bash")]
7650    #[test]
7651    fn bash_heredoc_continuation_row_is_code() {
7652        // rows: 0 `cat <<EOT | \`, 1 `\`, 2 `  grep x`, 3 `body`, 4 `EOT`.
7653        // Valid Bash — `bash -n` accepts it — and every row is code.
7654        let continuation = bash_loc_verbatim(b"cat <<EOT | \\\n\\\n  grep x\nbody\nEOT\n");
7655        assert_eq!(continuation.sloc(), 5);
7656        assert_eq!(
7657            continuation.ploc(),
7658            5,
7659            "the `\\`-only continuation row is part of the command"
7660        );
7661        assert_eq!(continuation.cloc(), 0);
7662        assert_eq!(continuation.blank(), 0);
7663
7664        // The same command with content on the continuation row, which
7665        // the catch-all credits through its own leaves. Unaffected by the
7666        // range either way, and here so the pair brackets the shape.
7667        let with_content = bash_loc_verbatim(b"cat <<EOT | \\\n  grep x\nbody\nEOT\n");
7668        assert_eq!(
7669            (
7670                with_content.sloc(),
7671                with_content.ploc(),
7672                with_content.blank()
7673            ),
7674            (4, 4, 0)
7675        );
7676    }
7677
7678    /// The orphan `heredoc_body` — the shape that makes routing the
7679    /// wrapper an *addition* rather than a replacement.
7680    ///
7681    /// `node-types.json` lists `heredoc_body` only as a child of
7682    /// `heredoc_redirect`, which describes the well-formed grammar and
7683    /// says nothing about error recovery. A single-line compound
7684    /// carrying a heredoc parses to an `{ERROR}` root whose direct child
7685    /// is the body, with no wrapper anywhere — so an arm keyed on the
7686    /// wrapper alone leaves that node to the leaf-gated catch-all, which
7687    /// credits only its start row, and the terminator row falls to
7688    /// `blank`. That is #1412's own symptom.
7689    ///
7690    /// Every input here is valid, executable Bash. Measured: each
7691    /// reported `blank 1` while the body kinds were dropped from the arm
7692    /// and `blank 0` both before and after.
7693    #[cfg(feature = "bash")]
7694    #[test]
7695    fn bash_heredoc_body_without_a_wrapper_still_credits_every_row() {
7696        // Three rows each: the compound, the body, the terminator. All
7697        // code, none blank — the same answer the well-formed spellings
7698        // give above.
7699        for source in [
7700            &b"f() { cat <<EOT; }\nbody\nEOT\n"[..],
7701            &b"if true; then cat <<EOT; fi\nx\nEOT\n"[..],
7702            &b"for i in 1; do cat <<EOT; done\nx\nEOT\n"[..],
7703        ] {
7704            let loc = bash_loc_verbatim(source);
7705            let text = String::from_utf8_lossy(source);
7706            assert_eq!(loc.sloc(), 3, "sloc for {text:?}");
7707            assert_eq!(loc.ploc(), 3, "ploc for {text:?}");
7708            assert_eq!(loc.cloc(), 0, "cloc for {text:?}");
7709            assert_eq!(
7710                loc.blank(),
7711                0,
7712                "the terminator row of a wrapper-less heredoc body is code, not blank, for {text:?}"
7713            );
7714        }
7715    }
7716
7717    #[cfg(feature = "bash")]
7718    #[test]
7719    fn bash_literal_that_owns_its_opening_row_still_credits_it() {
7720        // The other branch of `add_multiline_string_ploc`'s parent gate,
7721        // and the one every fixture above misses: each of those writes the
7722        // literal after `s=`, so the opening row is already PLOC and the
7723        // gate's *skip* path is harmless. Here the literal is the only
7724        // thing on its row.
7725        //
7726        // Bash is the language where that distinction bites. The helper
7727        // skips the opening row when the parent starts on it, which is safe
7728        // only where the catch-all credits every node's start row — and
7729        // Bash's is leaf-gated, so `command` / `command_name` contribute
7730        // nothing and a childless `raw_string` is the sole node covering
7731        // its own row. Routing it through the helper alone reported
7732        // `ploc 0, blank 1` for a one-line file.
7733
7734        // expected: one row, one code row, nothing blank. `'ls'` parses as
7735        // `command → command_name → raw_string`, none of whose ancestors
7736        // reaches PLOC.
7737        check_metrics::<BashParser>("'ls'", "foo.sh", |metric| {
7738            assert_eq!(metric.loc.sloc(), 1);
7739            assert_eq!(metric.loc.ploc(), 1);
7740            assert_eq!(metric.loc.cloc(), 0);
7741            assert_eq!(metric.loc.blank(), 0);
7742        });
7743
7744        // expected: the same for an `ansi_c_string`, the other childless
7745        // single-row literal.
7746        check_metrics::<BashParser>("$'ls'", "foo.sh", |metric| {
7747            assert_eq!(metric.loc.sloc(), 1);
7748            assert_eq!(metric.loc.ploc(), 1);
7749            assert_eq!(metric.loc.cloc(), 0);
7750            assert_eq!(metric.loc.blank(), 0);
7751        });
7752
7753        // expected: 3 rows — `f() {`, `  'ls'`, `}` — all code. The literal
7754        // sits inside a function body, so a lost row shows as blank 1
7755        // rather than as an empty file.
7756        check_metrics::<BashParser>("f() {\n  'ls'\n}", "foo.sh", |metric| {
7757            assert_eq!(metric.loc.sloc(), 3);
7758            assert_eq!(metric.loc.ploc(), 3);
7759            assert_eq!(metric.loc.cloc(), 0);
7760            assert_eq!(metric.loc.blank(), 0);
7761        });
7762
7763        // expected: 2 rows — `# c` (comment-only) and `'ls'` (code). This
7764        // also covers the `check_comment_ends_on_code_line` call the gate
7765        // skipped along with the row: cloc stays 1 and blank stays 0.
7766        check_metrics::<BashParser>("# c\n'ls'", "foo.sh", |metric| {
7767            assert_eq!(metric.loc.sloc(), 2);
7768            assert_eq!(metric.loc.ploc(), 1);
7769            assert_eq!(metric.loc.cloc(), 1);
7770            assert_eq!(metric.loc.blank(), 0);
7771        });
7772
7773        // expected: 2 rows of a multi-row raw string that is the whole
7774        // command — `'a` and `b'`. Both code. Before #1260 this reported
7775        // `ploc 1` (the opening row only); the gate alone would have
7776        // reported `ploc 1` again, having traded the opening row for the
7777        // closing one.
7778        check_metrics::<BashParser>("'a\nb'", "foo.sh", |metric| {
7779            assert_eq!(metric.loc.sloc(), 2);
7780            assert_eq!(metric.loc.ploc(), 2);
7781            assert_eq!(metric.loc.cloc(), 0);
7782            assert_eq!(metric.loc.blank(), 0);
7783        });
7784    }
7785
7786    #[cfg(all(feature = "bash", feature = "tcl"))]
7787    #[test]
7788    fn a_multiline_literal_credits_no_row_past_its_own_span() {
7789        // The #1260 arms insert a *range* of rows, so the row after the
7790        // literal is the boundary worth pinning: an off-by-one in the range
7791        // end, or a literal node whose span is read one row too wide, would
7792        // pull the following comment row into PLOC. Each fixture puts a
7793        // comment-only row immediately after a closed literal, so that row
7794        // must stay comment-only.
7795
7796        // expected: 7 rows — `cat <<A`, `a1`, `A`, `# note`, `cat <<B`,
7797        // `b1`, `B`. Six are code and row 4 is comment-only: ploc 6,
7798        // cloc 1, blank 0. Two heredocs, so the row is bracketed on both
7799        // sides by a literal.
7800        check_metrics::<BashParser>(
7801            "cat <<A\na1\nA\n# note\ncat <<B\nb1\nB",
7802            "foo.sh",
7803            |metric| {
7804                assert_eq!(metric.loc.sloc(), 7);
7805                assert_eq!(metric.loc.ploc(), 6);
7806                assert_eq!(metric.loc.cloc(), 1);
7807                assert_eq!(metric.loc.blank(), 0);
7808            },
7809        );
7810
7811        // expected: 4 rows — `set a "x`, `y"`, `# note`, `set b 1` — with
7812        // row 3 comment-only: ploc 3, cloc 1, blank 0.
7813        check_metrics::<TclParser>("set a \"x\ny\"\n# note\nset b 1", "foo.tcl", |metric| {
7814            assert_eq!(metric.loc.sloc(), 4);
7815            assert_eq!(metric.loc.ploc(), 3);
7816            assert_eq!(metric.loc.cloc(), 1);
7817            assert_eq!(metric.loc.blank(), 0);
7818        });
7819    }
7820
7821    #[cfg(feature = "kotlin")]
7822    #[test]
7823    fn kotlin_loc_blank() {
7824        check_metrics::<KotlinParser>(
7825            "fun f(): Int {
7826
7827            val x = 1
7828
7829            return x
7830        }",
7831            "foo.kt",
7832            |metric| {
7833                assert_eq!(metric.loc.sloc(), 6);
7834                assert_eq!(metric.loc.ploc(), 4);
7835                assert_eq!(metric.loc.lloc(), 2);
7836                assert_eq!(metric.loc.cloc(), 0);
7837                assert_eq!(metric.loc.blank(), 2);
7838                insta::assert_json_snapshot!(metric.loc);
7839            },
7840        );
7841    }
7842
7843    #[cfg(feature = "kotlin")]
7844    #[test]
7845    fn kotlin_loc_cloc() {
7846        check_metrics::<KotlinParser>(
7847            "// header comment
7848        /* block
7849           comment */
7850        fun f(): Int {
7851            return 42 // inline
7852        }",
7853            "foo.kt",
7854            |metric| {
7855                assert_eq!(metric.loc.sloc(), 6);
7856                assert_eq!(metric.loc.ploc(), 3);
7857                assert_eq!(metric.loc.lloc(), 1);
7858                assert_eq!(metric.loc.cloc(), 4);
7859                assert_eq!(metric.loc.blank(), 0);
7860                insta::assert_json_snapshot!(metric.loc);
7861            },
7862        );
7863    }
7864
7865    #[cfg(feature = "kotlin")]
7866    #[test]
7867    fn kotlin_loc_no_zero_blank() {
7868        // Checks that the blank metric is not equal to 0 when there are some
7869        // comments next to code lines. Mirrors rust_no_zero_blank.
7870        check_metrics::<KotlinParser>(
7871            "fun connectToUpdateServer() {
7872              val pool = 0
7873
7874              val updateServer = -42
7875              val isConnected = false
7876              val currTry = 0
7877              val numRetries = 10  // Number of IPC connection retries before
7878                                    // giving up.
7879              val numTries = 20    // Number of IPC connection tries before
7880                                    // giving up.
7881            }",
7882            "foo.kt",
7883            |metric| {
7884                // Anchor the headline integer values; in particular
7885                // `blank() > 0` is the contract this test's name advertises.
7886                assert_eq!(metric.loc.sloc(), 11);
7887                assert_eq!(metric.loc.ploc(), 8);
7888                assert_eq!(metric.loc.cloc(), 4);
7889                assert_eq!(metric.loc.blank(), 1);
7890                insta::assert_json_snapshot!(
7891                    metric.loc,
7892                    @r#"
7893                {
7894                  "sloc": 11,
7895                  "ploc": 8,
7896                  "lloc": 6,
7897                  "cloc": 4,
7898                  "blank": 1,
7899                  "sloc_average": 5.5,
7900                  "ploc_average": 4.0,
7901                  "lloc_average": 3.0,
7902                  "cloc_average": 2.0,
7903                  "blank_average": 0.5,
7904                  "sloc_min": 11,
7905                  "sloc_max": 11,
7906                  "cloc_min": 4,
7907                  "cloc_max": 4,
7908                  "ploc_min": 8,
7909                  "ploc_max": 8,
7910                  "lloc_min": 6,
7911                  "lloc_max": 6,
7912                  "blank_min": 1,
7913                  "blank_max": 1
7914                }
7915                "#
7916                );
7917            },
7918        );
7919    }
7920
7921    #[cfg(feature = "kotlin")]
7922    #[test]
7923    fn kotlin_loc_blank_zero_sanity() {
7924        // Sanity: when the source has no blank lines, blank() must be 0.
7925        // Preserves the no-blank coverage previously held by
7926        // kotlin_loc_no_zero_blank before it was rewritten to assert the
7927        // positive case its name advertises.
7928        check_metrics::<KotlinParser>(
7929            "fun f(): Int {
7930            val x = 1 // x
7931            val y = 2 // y
7932            return x + y
7933        }",
7934            "foo.kt",
7935            |metric| {
7936                assert_eq!(metric.loc.sloc(), 5);
7937                assert_eq!(metric.loc.ploc(), 5);
7938                assert_eq!(metric.loc.lloc(), 3);
7939                assert_eq!(metric.loc.cloc(), 2);
7940                assert_eq!(metric.loc.blank(), 0);
7941            },
7942        );
7943    }
7944
7945    #[cfg(feature = "kotlin")]
7946    #[test]
7947    fn kotlin_loc_if_lloc() {
7948        check_metrics::<KotlinParser>(
7949            "fun classify(n: Int): String {
7950            if (n > 0) {
7951                return \"positive\"
7952            } else if (n < 0) {
7953                return \"negative\"
7954            }
7955            return \"zero\"
7956        }",
7957            "foo.kt",
7958            |metric| {
7959                assert_eq!(metric.loc.sloc(), 8);
7960                assert_eq!(metric.loc.ploc(), 8);
7961                assert_eq!(metric.loc.lloc(), 5);
7962                assert_eq!(metric.loc.cloc(), 0);
7963                assert_eq!(metric.loc.blank(), 0);
7964                insta::assert_json_snapshot!(metric.loc);
7965            },
7966        );
7967    }
7968
7969    #[cfg(feature = "kotlin")]
7970    #[test]
7971    fn kotlin_loc_for_lloc() {
7972        check_metrics::<KotlinParser>(
7973            "fun sum(n: Int): Int {
7974            var s = 0
7975            for (i in 1..n) {
7976                s += i
7977            }
7978            return s
7979        }",
7980            "foo.kt",
7981            |metric| {
7982                assert_eq!(metric.loc.sloc(), 7);
7983                assert_eq!(metric.loc.ploc(), 7);
7984                assert_eq!(metric.loc.lloc(), 4);
7985                assert_eq!(metric.loc.cloc(), 0);
7986                assert_eq!(metric.loc.blank(), 0);
7987                insta::assert_json_snapshot!(metric.loc);
7988            },
7989        );
7990    }
7991
7992    #[cfg(feature = "kotlin")]
7993    #[test]
7994    fn kotlin_loc_when_lloc() {
7995        check_metrics::<KotlinParser>(
7996            "fun describe(x: Int): String {
7997            return when (x) {
7998                1 -> \"one\"
7999                2 -> \"two\"
8000                else -> \"other\"
8001            }
8002        }",
8003            "foo.kt",
8004            |metric| {
8005                assert_eq!(metric.loc.sloc(), 7);
8006                assert_eq!(metric.loc.ploc(), 7);
8007                assert_eq!(metric.loc.lloc(), 2);
8008                assert_eq!(metric.loc.cloc(), 0);
8009                assert_eq!(metric.loc.blank(), 0);
8010                insta::assert_json_snapshot!(metric.loc);
8011            },
8012        );
8013    }
8014
8015    #[cfg(feature = "kotlin")]
8016    #[test]
8017    fn kotlin_loc_lambda_lloc() {
8018        check_metrics::<KotlinParser>(
8019            "fun f(list: List<Int>): List<Int> {
8020            return list.filter { it > 0 }
8021                       .map { it * 2 }
8022        }",
8023            "foo.kt",
8024            |metric| {
8025                assert_eq!(metric.loc.sloc(), 4);
8026                assert_eq!(metric.loc.ploc(), 4);
8027                assert_eq!(metric.loc.lloc(), 1);
8028                assert_eq!(metric.loc.cloc(), 0);
8029                assert_eq!(metric.loc.blank(), 0);
8030                insta::assert_json_snapshot!(metric.loc);
8031            },
8032        );
8033    }
8034
8035    #[cfg(feature = "kotlin")]
8036    #[test]
8037    fn kotlin_loc_class_loc() {
8038        check_metrics::<KotlinParser>(
8039            "class Counter {
8040            private var count = 0
8041            fun increment() { count++ }
8042            fun get(): Int = count
8043        }",
8044            "foo.kt",
8045            |metric| {
8046                assert_eq!(metric.loc.sloc(), 5);
8047                assert_eq!(metric.loc.ploc(), 5);
8048                assert_eq!(metric.loc.lloc(), 1);
8049                assert_eq!(metric.loc.cloc(), 0);
8050                assert_eq!(metric.loc.blank(), 0);
8051                insta::assert_json_snapshot!(metric.loc);
8052            },
8053        );
8054    }
8055
8056    #[cfg(feature = "kotlin")]
8057    #[test]
8058    fn kotlin_loc_multiple_functions_loc() {
8059        check_metrics::<KotlinParser>(
8060            "fun f(): Int {
8061            return 1
8062        }
8063        fun g(): Int {
8064            return 2
8065        }",
8066            "foo.kt",
8067            |metric| {
8068                assert_eq!(metric.loc.sloc(), 6);
8069                assert_eq!(metric.loc.ploc(), 6);
8070                assert_eq!(metric.loc.lloc(), 2);
8071                assert_eq!(metric.loc.cloc(), 0);
8072                assert_eq!(metric.loc.blank(), 0);
8073                insta::assert_json_snapshot!(metric.loc);
8074            },
8075        );
8076    }
8077
8078    #[cfg(feature = "kotlin")]
8079    #[test]
8080    fn kotlin_loc_while_lloc() {
8081        check_metrics::<KotlinParser>(
8082            "fun countdown(n: Int) {
8083            var i = n
8084            while (i > 0) {
8085                println(i)
8086                i--
8087            }
8088        }",
8089            "foo.kt",
8090            |metric| {
8091                assert_eq!(metric.loc.sloc(), 7);
8092                assert_eq!(metric.loc.ploc(), 7);
8093                assert_eq!(metric.loc.lloc(), 3);
8094                assert_eq!(metric.loc.cloc(), 0);
8095                assert_eq!(metric.loc.blank(), 0);
8096                insta::assert_json_snapshot!(metric.loc);
8097            },
8098        );
8099    }
8100
8101    #[cfg(feature = "typescript")]
8102    #[test]
8103    fn typescript_blank() {
8104        check_metrics::<TypescriptParser>(
8105            "function f(): void {
8106
8107            const x = 1;
8108
8109        }",
8110            "foo.ts",
8111            |metric| {
8112                assert_eq!(metric.loc.sloc(), 5);
8113                assert_eq!(metric.loc.ploc(), 3);
8114                assert_eq!(metric.loc.lloc(), 1);
8115                assert_eq!(metric.loc.cloc(), 0);
8116                assert_eq!(metric.loc.blank(), 2);
8117                insta::assert_json_snapshot!(metric.loc);
8118            },
8119        );
8120    }
8121
8122    #[cfg(feature = "typescript")]
8123    #[test]
8124    fn typescript_cloc() {
8125        check_metrics::<TypescriptParser>(
8126            "// header
8127        /* block
8128           comment */
8129        function f(): number {
8130            return 42; // inline
8131        }",
8132            "foo.ts",
8133            |metric| {
8134                assert_eq!(metric.loc.sloc(), 6);
8135                assert_eq!(metric.loc.ploc(), 3);
8136                assert_eq!(metric.loc.lloc(), 1);
8137                assert_eq!(metric.loc.cloc(), 4);
8138                assert_eq!(metric.loc.blank(), 0);
8139                insta::assert_json_snapshot!(metric.loc);
8140            },
8141        );
8142    }
8143
8144    #[cfg(feature = "typescript")]
8145    #[test]
8146    fn typescript_no_zero_blank() {
8147        // Blank line interleaved with code that carries trailing comments —
8148        // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
8149        check_metrics::<TypescriptParser>(
8150            "function f(): void {
8151  const a = 1;
8152
8153  const b = 2; // trailing
8154  const c = 3; // trailing
8155}",
8156            "foo.ts",
8157            |metric| {
8158                assert_eq!(metric.loc.sloc(), 6);
8159                assert_eq!(metric.loc.ploc(), 5);
8160                assert_eq!(metric.loc.cloc(), 2);
8161                assert_eq!(metric.loc.blank(), 1);
8162                insta::assert_json_snapshot!(metric.loc);
8163            },
8164        );
8165    }
8166
8167    #[cfg(feature = "typescript")]
8168    #[test]
8169    fn typescript_if_lloc() {
8170        check_metrics::<TypescriptParser>(
8171            "function classify(n: number): string {
8172            if (n > 0) {
8173                return 'positive';
8174            } else {
8175                return 'non-positive';
8176            }
8177        }",
8178            "foo.ts",
8179            |metric| {
8180                assert_eq!(metric.loc.sloc(), 7);
8181                assert_eq!(metric.loc.ploc(), 7);
8182                assert_eq!(metric.loc.lloc(), 3);
8183                assert_eq!(metric.loc.cloc(), 0);
8184                assert_eq!(metric.loc.blank(), 0);
8185                insta::assert_json_snapshot!(metric.loc);
8186            },
8187        );
8188    }
8189
8190    #[cfg(feature = "typescript")]
8191    #[test]
8192    fn typescript_for_lloc() {
8193        check_metrics::<TypescriptParser>(
8194            "function sum(n: number): number {
8195            let s = 0;
8196            for (let i = 0; i < n; i++) {
8197                s += i;
8198            }
8199            return s;
8200        }",
8201            "foo.ts",
8202            |metric| {
8203                assert_eq!(metric.loc.sloc(), 7);
8204                assert_eq!(metric.loc.ploc(), 7);
8205                assert_eq!(metric.loc.lloc(), 4);
8206                assert_eq!(metric.loc.cloc(), 0);
8207                assert_eq!(metric.loc.blank(), 0);
8208                insta::assert_json_snapshot!(metric.loc);
8209            },
8210        );
8211    }
8212
8213    #[cfg(feature = "typescript")]
8214    #[test]
8215    fn typescript_while_lloc() {
8216        check_metrics::<TypescriptParser>(
8217            "function countdown(n: number): void {
8218            let i = n;
8219            while (i > 0) {
8220                console.log(i);
8221                i--;
8222            }
8223        }",
8224            "foo.ts",
8225            |metric| {
8226                assert_eq!(metric.loc.sloc(), 7);
8227                assert_eq!(metric.loc.ploc(), 7);
8228                assert_eq!(metric.loc.lloc(), 4);
8229                assert_eq!(metric.loc.cloc(), 0);
8230                assert_eq!(metric.loc.blank(), 0);
8231                insta::assert_json_snapshot!(metric.loc);
8232            },
8233        );
8234    }
8235
8236    #[cfg(feature = "typescript")]
8237    #[test]
8238    fn typescript_switch_lloc() {
8239        check_metrics::<TypescriptParser>(
8240            "function describe(x: number): string {
8241            switch (x) {
8242                case 1: return 'one';
8243                case 2: return 'two';
8244                default: return 'other';
8245            }
8246        }",
8247            "foo.ts",
8248            |metric| {
8249                assert_eq!(metric.loc.sloc(), 7);
8250                assert_eq!(metric.loc.ploc(), 7);
8251                assert_eq!(metric.loc.lloc(), 4);
8252                assert_eq!(metric.loc.cloc(), 0);
8253                assert_eq!(metric.loc.blank(), 0);
8254                insta::assert_json_snapshot!(metric.loc);
8255            },
8256        );
8257    }
8258
8259    #[cfg(feature = "typescript")]
8260    #[test]
8261    fn typescript_class_loc() {
8262        check_metrics::<TypescriptParser>(
8263            "class Counter {
8264            private count: number = 0;
8265            increment(): void { this.count++; }
8266            get(): number { return this.count; }
8267        }",
8268            "foo.ts",
8269            |metric| {
8270                assert_eq!(metric.loc.sloc(), 5);
8271                assert_eq!(metric.loc.ploc(), 5);
8272                assert_eq!(metric.loc.lloc(), 2);
8273                assert_eq!(metric.loc.cloc(), 0);
8274                assert_eq!(metric.loc.blank(), 0);
8275                insta::assert_json_snapshot!(metric.loc);
8276            },
8277        );
8278    }
8279
8280    #[cfg(feature = "typescript")]
8281    #[test]
8282    fn typescript_arrow_function_loc() {
8283        check_metrics::<TypescriptParser>(
8284            "const add = (a: number, b: number): number => a + b;
8285        const greet = (name: string): string => {
8286            return `Hello, ${name}`;
8287        };",
8288            "foo.ts",
8289            |metric| {
8290                assert_eq!(metric.loc.sloc(), 4);
8291                assert_eq!(metric.loc.ploc(), 4);
8292                assert_eq!(metric.loc.lloc(), 3);
8293                assert_eq!(metric.loc.cloc(), 0);
8294                assert_eq!(metric.loc.blank(), 0);
8295                insta::assert_json_snapshot!(metric.loc);
8296            },
8297        );
8298    }
8299
8300    #[cfg(feature = "typescript")]
8301    #[test]
8302    fn typescript_interface_loc() {
8303        check_metrics::<TypescriptParser>(
8304            "interface Shape {
8305            area(): number;
8306            perimeter(): number;
8307        }
8308        function describe(s: Shape): string {
8309            return `area=${s.area()}`;
8310        }",
8311            "foo.ts",
8312            |metric| {
8313                assert_eq!(metric.loc.sloc(), 7);
8314                assert_eq!(metric.loc.ploc(), 7);
8315                assert_eq!(metric.loc.lloc(), 1);
8316                assert_eq!(metric.loc.cloc(), 0);
8317                assert_eq!(metric.loc.blank(), 0);
8318                insta::assert_json_snapshot!(metric.loc);
8319            },
8320        );
8321    }
8322
8323    #[cfg(feature = "typescript")]
8324    #[test]
8325    fn typescript_multiple_functions_loc() {
8326        check_metrics::<TypescriptParser>(
8327            "function f(): number {
8328            return 1;
8329        }
8330        function g(): number {
8331            return 2;
8332        }
8333        function h(): number {
8334            return 3;
8335        }",
8336            "foo.ts",
8337            |metric| {
8338                assert_eq!(metric.loc.sloc(), 9);
8339                assert_eq!(metric.loc.ploc(), 9);
8340                assert_eq!(metric.loc.lloc(), 3);
8341                assert_eq!(metric.loc.cloc(), 0);
8342                assert_eq!(metric.loc.blank(), 0);
8343                insta::assert_json_snapshot!(metric.loc);
8344            },
8345        );
8346    }
8347
8348    #[cfg(feature = "typescript")]
8349    #[test]
8350    fn typescript_try_catch_lloc() {
8351        check_metrics::<TypescriptParser>(
8352            "function safe(x: number): number {
8353            try {
8354                return 1 / x;
8355            } catch (e) {
8356                return 0;
8357            }
8358        }",
8359            "foo.ts",
8360            |metric| {
8361                assert_eq!(metric.loc.sloc(), 7);
8362                assert_eq!(metric.loc.ploc(), 7);
8363                assert_eq!(metric.loc.lloc(), 3);
8364                assert_eq!(metric.loc.cloc(), 0);
8365                assert_eq!(metric.loc.blank(), 0);
8366                insta::assert_json_snapshot!(metric.loc);
8367            },
8368        );
8369    }
8370
8371    #[cfg(feature = "typescript")]
8372    #[test]
8373    fn typescript_nested_functions_loc() {
8374        check_metrics::<TypescriptParser>(
8375            "function outer(x: number): number {
8376            function inner(y: number): number {
8377                return y * 2;
8378            }
8379            return inner(x) + 1;
8380        }",
8381            "foo.ts",
8382            |metric| {
8383                assert_eq!(metric.loc.sloc(), 6);
8384                assert_eq!(metric.loc.ploc(), 6);
8385                assert_eq!(metric.loc.lloc(), 2);
8386                assert_eq!(metric.loc.cloc(), 0);
8387                assert_eq!(metric.loc.blank(), 0);
8388                insta::assert_json_snapshot!(metric.loc);
8389            },
8390        );
8391    }
8392
8393    #[cfg(feature = "typescript")]
8394    #[test]
8395    fn typescript_generic_function_loc() {
8396        check_metrics::<TypescriptParser>(
8397            "function identity<T>(value: T): T {
8398            return value;
8399        }
8400        function first<T>(arr: T[]): T | undefined {
8401            return arr[0];
8402        }",
8403            "foo.ts",
8404            |metric| {
8405                assert_eq!(metric.loc.sloc(), 6);
8406                assert_eq!(metric.loc.ploc(), 6);
8407                assert_eq!(metric.loc.lloc(), 2);
8408                assert_eq!(metric.loc.cloc(), 0);
8409                assert_eq!(metric.loc.blank(), 0);
8410                insta::assert_json_snapshot!(metric.loc);
8411            },
8412        );
8413    }
8414
8415    #[cfg(feature = "typescript")]
8416    #[test]
8417    fn tsx_blank() {
8418        check_metrics::<TsxParser>(
8419            "function f(): void {
8420
8421            const x = 1;
8422
8423        }",
8424            "foo.tsx",
8425            |metric| {
8426                assert_eq!(metric.loc.sloc(), 5);
8427                assert_eq!(metric.loc.ploc(), 3);
8428                assert_eq!(metric.loc.lloc(), 1);
8429                assert_eq!(metric.loc.cloc(), 0);
8430                assert_eq!(metric.loc.blank(), 2);
8431                insta::assert_json_snapshot!(metric.loc);
8432            },
8433        );
8434    }
8435
8436    #[cfg(feature = "typescript")]
8437    #[test]
8438    fn tsx_cloc() {
8439        check_metrics::<TsxParser>(
8440            "// header
8441        /* block
8442           comment */
8443        function f(): number {
8444            return 42; // inline
8445        }",
8446            "foo.tsx",
8447            |metric| {
8448                assert_eq!(metric.loc.sloc(), 6);
8449                assert_eq!(metric.loc.ploc(), 3);
8450                assert_eq!(metric.loc.lloc(), 1);
8451                assert_eq!(metric.loc.cloc(), 4);
8452                assert_eq!(metric.loc.blank(), 0);
8453                insta::assert_json_snapshot!(metric.loc);
8454            },
8455        );
8456    }
8457
8458    #[cfg(feature = "typescript")]
8459    #[test]
8460    fn tsx_no_zero_blank() {
8461        // Blank line interleaved with code that carries trailing comments —
8462        // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
8463        check_metrics::<TsxParser>(
8464            "function f(): void {
8465  const a = 1;
8466
8467  const b = 2; // trailing
8468  const c = 3; // trailing
8469}",
8470            "foo.tsx",
8471            |metric| {
8472                assert_eq!(metric.loc.sloc(), 6);
8473                assert_eq!(metric.loc.ploc(), 5);
8474                assert_eq!(metric.loc.cloc(), 2);
8475                assert_eq!(metric.loc.blank(), 1);
8476                insta::assert_json_snapshot!(metric.loc);
8477            },
8478        );
8479    }
8480
8481    #[cfg(feature = "typescript")]
8482    #[test]
8483    fn tsx_if_lloc() {
8484        check_metrics::<TsxParser>(
8485            "function classify(n: number): string {
8486            if (n > 0) {
8487                return 'positive';
8488            } else {
8489                return 'non-positive';
8490            }
8491        }",
8492            "foo.tsx",
8493            |metric| {
8494                assert_eq!(metric.loc.sloc(), 7);
8495                assert_eq!(metric.loc.ploc(), 7);
8496                assert_eq!(metric.loc.lloc(), 3);
8497                assert_eq!(metric.loc.cloc(), 0);
8498                assert_eq!(metric.loc.blank(), 0);
8499                insta::assert_json_snapshot!(metric.loc);
8500            },
8501        );
8502    }
8503
8504    #[cfg(feature = "typescript")]
8505    #[test]
8506    fn tsx_for_lloc() {
8507        check_metrics::<TsxParser>(
8508            "function sum(n: number): number {
8509            let s = 0;
8510            for (let i = 0; i < n; i++) {
8511                s += i;
8512            }
8513            return s;
8514        }",
8515            "foo.tsx",
8516            |metric| {
8517                assert_eq!(metric.loc.sloc(), 7);
8518                assert_eq!(metric.loc.ploc(), 7);
8519                assert_eq!(metric.loc.lloc(), 4);
8520                assert_eq!(metric.loc.cloc(), 0);
8521                assert_eq!(metric.loc.blank(), 0);
8522                insta::assert_json_snapshot!(metric.loc);
8523            },
8524        );
8525    }
8526
8527    #[cfg(feature = "typescript")]
8528    #[test]
8529    fn tsx_while_lloc() {
8530        check_metrics::<TsxParser>(
8531            "function countdown(n: number): void {
8532            let i = n;
8533            while (i > 0) {
8534                console.log(i);
8535                i--;
8536            }
8537        }",
8538            "foo.tsx",
8539            |metric| {
8540                assert_eq!(metric.loc.sloc(), 7);
8541                assert_eq!(metric.loc.ploc(), 7);
8542                assert_eq!(metric.loc.lloc(), 4);
8543                assert_eq!(metric.loc.cloc(), 0);
8544                assert_eq!(metric.loc.blank(), 0);
8545                insta::assert_json_snapshot!(metric.loc);
8546            },
8547        );
8548    }
8549
8550    #[cfg(feature = "typescript")]
8551    #[test]
8552    fn tsx_switch_lloc() {
8553        check_metrics::<TsxParser>(
8554            "function describe(x: number): string {
8555            switch (x) {
8556                case 1: return 'one';
8557                case 2: return 'two';
8558                default: return 'other';
8559            }
8560        }",
8561            "foo.tsx",
8562            |metric| {
8563                assert_eq!(metric.loc.sloc(), 7);
8564                assert_eq!(metric.loc.ploc(), 7);
8565                assert_eq!(metric.loc.lloc(), 4);
8566                assert_eq!(metric.loc.cloc(), 0);
8567                assert_eq!(metric.loc.blank(), 0);
8568                insta::assert_json_snapshot!(metric.loc);
8569            },
8570        );
8571    }
8572
8573    #[cfg(feature = "typescript")]
8574    #[test]
8575    fn tsx_class_loc() {
8576        check_metrics::<TsxParser>(
8577            "class Counter {
8578            private count: number = 0;
8579            increment(): void { this.count++; }
8580            get(): number { return this.count; }
8581        }",
8582            "foo.tsx",
8583            |metric| {
8584                assert_eq!(metric.loc.sloc(), 5);
8585                assert_eq!(metric.loc.ploc(), 5);
8586                assert_eq!(metric.loc.lloc(), 2);
8587                assert_eq!(metric.loc.cloc(), 0);
8588                assert_eq!(metric.loc.blank(), 0);
8589                insta::assert_json_snapshot!(metric.loc);
8590            },
8591        );
8592    }
8593
8594    #[cfg(feature = "typescript")]
8595    #[test]
8596    fn tsx_arrow_function_loc() {
8597        check_metrics::<TsxParser>(
8598            "const add = (a: number, b: number): number => a + b;
8599        const greet = (name: string): string => {
8600            return `Hello, ${name}`;
8601        };",
8602            "foo.tsx",
8603            |metric| {
8604                assert_eq!(metric.loc.sloc(), 4);
8605                assert_eq!(metric.loc.ploc(), 4);
8606                assert_eq!(metric.loc.lloc(), 3);
8607                assert_eq!(metric.loc.cloc(), 0);
8608                assert_eq!(metric.loc.blank(), 0);
8609                insta::assert_json_snapshot!(metric.loc);
8610            },
8611        );
8612    }
8613
8614    #[cfg(feature = "typescript")]
8615    #[test]
8616    fn tsx_multiple_functions_loc() {
8617        check_metrics::<TsxParser>(
8618            "function f(): number {
8619            return 1;
8620        }
8621        function g(): number {
8622            return 2;
8623        }
8624        function h(): number {
8625            return 3;
8626        }",
8627            "foo.tsx",
8628            |metric| {
8629                assert_eq!(metric.loc.sloc(), 9);
8630                assert_eq!(metric.loc.ploc(), 9);
8631                assert_eq!(metric.loc.lloc(), 3);
8632                assert_eq!(metric.loc.cloc(), 0);
8633                assert_eq!(metric.loc.blank(), 0);
8634                insta::assert_json_snapshot!(metric.loc);
8635            },
8636        );
8637    }
8638
8639    #[cfg(feature = "typescript")]
8640    #[test]
8641    fn tsx_try_catch_lloc() {
8642        check_metrics::<TsxParser>(
8643            "function safe(x: number): number {
8644            try {
8645                return 1 / x;
8646            } catch (e) {
8647                return 0;
8648            }
8649        }",
8650            "foo.tsx",
8651            |metric| {
8652                assert_eq!(metric.loc.sloc(), 7);
8653                assert_eq!(metric.loc.ploc(), 7);
8654                assert_eq!(metric.loc.lloc(), 3);
8655                assert_eq!(metric.loc.cloc(), 0);
8656                assert_eq!(metric.loc.blank(), 0);
8657                insta::assert_json_snapshot!(metric.loc);
8658            },
8659        );
8660    }
8661
8662    #[cfg(feature = "typescript")]
8663    #[test]
8664    fn tsx_nested_functions_loc() {
8665        check_metrics::<TsxParser>(
8666            "function outer(x: number): number {
8667            function inner(y: number): number {
8668                return y * 2;
8669            }
8670            return inner(x) + 1;
8671        }",
8672            "foo.tsx",
8673            |metric| {
8674                assert_eq!(metric.loc.sloc(), 6);
8675                assert_eq!(metric.loc.ploc(), 6);
8676                assert_eq!(metric.loc.lloc(), 2);
8677                assert_eq!(metric.loc.cloc(), 0);
8678                assert_eq!(metric.loc.blank(), 0);
8679                insta::assert_json_snapshot!(metric.loc);
8680            },
8681        );
8682    }
8683
8684    #[cfg(feature = "typescript")]
8685    #[test]
8686    fn tsx_interface_loc() {
8687        check_metrics::<TsxParser>(
8688            "interface Shape {
8689            area(): number;
8690            perimeter(): number;
8691        }
8692        function describe(s: Shape): string {
8693            return `area=${s.area()}`;
8694        }",
8695            "foo.tsx",
8696            |metric| {
8697                assert_eq!(metric.loc.sloc(), 7);
8698                assert_eq!(metric.loc.ploc(), 7);
8699                assert_eq!(metric.loc.lloc(), 1);
8700                assert_eq!(metric.loc.cloc(), 0);
8701                assert_eq!(metric.loc.blank(), 0);
8702                insta::assert_json_snapshot!(metric.loc);
8703            },
8704        );
8705    }
8706
8707    #[cfg(feature = "typescript")]
8708    #[test]
8709    fn tsx_generic_function_loc() {
8710        check_metrics::<TsxParser>(
8711            "function identity<T>(value: T): T {
8712            return value;
8713        }
8714        function first<T>(arr: T[]): T | undefined {
8715            return arr[0];
8716        }",
8717            "foo.tsx",
8718            |metric| {
8719                assert_eq!(metric.loc.sloc(), 6);
8720                assert_eq!(metric.loc.ploc(), 6);
8721                assert_eq!(metric.loc.lloc(), 2);
8722                assert_eq!(metric.loc.cloc(), 0);
8723                assert_eq!(metric.loc.blank(), 0);
8724                insta::assert_json_snapshot!(metric.loc);
8725            },
8726        );
8727    }
8728
8729    #[cfg(feature = "php")]
8730    #[test]
8731    fn php_blank() {
8732        check_metrics::<PhpParser>(
8733            "<?php
8734
8735$a = 1;
8736
8737$b = 2;
8738
8739",
8740            "foo.php",
8741            |metric| {
8742                assert_eq!(metric.loc.sloc(), 5);
8743                assert_eq!(metric.loc.ploc(), 3);
8744                assert_eq!(metric.loc.lloc(), 2);
8745                assert_eq!(metric.loc.cloc(), 0);
8746                assert_eq!(metric.loc.blank(), 2);
8747                insta::assert_json_snapshot!(metric.loc);
8748            },
8749        );
8750    }
8751
8752    #[cfg(feature = "php")]
8753    #[test]
8754    fn php_no_zero_blank() {
8755        // Blank line interleaved with code that carries trailing comments —
8756        // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
8757        check_metrics::<PhpParser>(
8758            "<?php
8759$a = 1;
8760
8761$b = 2; // trailing
8762$c = 3; // trailing
8763",
8764            "foo.php",
8765            |metric| {
8766                assert_eq!(metric.loc.sloc(), 5);
8767                assert_eq!(metric.loc.ploc(), 4);
8768                assert_eq!(metric.loc.cloc(), 2);
8769                assert_eq!(metric.loc.blank(), 1);
8770                insta::assert_json_snapshot!(metric.loc);
8771            },
8772        );
8773    }
8774
8775    #[cfg(feature = "php")]
8776    #[test]
8777    fn php_cloc_double_slash() {
8778        check_metrics::<PhpParser>(
8779            "<?php
8780// first
8781// second
8782$a = 1; // trailing",
8783            "foo.php",
8784            |metric| {
8785                assert_eq!(metric.loc.sloc(), 4);
8786                assert_eq!(metric.loc.ploc(), 2);
8787                assert_eq!(metric.loc.lloc(), 1);
8788                assert_eq!(metric.loc.cloc(), 3);
8789                assert_eq!(metric.loc.blank(), 0);
8790                insta::assert_json_snapshot!(metric.loc);
8791            },
8792        );
8793    }
8794
8795    #[cfg(feature = "php")]
8796    #[test]
8797    fn php_cloc_hash() {
8798        check_metrics::<PhpParser>(
8799            "<?php
8800# first
8801# second
8802$a = 1;",
8803            "foo.php",
8804            |metric| {
8805                assert_eq!(metric.loc.sloc(), 4);
8806                assert_eq!(metric.loc.ploc(), 2);
8807                assert_eq!(metric.loc.lloc(), 1);
8808                assert_eq!(metric.loc.cloc(), 2);
8809                assert_eq!(metric.loc.blank(), 0);
8810                insta::assert_json_snapshot!(metric.loc);
8811            },
8812        );
8813    }
8814
8815    #[cfg(feature = "php")]
8816    #[test]
8817    fn php_cloc_block() {
8818        check_metrics::<PhpParser>(
8819            "<?php
8820/*
8821 * block
8822 * comment
8823 */
8824$a = 1;",
8825            "foo.php",
8826            |metric| {
8827                assert_eq!(metric.loc.sloc(), 6);
8828                assert_eq!(metric.loc.ploc(), 2);
8829                assert_eq!(metric.loc.lloc(), 1);
8830                assert_eq!(metric.loc.cloc(), 4);
8831                assert_eq!(metric.loc.blank(), 0);
8832                insta::assert_json_snapshot!(metric.loc);
8833            },
8834        );
8835    }
8836
8837    #[cfg(feature = "php")]
8838    #[test]
8839    fn php_lloc() {
8840        // Three statements: assignment, if (with body), echo.
8841        check_metrics::<PhpParser>(
8842            "<?php
8843$a = 1;
8844if ($a > 0) {
8845    echo $a;
8846}",
8847            "foo.php",
8848            |metric| {
8849                assert_eq!(metric.loc.sloc(), 5);
8850                assert_eq!(metric.loc.ploc(), 5);
8851                assert_eq!(metric.loc.lloc(), 3);
8852                assert_eq!(metric.loc.cloc(), 0);
8853                assert_eq!(metric.loc.blank(), 0);
8854                insta::assert_json_snapshot!(metric.loc);
8855            },
8856        );
8857    }
8858
8859    #[cfg(feature = "php")]
8860    #[test]
8861    fn php_no_parenthesized_expression_lloc() {
8862        // Parenthesized expression should not add an extra LLOC over the
8863        // surrounding expression_statement.
8864        check_metrics::<PhpParser>(
8865            "<?php
8866$a = (1 + 2);",
8867            "foo.php",
8868            |metric| {
8869                assert_eq!(metric.loc.sloc(), 2);
8870                assert_eq!(metric.loc.ploc(), 2);
8871                assert_eq!(metric.loc.lloc(), 1);
8872                assert_eq!(metric.loc.cloc(), 0);
8873                assert_eq!(metric.loc.blank(), 0);
8874                insta::assert_json_snapshot!(metric.loc);
8875            },
8876        );
8877    }
8878
8879    #[cfg(feature = "php")]
8880    #[test]
8881    fn php_no_compound_statement_lloc() {
8882        // Block wrappers (`{ … }`) are not LLOC themselves.
8883        check_metrics::<PhpParser>(
8884            "<?php
8885function f(): void {
8886    $a = 1;
8887}",
8888            "foo.php",
8889            |metric| {
8890                assert_eq!(metric.loc.sloc(), 4);
8891                assert_eq!(metric.loc.ploc(), 4);
8892                assert_eq!(metric.loc.lloc(), 1);
8893                assert_eq!(metric.loc.cloc(), 0);
8894                assert_eq!(metric.loc.blank(), 0);
8895                insta::assert_json_snapshot!(metric.loc);
8896            },
8897        );
8898    }
8899
8900    #[cfg(feature = "php")]
8901    #[test]
8902    fn php_no_colon_block_lloc() {
8903        // Alternative syntax (`if: … endif;`) uses ColonBlock instead of
8904        // CompoundStatement; it is also not LLOC.
8905        check_metrics::<PhpParser>(
8906            "<?php
8907if (true):
8908    $a = 1;
8909endif;",
8910            "foo.php",
8911            |metric| {
8912                assert_eq!(metric.loc.sloc(), 4);
8913                assert_eq!(metric.loc.ploc(), 4);
8914                assert_eq!(metric.loc.lloc(), 2);
8915                assert_eq!(metric.loc.cloc(), 0);
8916                assert_eq!(metric.loc.blank(), 0);
8917                insta::assert_json_snapshot!(metric.loc);
8918            },
8919        );
8920    }
8921
8922    #[cfg(feature = "php")]
8923    #[test]
8924    fn php_no_else_clause_lloc() {
8925        // ElseClause and ElseIfClause are sub-parts of IfStatement.
8926        check_metrics::<PhpParser>(
8927            "<?php
8928if ($x) {
8929    $a = 1;
8930} elseif ($y) {
8931    $a = 2;
8932} else {
8933    $a = 3;
8934}",
8935            "foo.php",
8936            |metric| {
8937                assert_eq!(metric.loc.sloc(), 8);
8938                assert_eq!(metric.loc.ploc(), 8);
8939                assert_eq!(metric.loc.lloc(), 4);
8940                assert_eq!(metric.loc.cloc(), 0);
8941                assert_eq!(metric.loc.blank(), 0);
8942                insta::assert_json_snapshot!(metric.loc);
8943            },
8944        );
8945    }
8946
8947    #[cfg(feature = "php")]
8948    #[test]
8949    fn php_no_case_statement_lloc() {
8950        // CaseStatement / DefaultStatement are switch arms, not separate
8951        // statements.
8952        check_metrics::<PhpParser>(
8953            "<?php
8954switch ($x) {
8955    case 1:
8956        $a = 1;
8957        break;
8958    case 2:
8959        $a = 2;
8960        break;
8961    default:
8962        $a = 0;
8963}",
8964            "foo.php",
8965            |metric| {
8966                assert_eq!(metric.loc.sloc(), 11);
8967                assert_eq!(metric.loc.ploc(), 11);
8968                assert_eq!(metric.loc.lloc(), 6);
8969                assert_eq!(metric.loc.cloc(), 0);
8970                assert_eq!(metric.loc.blank(), 0);
8971                insta::assert_json_snapshot!(metric.loc);
8972            },
8973        );
8974    }
8975
8976    #[cfg(feature = "php")]
8977    #[test]
8978    fn php_no_match_arm_lloc() {
8979        // MatchConditionalExpression / MatchDefaultExpression are arms;
8980        // only the surrounding expression_statement counts.
8981        check_metrics::<PhpParser>(
8982            "<?php
8983$a = match ($x) {
8984    1 => 'one',
8985    2 => 'two',
8986    default => 'other',
8987};",
8988            "foo.php",
8989            |metric| {
8990                assert_eq!(metric.loc.sloc(), 6);
8991                assert_eq!(metric.loc.ploc(), 6);
8992                assert_eq!(metric.loc.lloc(), 1);
8993                assert_eq!(metric.loc.cloc(), 0);
8994                assert_eq!(metric.loc.blank(), 0);
8995                insta::assert_json_snapshot!(metric.loc);
8996            },
8997        );
8998    }
8999
9000    #[cfg(feature = "php")]
9001    #[test]
9002    fn php_no_throw_in_expression_lloc() {
9003        // PHP 8 `throw` as expression: only the surrounding statement
9004        // counts (the `??` in this example), not the throw_expression.
9005        check_metrics::<PhpParser>(
9006            "<?php
9007$x = $y ?? throw new \\Exception('nope');",
9008            "foo.php",
9009            |metric| {
9010                assert_eq!(metric.loc.sloc(), 2);
9011                assert_eq!(metric.loc.ploc(), 2);
9012                assert_eq!(metric.loc.lloc(), 1);
9013                assert_eq!(metric.loc.cloc(), 0);
9014                assert_eq!(metric.loc.blank(), 0);
9015                insta::assert_json_snapshot!(metric.loc);
9016            },
9017        );
9018    }
9019
9020    #[cfg(feature = "php")]
9021    #[test]
9022    fn php_no_closure_in_assignment_lloc() {
9023        // Anonymous function as RHS does not add an LLOC; only the
9024        // expression_statement counts. The closure body's statements are
9025        // counted in its own FuncSpace.
9026        check_metrics::<PhpParser>(
9027            "<?php
9028$f = function (): int {
9029    return 42;
9030};",
9031            "foo.php",
9032            |metric| {
9033                assert_eq!(metric.loc.sloc(), 4);
9034                assert_eq!(metric.loc.ploc(), 4);
9035                assert_eq!(metric.loc.lloc(), 2);
9036                assert_eq!(metric.loc.cloc(), 0);
9037                assert_eq!(metric.loc.blank(), 0);
9038                insta::assert_json_snapshot!(metric.loc);
9039            },
9040        );
9041    }
9042
9043    #[cfg(feature = "php")]
9044    #[test]
9045    fn php_for_lloc() {
9046        // The for_statement contributes 1 LLOC; init/cond/update are NOT
9047        // separate statements in PHP's grammar.
9048        check_metrics::<PhpParser>(
9049            "<?php
9050for ($i = 0; $i < 10; $i++) {
9051    echo $i;
9052}",
9053            "foo.php",
9054            |metric| {
9055                assert_eq!(metric.loc.sloc(), 4);
9056                assert_eq!(metric.loc.ploc(), 4);
9057                assert_eq!(metric.loc.lloc(), 2);
9058                assert_eq!(metric.loc.cloc(), 0);
9059                assert_eq!(metric.loc.blank(), 0);
9060                insta::assert_json_snapshot!(metric.loc);
9061            },
9062        );
9063    }
9064
9065    #[cfg(feature = "php")]
9066    #[test]
9067    fn php_foreach_lloc() {
9068        check_metrics::<PhpParser>(
9069            "<?php
9070foreach ($items as $k => $v) {
9071    echo $v;
9072}",
9073            "foo.php",
9074            |metric| {
9075                assert_eq!(metric.loc.sloc(), 4);
9076                assert_eq!(metric.loc.ploc(), 4);
9077                assert_eq!(metric.loc.lloc(), 2);
9078                assert_eq!(metric.loc.cloc(), 0);
9079                assert_eq!(metric.loc.blank(), 0);
9080                insta::assert_json_snapshot!(metric.loc);
9081            },
9082        );
9083    }
9084
9085    #[cfg(feature = "php")]
9086    #[test]
9087    fn php_try_lloc() {
9088        check_metrics::<PhpParser>(
9089            "<?php
9090try {
9091    $a = 1;
9092} catch (\\Exception $e) {
9093    $a = 0;
9094} finally {
9095    $b = 2;
9096}",
9097            "foo.php",
9098            |metric| {
9099                assert_eq!(metric.loc.sloc(), 8);
9100                assert_eq!(metric.loc.ploc(), 8);
9101                assert_eq!(metric.loc.lloc(), 4);
9102                assert_eq!(metric.loc.cloc(), 0);
9103                assert_eq!(metric.loc.blank(), 0);
9104                insta::assert_json_snapshot!(metric.loc);
9105            },
9106        );
9107    }
9108
9109    #[cfg(feature = "php")]
9110    #[test]
9111    fn php_class_loc() {
9112        check_metrics::<PhpParser>(
9113            "<?php
9114class A {
9115    public int $x = 0;
9116    private const Y = 1;
9117    public function f(): int {
9118        return $this->x;
9119    }
9120}",
9121            "foo.php",
9122            |metric| {
9123                assert_eq!(metric.loc.sloc(), 8);
9124                assert_eq!(metric.loc.ploc(), 8);
9125                assert_eq!(metric.loc.lloc(), 3);
9126                assert_eq!(metric.loc.cloc(), 0);
9127                assert_eq!(metric.loc.blank(), 0);
9128                insta::assert_json_snapshot!(metric.loc);
9129            },
9130        );
9131    }
9132
9133    #[cfg(feature = "php")]
9134    #[test]
9135    fn php_namespace_use_lloc() {
9136        check_metrics::<PhpParser>(
9137            "<?php
9138namespace App;
9139use App\\Foo;
9140use App\\Bar;
9141$a = 1;",
9142            "foo.php",
9143            |metric| {
9144                assert_eq!(metric.loc.sloc(), 5);
9145                assert_eq!(metric.loc.ploc(), 5);
9146                assert_eq!(metric.loc.lloc(), 3);
9147                assert_eq!(metric.loc.cloc(), 0);
9148                assert_eq!(metric.loc.blank(), 0);
9149                insta::assert_json_snapshot!(metric.loc);
9150            },
9151        );
9152    }
9153
9154    #[cfg(feature = "php")]
9155    #[test]
9156    fn php_general_loc() {
9157        check_metrics::<PhpParser>(
9158            "<?php
9159// header
9160namespace App;
9161use App\\Foo;
9162
9163class Bar {
9164    public int $n = 0;
9165
9166    public function add(int $x): int {
9167        if ($x > 0) {
9168            return $this->n + $x;
9169        }
9170        return $this->n;
9171    }
9172}",
9173            "foo.php",
9174            |metric| {
9175                assert_eq!(metric.loc.sloc(), 15);
9176                assert_eq!(metric.loc.ploc(), 12);
9177                assert_eq!(metric.loc.lloc(), 5);
9178                assert_eq!(metric.loc.cloc(), 1);
9179                assert_eq!(metric.loc.blank(), 2);
9180                insta::assert_json_snapshot!(metric.loc);
9181            },
9182        );
9183    }
9184
9185    #[cfg(feature = "php")]
9186    #[test]
9187    fn php_match_in_expression_lloc() {
9188        // Match inside another expression (e.g. assignment RHS) — the
9189        // outer expression_statement counts, the inner match arms do not.
9190        check_metrics::<PhpParser>(
9191            "<?php
9192$y = 10 + match ($x) { 1 => 2, default => 0 };",
9193            "foo.php",
9194            |metric| {
9195                assert_eq!(metric.loc.sloc(), 2);
9196                assert_eq!(metric.loc.ploc(), 2);
9197                assert_eq!(metric.loc.lloc(), 1);
9198                assert_eq!(metric.loc.cloc(), 0);
9199                assert_eq!(metric.loc.blank(), 0);
9200                insta::assert_json_snapshot!(metric.loc);
9201            },
9202        );
9203    }
9204
9205    #[cfg(feature = "php")]
9206    #[test]
9207    fn php_html_island_ploc() {
9208        // Embedded HTML between PHP tags ("text interpolation"). HTML
9209        // rows must contribute to PLOC (they are not blank and not a
9210        // PHP comment); this test locks that behavior so a future
9211        // grammar bump or impl tweak that excludes `text` nodes from
9212        // the default PLOC branch is caught.
9213        check_metrics::<PhpParser>(
9214            "<?php if ($cond): ?>
9215<div>hello</div>
9216<p>world</p>
9217<?php endif; ?>",
9218            "foo.php",
9219            |metric| {
9220                assert_eq!(metric.loc.sloc(), 4);
9221                assert_eq!(metric.loc.ploc(), 3);
9222                assert_eq!(metric.loc.lloc(), 1);
9223                assert_eq!(metric.loc.cloc(), 0);
9224                assert_eq!(metric.loc.blank(), 1);
9225                insta::assert_json_snapshot!(metric.loc);
9226            },
9227        );
9228    }
9229
9230    #[cfg(feature = "php")]
9231    #[test]
9232    fn php_short_echo_tag_ploc() {
9233        // `<?=` is the same `php_tag` kind as `<?php` per
9234        // tree-sitter-php 0.24.2. A regression that re-classified `<?=`
9235        // would shift PLOC; this test pins the current behavior.
9236        check_metrics::<PhpParser>("<p><?= $name ?></p>", "foo.php", |metric| {
9237            assert_eq!(metric.loc.sloc(), 1);
9238            assert_eq!(metric.loc.ploc(), 1);
9239            assert_eq!(metric.loc.lloc(), 1);
9240            assert_eq!(metric.loc.cloc(), 0);
9241            assert_eq!(metric.loc.blank(), 0);
9242            insta::assert_json_snapshot!(metric.loc);
9243        });
9244    }
9245
9246    /// Analyses `source` byte-for-byte as PHP, for the #1396 tests
9247    /// `check_metrics` cannot carry — one ends at EOF, which that shim
9248    /// normalises away, and one loops over labelled cases, which its bare
9249    /// `fn` callback cannot close over — and for the bounds test beside
9250    /// them.
9251    #[cfg(feature = "php")]
9252    fn php_loc(source: &[u8]) -> Stats {
9253        metrics_verbatim(
9254            crate::LANG::Php,
9255            source,
9256            crate::MetricsOptions::default().with_only(&[crate::Metric::Loc]),
9257        )
9258        .loc
9259    }
9260
9261    /// #1396: a heredoc row that is empty *inside* the literal read as
9262    /// blank. tree-sitter-php emits one `string_content` child per body
9263    /// row that **has text**, so a row empty inside the literal held no
9264    /// node at all and `blank = sloc - ploc - cloc` claimed it. #778
9265    /// routed PHP's quoted literals through `add_multiline_string_ploc`
9266    /// but excluded the heredoc, on the premise that its inner nodes
9267    /// already covered every row.
9268    ///
9269    /// `sloc` is the fixture anchor (`.claude/rules/testing.md`):
9270    /// deleting the empty row drops it to 5 and fails this test, rather
9271    /// than leaving it quietly asserting `blank 0` about a literal with
9272    /// no interior gap left to misclassify.
9273    #[cfg(feature = "php")]
9274    #[test]
9275    fn php_heredoc_empty_interior_row_is_code_not_blank() {
9276        // expected: six rows, every one code — `<?php`, the opening
9277        // `<<<EOT`, `a`, the empty body row, `b`, and the closing marker.
9278        check_metrics::<PhpParser>("<?php\n$s = <<<EOT\na\n\nb\nEOT;\n", "foo.php", |metric| {
9279            assert_eq!(metric.loc.sloc(), 6);
9280            assert_eq!(metric.loc.ploc(), 6);
9281            assert_eq!(metric.loc.cloc(), 0);
9282            assert_eq!(metric.loc.lloc(), 1);
9283            assert_eq!(metric.loc.blank(), 0);
9284        });
9285    }
9286
9287    /// The nowdoc half of #1396, which the issue did not measure and
9288    /// which was worse: it reported `ploc 4, blank 2` on this fixture
9289    /// against the heredoc's `ploc 5, blank 1`. The grammar shape
9290    /// differs: tree-sitter-php 0.24.2 emits one `nowdoc_string` per body
9291    /// line, but each after the first starts at the *end of the row
9292    /// before it*, so the catch-all's start-row insertion credited every
9293    /// body row to its predecessor and the last one (`b`) to nothing, on
9294    /// top of the empty row.
9295    ///
9296    /// Same `sloc` fixture anchor as the heredoc test above.
9297    #[cfg(feature = "php")]
9298    #[test]
9299    fn php_nowdoc_empty_interior_row_is_code_not_blank() {
9300        // expected: the heredoc fixture's six rows, all code.
9301        check_metrics::<PhpParser>(
9302            "<?php\n$s = <<<'EOT'\na\n\nb\nEOT;\n",
9303            "foo.php",
9304            |metric| {
9305                assert_eq!(metric.loc.sloc(), 6);
9306                assert_eq!(metric.loc.ploc(), 6);
9307                assert_eq!(metric.loc.cloc(), 0);
9308                assert_eq!(metric.loc.lloc(), 1);
9309                assert_eq!(metric.loc.blank(), 0);
9310            },
9311        );
9312    }
9313
9314    /// The four #1396 spellings the two tests above do not cover, each
9315    /// exercising a different part of the new arm.
9316    ///
9317    /// The all-empty body is why the arm routes the `heredoc` / `nowdoc`
9318    /// wrapper rather than `heredoc_body` / `nowdoc_body`: for a body of
9319    /// one empty row the grammar emits **no body node at all**, so an
9320    /// arm keyed on the body would still report that row blank
9321    /// (`.claude/rules/grammar-dispatch.md` section 6). The interpolated
9322    /// form checks that an interior `{$v}` — whose own nodes cover only
9323    /// their row — does not stop the empty row above it being credited.
9324    /// The PHP 7.3 indented closing marker is syntax coverage rather
9325    /// than a distinct code path: `heredoc_end` is credited by the
9326    /// catch-all whatever its indentation, so nothing here can observe
9327    /// an end-row error. It earns its place by failing pre-fix on its
9328    /// interior empty row, like the rest.
9329    ///
9330    /// The backtick `shell_command_expression` is the fifth PHP literal
9331    /// that can span rows and had the nowdoc shape — one multi-row
9332    /// `string_content` — so it lost interior rows too.
9333    ///
9334    /// Every row of every fixture is code, so each case's `sloc` is the
9335    /// fixture anchor: trimming a row from any of them fails that row's
9336    /// case by name. `php_heredoc_credits_only_the_literals_rows` below
9337    /// carries the other half — that the arm credits no row the literal
9338    /// does not span.
9339    #[cfg(feature = "php")]
9340    #[test]
9341    fn php_heredoc_spellings_credit_every_row_to_ploc() {
9342        // (label, source, expected sloc) — expected ploc is that same
9343        // sloc and expected blank is 0 for all five, which is the
9344        // property under test.
9345        let cases = [
9346            ("all-empty heredoc body", "<?php\n$s = <<<EOT\n\nEOT;\n", 4),
9347            ("all-empty nowdoc body", "<?php\n$s = <<<'EOT'\n\nEOT;\n", 4),
9348            (
9349                "interpolated heredoc",
9350                "<?php\n$v = 1;\n$s = <<<EOT\na\n\n{$v}\nEOT;\n",
9351                7,
9352            ),
9353            (
9354                "indented closing marker",
9355                "<?php\n$s = <<<EOT\n    a\n\n    b\n    EOT;\n",
9356                6,
9357            ),
9358            ("backtick shell command", "<?php\n$s = `ls\n\n-l`;\n", 4),
9359        ];
9360        for (label, source, sloc) in cases {
9361            let loc = php_loc(source.as_bytes());
9362            assert_eq!(loc.sloc(), sloc, "{label} sloc");
9363            assert_eq!(loc.ploc(), sloc, "{label} ploc");
9364            assert_eq!(loc.cloc(), 0, "{label} cloc");
9365            assert_eq!(loc.blank(), 0, "{label} blank");
9366        }
9367    }
9368
9369    /// The upper bound the other #1396 tests cannot state. Every fixture
9370    /// above is 100% code, so `ploc == sloc` and `blank == 0` hold just
9371    /// as well for an arm that credits *more* than the literal — an arm
9372    /// inserting `start - 1`, or the whole file from row 0, passes all
9373    /// of them and the rest of the lib suite besides (measured).
9374    ///
9375    /// This fixture carries a comment row and a genuine blank row
9376    /// *outside* the literal, so `cloc` and `blank` are the axes that
9377    /// only a correctly-bounded arm can hit. Both over-crediting
9378    /// perturbations above drive `blank` to 0 and fail here.
9379    ///
9380    /// It is also a #1396 regression test in its own right: pre-fix the
9381    /// heredoc's empty interior row made this `ploc 5, blank 2`.
9382    #[cfg(feature = "php")]
9383    #[test]
9384    fn php_heredoc_credits_only_the_literals_rows() {
9385        // rows: 1 `<?php`, 2 comment, 3 blank, 4 opening, 5 `a`,
9386        // 6 empty-inside-the-literal, 7 `b`, 8 closing marker.
9387        // expected: ploc 6 (1, 4-8), cloc 1 (row 2), blank 1 (row 3).
9388        let loc = php_loc(b"<?php\n// note\n\n$s = <<<EOT\na\n\nb\nEOT;\n");
9389        assert_eq!(loc.sloc(), 8);
9390        assert_eq!(loc.ploc(), 6);
9391        assert_eq!(loc.cloc(), 1);
9392        assert_eq!(loc.blank(), 1);
9393    }
9394
9395    /// #1396 in the input class both metric harnesses normalise away: a
9396    /// heredoc whose closing marker is the last byte of the file.
9397    /// `check_metrics` trims and re-appends a trailing newline, so that
9398    /// shape cannot reach the parser through it at all
9399    /// (`.claude/rules/testing.md`); `metrics_verbatim` hands the bytes
9400    /// over untouched.
9401    ///
9402    /// Unlike #1051 and #1067, no *known* defect hides in this class for
9403    /// #1396 — the trailing-newline twin above already fails against
9404    /// unfixed code, and no perturbation yet found separates the two.
9405    /// A distinct input class costs one call, so it stays.
9406    #[cfg(feature = "php")]
9407    #[test]
9408    fn php_heredoc_ending_at_eof_credits_every_row() {
9409        // expected: the six rows of the issue's fixture, all code, with
9410        // no newline after `EOT;`.
9411        let loc = php_loc(b"<?php\n$s = <<<EOT\na\n\nb\nEOT;");
9412        assert_eq!(loc.sloc(), 6);
9413        assert_eq!(loc.ploc(), 6);
9414        assert_eq!(loc.cloc(), 0);
9415        assert_eq!(loc.blank(), 0);
9416    }
9417
9418    /// The #1414 fixture for [`add_multiline_string_ploc`]'s parent gate:
9419    /// two multi-row Rust literals, the first opening on row 1 under an
9420    /// `array_expression` that started on row 0, the second opening on
9421    /// row 4 alongside the `const_item` that owns it.
9422    ///
9423    /// Returned as a [`crate::Ast`] rather than as a pair of [`Node`]s
9424    /// because the nodes borrow the held tree.
9425    #[cfg(feature = "rust")]
9426    fn multiline_string_gate_fixture() -> crate::Ast {
9427        crate::test_support::parse_named(
9428            LANG::Rust,
9429            "lib.rs",
9430            "const A: [&str; 1] = [\n    \"a\nb\",\n];\nconst B: &str = \"c\nd\";\n",
9431        )
9432    }
9433
9434    /// Returns the `index`-th `string_literal` of
9435    /// [`multiline_string_gate_fixture`], asserting the fixture still
9436    /// holds both and that this one still spans `rows`.
9437    ///
9438    /// The span check is the fixture's own guard: a grammar bump that
9439    /// re-shaped the literal would otherwise turn the row assertions
9440    /// below into claims about some other node.
9441    #[cfg(feature = "rust")]
9442    fn multiline_string_gate_literal<'a>(
9443        root: &Node<'a>,
9444        index: usize,
9445        rows: (usize, usize),
9446    ) -> Node<'a> {
9447        let literals = root.descendants_by_kind(&["string_literal"]);
9448        assert_eq!(
9449            literals.len(),
9450            2,
9451            "fixture drift: expected exactly two string literals"
9452        );
9453        let literal = literals[index];
9454        assert_eq!(
9455            (literal.start_row(), literal.end_row()),
9456            rows,
9457            "fixture drift: literal {index} no longer spans the expected rows"
9458        );
9459        literal
9460    }
9461
9462    /// [`add_multiline_string_ploc`] credits the opening row when the
9463    /// parent started earlier — the branch #1414 measured as unguarded.
9464    ///
9465    /// End-to-end neither half of that gate is observable, which is why
9466    /// this test is at the helper's level rather than through a
9467    /// fixture. Every language routing a literal through this helper
9468    /// ends its match with an unconditional catch-all that has already
9469    /// inserted the literal's start row, and both `LineSet::insert` and
9470    /// `check_comment_ends_on_code_line` are idempotent — so crediting
9471    /// the row twice reads the same as crediting it once, and skipping
9472    /// it drops nothing the catch-all does not put back. Before these
9473    /// tests, removing the gate and inverting it each failed zero of
9474    /// the ~3,330 lib tests. Only a call that reaches the helper with
9475    /// the row still absent separates them.
9476    #[cfg(feature = "rust")]
9477    #[test]
9478    fn multiline_string_credits_an_opening_row_its_parent_did_not_start() {
9479        let ast = multiline_string_gate_fixture();
9480        let root = ast.root_node();
9481        let literal = multiline_string_gate_literal(&root, 0, (1, 2));
9482
9483        let mut stats = Stats::default();
9484        // Seed the row a real caller's catch-all would already hold, so
9485        // nothing below can pass against a default-empty set.
9486        stats.ploc.lines.insert(0);
9487
9488        add_multiline_string_ploc(
9489            &literal,
9490            Ancestors::unknown(),
9491            &mut stats,
9492            literal.start_row(),
9493        );
9494
9495        assert!(stats.ploc.lines.contains(0), "the seeded row must survive");
9496        assert!(
9497            stats.ploc.lines.contains(1),
9498            "the `array_expression` parent starts on row 0, so nothing but \
9499             this helper covers the literal's opening row"
9500        );
9501        assert!(
9502            stats.ploc.lines.contains(2),
9503            "the literal's closing row is interior to it"
9504        );
9505        assert!(
9506            !stats.ploc.lines.contains(3),
9507            "row 3 is `];`, past the literal"
9508        );
9509    }
9510
9511    /// The gate's other side: [`add_multiline_string_ploc`] leaves the
9512    /// opening row alone when the parent started on it, and still runs
9513    /// the interior insertion. See the sibling above for why this is
9514    /// invisible end-to-end.
9515    #[cfg(feature = "rust")]
9516    #[test]
9517    fn multiline_string_leaves_an_opening_row_its_parent_started() {
9518        let ast = multiline_string_gate_fixture();
9519        let root = ast.root_node();
9520        let literal = multiline_string_gate_literal(&root, 1, (4, 5));
9521
9522        let mut stats = Stats::default();
9523        stats.ploc.lines.insert(0);
9524
9525        add_multiline_string_ploc(
9526            &literal,
9527            Ancestors::unknown(),
9528            &mut stats,
9529            literal.start_row(),
9530        );
9531
9532        assert!(stats.ploc.lines.contains(0), "the seeded row must survive");
9533        assert!(
9534            !stats.ploc.lines.contains(4),
9535            "the `const_item` parent starts on row 4 and already covers it, \
9536             so the gate must skip it"
9537        );
9538        assert!(
9539            stats.ploc.lines.contains(5),
9540            "the interior insertion runs whichever way the gate goes"
9541        );
9542    }
9543
9544    /// [`add_string_interior_ploc`] stops at the last row the literal
9545    /// *occupies*, not at its raw end row — the third #1414 branch, and
9546    /// the one that helper's own doc comment argues for.
9547    ///
9548    /// The two spellings differ only for a node whose end column is 0,
9549    /// and the one *string literal* known to have that shape here is a
9550    /// Bash `heredoc_body`: it absorbs the newline after its last
9551    /// content row and so ends at column 0 of the terminator's row.
9552    /// Everywhere else a literal stops just past its closing delimiter,
9553    /// where `end_line() - 1` and `end_row()` agree. Since #1423 the
9554    /// C-family `preproc_arg` is a second caller of this helper that
9555    /// reaches column 0 — a macro body ending on a dangling backslash —
9556    /// and `a_dangling_macro_continuation_does_not_credit_the_row_below`
9557    /// covers that one end-to-end.
9558    ///
9559    /// The call is direct rather than through `bca metrics`
9560    /// deliberately — which node `bash.rs` routes is a separate
9561    /// question (#1412), and an end-to-end fixture would stop
9562    /// discriminating the moment that answer changed.
9563    #[cfg(feature = "bash")]
9564    #[test]
9565    fn string_interior_stops_at_the_last_row_the_literal_occupies() {
9566        // rows: 0 `cat <<EOF`, 1 `a`, 2 `b`, 3 `EOF`.
9567        let ast = crate::test_support::parse_named(LANG::Bash, "f.sh", "cat <<EOF\na\nb\nEOF\n");
9568        let root = ast.root_node();
9569        let bodies = root.descendants_by_kind(&["heredoc_body"]);
9570        let body = bodies.first().expect("the fixture holds one heredoc body");
9571        assert_eq!(
9572            (body.start_position(), body.end_position()),
9573            ((1, 0), (3, 0)),
9574            "fixture drift: the end column must be 0 for this test to mean anything"
9575        );
9576
9577        let mut stats = Stats::default();
9578        // Row 0 is the `cat <<EOF` line, which Bash's arm credits
9579        // through a different node; seeding it keeps every assertion
9580        // below distinguishable from the default-empty set.
9581        stats.ploc.lines.insert(0);
9582
9583        add_string_interior_ploc(body, &mut stats, body.start_row());
9584
9585        assert!(stats.ploc.lines.contains(0), "the seeded row must survive");
9586        assert!(
9587            !stats.ploc.lines.contains(1),
9588            "this helper leaves its `start` row to the caller"
9589        );
9590        assert!(
9591            stats.ploc.lines.contains(2),
9592            "row 2 is the body's last content row"
9593        );
9594        assert!(
9595            !stats.ploc.lines.contains(3),
9596            "row 3 holds the `EOF` terminator: the body ends at its column 0 \
9597             and so does not occupy it"
9598        );
9599    }
9600
9601    #[cfg(feature = "elixir")]
9602    #[test]
9603    fn elixir_blank() {
9604        // Two blank lines separate three top-level expressions.
9605        check_metrics::<ElixirParser>(
9606            "defmodule Foo do\n\n  def a, do: :a\n\n  def b, do: :b\nend\n",
9607            "foo.ex",
9608            |metric| {
9609                assert_eq!(metric.loc.sloc(), 6);
9610                assert_eq!(metric.loc.ploc(), 4);
9611                assert_eq!(metric.loc.lloc(), 3);
9612                assert_eq!(metric.loc.cloc(), 0);
9613                assert_eq!(metric.loc.blank(), 2);
9614                insta::assert_json_snapshot!(
9615                    metric.loc,
9616                    @r#"
9617                {
9618                  "sloc": 6,
9619                  "ploc": 4,
9620                  "lloc": 3,
9621                  "cloc": 0,
9622                  "blank": 2,
9623                  "sloc_average": 1.5,
9624                  "ploc_average": 1.0,
9625                  "lloc_average": 0.75,
9626                  "cloc_average": 0.0,
9627                  "blank_average": 0.5,
9628                  "sloc_min": 1,
9629                  "sloc_max": 6,
9630                  "cloc_min": 0,
9631                  "cloc_max": 0,
9632                  "ploc_min": 1,
9633                  "ploc_max": 4,
9634                  "lloc_min": 1,
9635                  "lloc_max": 3,
9636                  "blank_min": 0,
9637                  "blank_max": 2
9638                }
9639                "#
9640                );
9641            },
9642        );
9643    }
9644
9645    #[cfg(feature = "elixir")]
9646    #[test]
9647    fn elixir_no_zero_blank() {
9648        // Blank line interleaved with code that carries trailing comments —
9649        // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
9650        check_metrics::<ElixirParser>(
9651            "defmodule Foo do\n  def f, do: :ok\n\n  def g, do: :ok # trailing\n  def h, do: :ok # trailing\nend\n",
9652            "foo.ex",
9653            |metric| {
9654                assert_eq!(metric.loc.sloc(), 6);
9655                assert_eq!(metric.loc.ploc(), 5);
9656                assert_eq!(metric.loc.cloc(), 2);
9657                assert_eq!(metric.loc.blank(), 1);
9658            },
9659        );
9660    }
9661
9662    #[cfg(feature = "elixir")]
9663    #[test]
9664    fn elixir_blank_zero_sanity() {
9665        // Sanity check: blank must report 0, never go negative, when the
9666        // input has no blank lines.
9667        check_metrics::<ElixirParser>(
9668            "defmodule Foo do\n  def f, do: :ok\nend\n",
9669            "foo.ex",
9670            |metric| {
9671                assert_eq!(metric.loc.blank(), 0);
9672            },
9673        );
9674    }
9675
9676    #[cfg(feature = "elixir")]
9677    #[test]
9678    fn elixir_cloc() {
9679        // Mix of standalone comments and a comment on the same line as
9680        // code. Elixir has no block comment syntax — only `#` lines.
9681        check_metrics::<ElixirParser>(
9682            "# top\ndefmodule Foo do\n  # body\n  def f, do: :ok # trailing\nend\n",
9683            "foo.ex",
9684            |metric| {
9685                assert_eq!(metric.loc.cloc(), 3);
9686            },
9687        );
9688    }
9689
9690    #[cfg(feature = "elixir")]
9691    #[test]
9692    fn elixir_lloc() {
9693        // Two statements at the top level of the module body — the
9694        // `defmodule` call itself counts as one statement (since its
9695        // parent is `Source`), and each `def` inside its `do_block`
9696        // counts too: 1 + 2 = 3.
9697        check_metrics::<ElixirParser>(
9698            "defmodule Foo do\n  def a, do: 1\n  def b, do: 2\nend\n",
9699            "foo.ex",
9700            |metric| {
9701                assert_eq!(metric.loc.lloc(), 3);
9702            },
9703        );
9704    }
9705
9706    #[cfg(feature = "elixir")]
9707    #[test]
9708    fn elixir_no_nested_call_lloc() {
9709        // Calls nested inside another call's arguments are NOT direct
9710        // children of a statement container, so they do not bump LLOC.
9711        // Three syntactic calls (`defmodule`, `def`, `IO.puts`) → 3.
9712        check_metrics::<ElixirParser>(
9713            "defmodule Foo do\n  def f do\n    IO.puts(Enum.join([1, 2, 3], \", \"))\n  end\nend\n",
9714            "foo.ex",
9715            |metric| {
9716                assert_eq!(metric.loc.lloc(), 3);
9717            },
9718        );
9719    }
9720
9721    #[cfg(feature = "elixir")]
9722    #[test]
9723    fn elixir_no_binary_operator_inside_call_lloc() {
9724        // Binary operators inside call arguments are sub-expressions,
9725        // not statements. A single `def` body containing `IO.puts(a + b)`
9726        // produces 3 LLOC (defmodule, def, IO.puts) — the `a + b`
9727        // binary_operator is not a direct child of any statement
9728        // container.
9729        check_metrics::<ElixirParser>(
9730            "defmodule Foo do\n  def f(a, b) do\n    IO.puts(a + b)\n  end\nend\n",
9731            "foo.ex",
9732            |metric| {
9733                assert_eq!(metric.loc.lloc(), 3);
9734            },
9735        );
9736    }
9737
9738    #[cfg(feature = "elixir")]
9739    #[test]
9740    fn elixir_stab_clause_counts_lloc() {
9741        // Each `stab_clause` arm in a `case do ... end` is a direct
9742        // child of the inner `do_block`, so each one is its own LLOC.
9743        // defmodule + def + case + 3 arms = 6 logical lines.
9744        check_metrics::<ElixirParser>(
9745            "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",
9746            "foo.ex",
9747            |metric| {
9748                assert_eq!(metric.loc.lloc(), 6);
9749            },
9750        );
9751    }
9752
9753    #[cfg(feature = "elixir")]
9754    #[test]
9755    fn elixir_no_comment_lloc() {
9756        // Comments are direct children of a statement container but
9757        // are routed through the dedicated `Comment` arm in `compute`,
9758        // so they MUST NOT bump LLOC. Only `defmodule` and `def`
9759        // contribute LLOC here.
9760        check_metrics::<ElixirParser>(
9761            "# leading\ndefmodule Foo do\n  # inside\n  def f, do: :ok\n  # trailing\nend\n",
9762            "foo.ex",
9763            |metric| {
9764                assert_eq!(metric.loc.lloc(), 2);
9765            },
9766        );
9767    }
9768
9769    #[cfg(feature = "elixir")]
9770    #[test]
9771    fn elixir_no_do_token_lloc() {
9772        // The `do` and `end` keyword tokens are unnamed leaves inside a
9773        // `do_block`; they must not be counted as statements. A body
9774        // with one expression produces exactly 2 LLOC (defmodule and
9775        // the inner expression).
9776        check_metrics::<ElixirParser>("defmodule Foo do\n  :ok\nend\n", "foo.ex", |metric| {
9777            // `:ok` is an `Atom` whose parent is the module-call's
9778            // `do_block`; that counts. Plus the `defmodule` call.
9779            assert_eq!(metric.loc.lloc(), 2);
9780        });
9781    }
9782
9783    #[cfg(feature = "elixir")]
9784    #[test]
9785    fn elixir_no_keyword_pair_lloc() {
9786        // `key: value` keyword pairs inside an argument list (`def f,
9787        // do: :ok`) are children of an `arguments` / `keywords` node,
9788        // not a statement container, so they don't bump LLOC.
9789        check_metrics::<ElixirParser>(
9790            "defmodule Foo do\n  def add(a, b), do: a + b\nend\n",
9791            "foo.ex",
9792            |metric| {
9793                // defmodule (1) + def (1) = 2
9794                assert_eq!(metric.loc.lloc(), 2);
9795            },
9796        );
9797    }
9798
9799    #[cfg(feature = "elixir")]
9800    #[test]
9801    fn elixir_no_string_content_lloc() {
9802        // `quoted_content` chunks inside a heredoc / regular string are
9803        // structural and don't represent statements. A `@moduledoc`
9804        // attribute call with a multi-line string contributes exactly
9805        // one LLOC (the `@moduledoc` call), not one per content line.
9806        check_metrics::<ElixirParser>(
9807            "defmodule Foo do\n  @moduledoc \"\"\"\n  line one\n  line two\n  \"\"\"\n  def f, do: :ok\nend\n",
9808            "foo.ex",
9809            |metric| {
9810                // defmodule + @moduledoc + def = 3
9811                assert_eq!(metric.loc.lloc(), 3);
9812            },
9813        );
9814    }
9815
9816    #[cfg(feature = "elixir")]
9817    #[test]
9818    fn elixir_multiline_string_forms_credit_every_row_to_ploc() {
9819        // Regression test for #1260. `quoted_content` — the literal text of
9820        // every Elixir string form — is childless, so the leaf branch of
9821        // the catch-all credited only its opening row and the interior rows
9822        // reached neither PLOC nor CLOC.
9823
9824        // expected: 3 rows of a plain `"…"` literal — `x = "line one`,
9825        // `line two`, `line three"` — all code, none blank.
9826        check_metrics::<ElixirParser>(
9827            "x = \"line one\nline two\nline three\"",
9828            "foo.ex",
9829            |metric| {
9830                assert_eq!(metric.loc.sloc(), 3);
9831                assert_eq!(metric.loc.ploc(), 3);
9832                assert_eq!(metric.loc.cloc(), 0);
9833                assert_eq!(metric.loc.blank(), 0);
9834            },
9835        );
9836
9837        // expected: 4 rows of a `'''` charlist whose third row is empty
9838        // inside the literal — `c = '''`, `c1`, ``, `'''`. Code, not blank.
9839        check_metrics::<ElixirParser>("c = '''\nc1\n\n'''", "foo.ex", |metric| {
9840            assert_eq!(metric.loc.sloc(), 4);
9841            assert_eq!(metric.loc.ploc(), 4);
9842            assert_eq!(metric.loc.cloc(), 0);
9843            assert_eq!(metric.loc.blank(), 0);
9844        });
9845
9846        // expected: 4 rows of a `~s"""` sigil heredoc — `s = ~s"""`, `s1`,
9847        // `s2`, `"""`. A sigil is a distinct grammar node from `string` and
9848        // `charlist`; all three wrap the same `quoted_content`, which is
9849        // why one arm covers them.
9850        check_metrics::<ElixirParser>("s = ~s\"\"\"\ns1\ns2\n\"\"\"", "foo.ex", |metric| {
9851            assert_eq!(metric.loc.sloc(), 4);
9852            assert_eq!(metric.loc.ploc(), 4);
9853            assert_eq!(metric.loc.cloc(), 0);
9854            assert_eq!(metric.loc.blank(), 0);
9855        });
9856    }
9857
9858    #[cfg(feature = "elixir")]
9859    #[test]
9860    fn elixir_module_attribute_docstring_rows_are_ploc_not_cloc() {
9861        // #1260 had to choose a bucket for `@doc` / `@moduledoc` heredoc
9862        // rows, and this pins it: PLOC.
9863        //
9864        // Python's one carve-out to CLOC is a *bare* string expression
9865        // statement whose value is discarded — a docstring by position. An
9866        // Elixir module attribute is not that shape: `@doc "…"` is an
9867        // assignment whose value the compiler stores and
9868        // `Code.fetch_docs/1` reads back, so its Python analogue is
9869        // `x = """…"""`, which Python counts as PLOC (#415).
9870        //
9871        // expected: 7 rows — `defmodule M do`, `  @doc """`, `  docs one`,
9872        // `  docs two`, `  """`, `  def f, do: :ok`, `end` — all code, none
9873        // blank and none comment. lloc 3 = defmodule + @doc + def.
9874        check_metrics::<ElixirParser>(
9875            "defmodule M do\n  @doc \"\"\"\n  docs one\n  docs two\n  \"\"\"\n  def f, do: :ok\nend",
9876            "foo.ex",
9877            |metric| {
9878                assert_eq!(metric.loc.sloc(), 7);
9879                assert_eq!(metric.loc.ploc(), 7);
9880                assert_eq!(metric.loc.lloc(), 3);
9881                assert_eq!(metric.loc.cloc(), 0);
9882                assert_eq!(metric.loc.blank(), 0);
9883            },
9884        );
9885    }
9886
9887    #[cfg(feature = "elixir")]
9888    #[test]
9889    fn elixir_rescue_arm_counts_lloc() {
9890        // Each rescue arm's body has a single expression (e.g. `:bad`)
9891        // that counts as one LLOC; the `stab_clause` header itself is
9892        // skipped. The rescue_block named node is also a direct child
9893        // of try's do_block, so it contributes one LLOC too.
9894        // Total: defmodule + def + try + do_it() + rescue_block
9895        //        + 2 arm bodies = 7.
9896        check_metrics::<ElixirParser>(
9897            "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",
9898            "foo.ex",
9899            |metric| {
9900                assert_eq!(metric.loc.lloc(), 7);
9901            },
9902        );
9903    }
9904
9905    #[cfg(feature = "elixir")]
9906    #[test]
9907    fn elixir_no_arg_punctuation_lloc() {
9908        // Function-call arguments (`a, b` inside `def add(a, b)`) are
9909        // children of an `arguments` node, not of a statement container.
9910        // They MUST NOT inflate LLOC.
9911        check_metrics::<ElixirParser>(
9912            "defmodule Foo do\n  def add(a, b, c, d) do\n    a + b + c + d\n  end\nend\n",
9913            "foo.ex",
9914            |metric| {
9915                // defmodule + def + (a+b+c+d) = 3
9916                assert_eq!(metric.loc.lloc(), 3);
9917            },
9918        );
9919    }
9920
9921    #[cfg(feature = "elixir")]
9922    #[test]
9923    fn elixir_no_list_element_lloc() {
9924        // List literal elements live under a `list` node, not a
9925        // statement container — they must not bump LLOC.
9926        check_metrics::<ElixirParser>(
9927            "defmodule Foo do\n  def f do\n    [:a, :b, :c, :d]\n  end\nend\n",
9928            "foo.ex",
9929            |metric| {
9930                // defmodule + def + the list expression = 3
9931                assert_eq!(metric.loc.lloc(), 3);
9932            },
9933        );
9934    }
9935
9936    #[cfg(feature = "elixir")]
9937    #[test]
9938    fn elixir_no_map_field_lloc() {
9939        // Map `pair`s live under `map`, not a statement container.
9940        check_metrics::<ElixirParser>(
9941            "defmodule Foo do\n  def f do\n    %{a: 1, b: 2, c: 3}\n  end\nend\n",
9942            "foo.ex",
9943            |metric| {
9944                assert_eq!(metric.loc.lloc(), 3);
9945            },
9946        );
9947    }
9948
9949    #[cfg(feature = "elixir")]
9950    #[test]
9951    fn elixir_anonymous_fn_body_lloc() {
9952        // `lloc()` on the Unit space returns the aggregate (own +
9953        // nested-space) count. Even though the anonymous_function is
9954        // its own function space, the merge step pulls its `lloc` back
9955        // into the parent. Counts:
9956        //   Unit own: defmodule, def, `add = fn ...`, final `add` = 4
9957        //   anon-fn:  `x + 1` body expression                       = 1
9958        //   aggregated total                                        = 5
9959        check_metrics::<ElixirParser>(
9960            "defmodule Foo do\n  def f do\n    add = fn x -> x + 1 end\n    add\n  end\nend\n",
9961            "foo.ex",
9962            |metric| {
9963                assert_eq!(metric.loc.lloc(), 5);
9964            },
9965        );
9966    }
9967
9968    #[cfg(feature = "ruby")]
9969    #[test]
9970    fn ruby_blank() {
9971        // The parser's root span starts at the first non-blank line, so
9972        // a blank line must sit BETWEEN code lines to be counted.
9973        // expected: line 3 is blank → blank = 1.
9974        check_metrics::<RubyParser>("def foo\n  a = 1\n\n  a + 1\nend\n", "foo.rb", |metric| {
9975            assert_eq!(metric.loc.blank(), 1);
9976        });
9977    }
9978
9979    #[cfg(feature = "ruby")]
9980    #[test]
9981    fn ruby_no_zero_blank() {
9982        // Mirrors `rust_no_zero_blank`: the blank counter must stay
9983        // non-zero when blank lines sit between code lines that carry
9984        // trailing comments. Catches regressions in the SLOC −
9985        // (PLOC ∪ CLOC) union math when PLOC and CLOC line-sets
9986        // overlap.
9987        check_metrics::<RubyParser>(
9988            "def foo  # entry\n  pool = 0\n\n  server = -42  # negative\n\n  ok = false\nend\n",
9989            "foo.rb",
9990            |metric| {
9991                assert_eq!(metric.loc.blank(), 2);
9992            },
9993        );
9994    }
9995
9996    #[cfg(feature = "ruby")]
9997    #[test]
9998    fn ruby_cloc() {
9999        // 3 comment lines.
10000        check_metrics::<RubyParser>(
10001            "# one\n# two\n# three\ndef foo\nend\n",
10002            "foo.rb",
10003            |metric| {
10004                assert_eq!(metric.loc.cloc(), 3);
10005            },
10006        );
10007    }
10008
10009    #[cfg(feature = "ruby")]
10010    #[test]
10011    fn ruby_lloc() {
10012        // expected: 3 logical lines = `def` (Method) + `if` (If) +
10013        // `while` (While). Bare expression-statements (assignments,
10014        // calls) are intentionally NOT counted.
10015        check_metrics::<RubyParser>(
10016            "def foo(a)\n  if a\n    a += 1\n  end\n  while a > 0\n    a -= 1\n  end\nend\n",
10017            "foo.rb",
10018            |metric| {
10019                assert_eq!(metric.loc.lloc(), 3);
10020            },
10021        );
10022    }
10023
10024    #[cfg(feature = "ruby")]
10025    #[test]
10026    fn ruby_no_call_lloc() {
10027        // expected: 1 logical line (the surrounding `def`). The bare
10028        // method calls `puts 'hello'` and `puts 'world'` are
10029        // intentionally NOT counted — there is no expression_statement
10030        // wrapper to disambiguate them from sub-expressions.
10031        check_metrics::<RubyParser>(
10032            "def foo\n  puts 'hello'\n  puts 'world'\nend\n",
10033            "foo.rb",
10034            |metric| {
10035                assert_eq!(metric.loc.lloc(), 1);
10036            },
10037        );
10038    }
10039
10040    #[cfg(feature = "ruby")]
10041    #[test]
10042    fn ruby_no_assignment_lloc() {
10043        // Same rationale as `ruby_no_call_lloc`. expected: 1 lloc
10044        // (the `def`); raw assignments aren't counted.
10045        check_metrics::<RubyParser>(
10046            "def foo\n  a = 1\n  b = 2\n  c = a + b\nend\n",
10047            "foo.rb",
10048            |metric| {
10049                assert_eq!(metric.loc.lloc(), 1);
10050            },
10051        );
10052    }
10053
10054    #[cfg(feature = "ruby")]
10055    #[test]
10056    fn ruby_modifier_lloc() {
10057        // Postfix modifier forms each count as one logical line. A
10058        // `return … if …` parses as an `IfModifier` wrapping a `Return`;
10059        // both fire the LLOC arm so the modifier line contributes +2.
10060        // expected: def(1) + if_modifier(1) + inner return(1)
10061        // + while_modifier(1) + rescue_modifier(1) = 5.
10062        check_metrics::<RubyParser>(
10063            "def foo(a)\n  return a if a.nil?\n  a -= 1 while a > 0\n  parse(a) rescue nil\nend\n",
10064            "foo.rb",
10065            |metric| {
10066                assert_eq!(metric.loc.lloc(), 5);
10067            },
10068        );
10069    }
10070
10071    #[cfg(feature = "ruby")]
10072    #[test]
10073    fn ruby_class_lloc() {
10074        // expected: 1 class + 1 module + 2 methods = 4.
10075        check_metrics::<RubyParser>(
10076            "module M\n  class C\n    def foo\n    end\n    def bar\n    end\n  end\nend\n",
10077            "foo.rb",
10078            |metric| {
10079                assert_eq!(metric.loc.lloc(), 4);
10080            },
10081        );
10082    }
10083
10084    #[cfg(feature = "ruby")]
10085    #[test]
10086    fn ruby_begin_rescue_lloc() {
10087        // expected: 1 def + 1 begin = 2. Rescue clauses are part of
10088        // the begin construct and not separately counted; the bare
10089        // expression body lines are not statements.
10090        check_metrics::<RubyParser>(
10091            "def foo\n  begin\n    risky\n  rescue StandardError\n    nil\n  end\nend\n",
10092            "foo.rb",
10093            |metric| {
10094                assert_eq!(metric.loc.lloc(), 2);
10095            },
10096        );
10097    }
10098
10099    #[cfg(feature = "ruby")]
10100    #[test]
10101    fn ruby_nested_defs_lloc() {
10102        // Each `Method` declaration contributes one logical line.
10103        // expected: outer `def` + inner `def` = 2.
10104        check_metrics::<RubyParser>(
10105            "def outer\n  def inner\n    1\n  end\nend\n",
10106            "foo.rb",
10107            |metric| {
10108                assert_eq!(metric.loc.lloc(), 2);
10109            },
10110        );
10111    }
10112
10113    #[cfg(feature = "ruby")]
10114    #[test]
10115    fn ruby_no_block_body_lloc() {
10116        // A top-level `[1,2,3].each do |x| puts x end` produces zero
10117        // logical lines: the surrounding `.each` is a `Call` (not in
10118        // the LLOC arm), the `DoBlock` is a closure (also not a
10119        // statement), and the `puts x` inside is another call. This
10120        // pins the documented expression-statement exclusion.
10121        check_metrics::<RubyParser>(
10122            "[1, 2, 3].each do |x|\n  puts x\nend\n",
10123            "foo.rb",
10124            |metric| {
10125                assert_eq!(metric.loc.lloc(), 0);
10126            },
10127        );
10128    }
10129
10130    #[cfg(feature = "ruby")]
10131    #[test]
10132    fn ruby_no_lambda_body_lloc() {
10133        // `add = ->(a, b) { a + b }` produces zero logical lines for
10134        // the same reason as `ruby_no_block_body_lloc`: assignments,
10135        // calls, and lambda bodies are intentionally not statements
10136        // in this impl.
10137        check_metrics::<RubyParser>("add = ->(a, b) {\n  a + b\n}\n", "foo.rb", |metric| {
10138            assert_eq!(metric.loc.lloc(), 0);
10139        });
10140    }
10141
10142    #[cfg(feature = "ruby")]
10143    #[test]
10144    fn ruby_heredoc_lloc_and_blank() {
10145        // A `<<~TXT` heredoc contributes: SLOC = every line in the file
10146        // (including the heredoc body); LLOC = just the surrounding `def`.
10147        // #778: the heredoc-body rows (`one`, `two`) hold real string text,
10148        // so they are credited to PLOC like Python's multi-line strings
10149        // (#415) rather than mislabelled as blank. Every row is now code.
10150        // expected: sloc = 7, ploc = 7, lloc = 1, blank = 0.
10151        check_metrics::<RubyParser>(
10152            "def foo\n  msg = <<~TXT\n    one\n    two\n  TXT\n  msg\nend\n",
10153            "foo.rb",
10154            |metric| {
10155                assert_eq!(metric.loc.sloc(), 7);
10156                assert_eq!(metric.loc.ploc(), 7);
10157                assert_eq!(metric.loc.lloc(), 1);
10158                assert_eq!(metric.loc.blank(), 0);
10159            },
10160        );
10161    }
10162
10163    #[cfg(feature = "ruby")]
10164    #[test]
10165    fn ruby_semicolon_multistatement_lloc_undercount() {
10166        // Documented limitation: Ruby has no `expression_statement`
10167        // wrapper, so `;`-separated multi-statement lines collapse to
10168        // a single LLOC bump (the surrounding `def`). A future
10169        // statement-counter that walks BlockBody children would
10170        // change this — pin the current behaviour so the regression
10171        // is visible.
10172        check_metrics::<RubyParser>(
10173            "def foo\n  a = 1; b = 2; a + b\nend\n",
10174            "foo.rb",
10175            |metric| {
10176                assert_eq!(metric.loc.lloc(), 1);
10177            },
10178        );
10179    }
10180
10181    #[cfg(feature = "ruby")]
10182    #[test]
10183    fn ruby_ploc_skips_comments_and_blanks() {
10184        // PLOC counts physical instruction lines: code-bearing lines
10185        // only. Comments and blanks are excluded.
10186        check_metrics::<RubyParser>("# header\n\ndef foo\n  a = 1\nend\n", "foo.rb", |metric| {
10187            assert_eq!(metric.loc.ploc(), 3);
10188            assert_eq!(metric.loc.cloc(), 1);
10189            assert_eq!(metric.loc.blank(), 1);
10190        });
10191    }
10192
10193    // -----------------------------------------------------------------
10194    // Issue #195: nested-function/closure LLOC tests across languages.
10195    // Mirrors the prior art for Rust (`rust_function_in_loop_lloc`,
10196    // `rust_closure_expression_lloc`), Mozjs (`mozjs_nested_function_loc`),
10197    // Bash (`bash_nested_function_loc`), and TypeScript
10198    // (`typescript_nested_functions_loc`, `tsx_nested_functions_loc`).
10199    // -----------------------------------------------------------------
10200
10201    #[cfg(feature = "python")]
10202    #[test]
10203    fn python_nested_def_lloc() {
10204        // Nested `def`: the inner function declaration plus the outer
10205        // body's `return inner()` are both LLOC; the outer `def` header
10206        // and the inner `return 1` belong to their own function spaces.
10207        check_metrics::<PythonParser>(
10208            "def outer():\n    def inner():\n        return 1\n    return inner()\n",
10209            "foo.py",
10210            |metric| {
10211                assert_eq!(metric.loc.sloc(), 4);
10212                assert_eq!(metric.loc.ploc(), 4);
10213                assert_eq!(metric.loc.lloc(), 2);
10214                assert_eq!(metric.loc.cloc(), 0);
10215                assert_eq!(metric.loc.blank(), 0);
10216                insta::assert_json_snapshot!(metric.loc);
10217            },
10218        );
10219    }
10220
10221    #[cfg(feature = "python")]
10222    #[test]
10223    fn python_lambda_in_def_lloc() {
10224        // `lambda x: x + 1` is an expression, not a Python `function_definition`,
10225        // so it does not start a new function space. The two LLOC come from
10226        // the assignment `f = lambda ...` and the `return f(2)` statement.
10227        check_metrics::<PythonParser>(
10228            "def outer():\n    f = lambda x: x + 1\n    return f(2)\n",
10229            "foo.py",
10230            |metric| {
10231                assert_eq!(metric.loc.sloc(), 3);
10232                assert_eq!(metric.loc.ploc(), 3);
10233                assert_eq!(metric.loc.lloc(), 2);
10234                assert_eq!(metric.loc.cloc(), 0);
10235                assert_eq!(metric.loc.blank(), 0);
10236                insta::assert_json_snapshot!(metric.loc);
10237            },
10238        );
10239    }
10240
10241    #[cfg(feature = "python")]
10242    #[test]
10243    fn python_match_statement_lloc() {
10244        // `match` (PEP 634) is a control-flow statement that must add one
10245        // LLOC like `if`/`try`, plus each `return` in a case body (#462).
10246        // Its `case_clause` children add nothing, mirroring how
10247        // `elif_clause`/`else_clause` are absent from the LLOC arm: the
10248        // construct counts once and the statements inside count via their
10249        // own arms. Here: match(1) + two `return`s = 3.
10250        check_metrics::<PythonParser>(
10251            "def f(x):\n    match x:\n        case 1: return 1\n        case _: return 0\n",
10252            "foo.py",
10253            |metric| {
10254                assert_eq!(metric.loc.lloc(), 3);
10255                insta::assert_json_snapshot!(metric.loc);
10256            },
10257        );
10258    }
10259
10260    #[cfg(feature = "python")]
10261    #[test]
10262    fn python_match_lloc_matches_if_else() {
10263        // Parity with the equivalent two-branch `if`/`else`: both have the
10264        // construct keyword (1) plus two `return` bodies (2), so each must
10265        // report an identical LLOC of 3. The match form previously
10266        // undercounted (2) because `match_statement` was absent from the
10267        // LLOC arm (#462). `check_metrics` takes a non-capturing `fn`, so
10268        // the shared expectation is pinned to the same literal in both
10269        // closures rather than threaded through a captured variable.
10270        check_metrics::<PythonParser>(
10271            "def f(x):\n    if x == 1: return 1\n    else: return 0\n",
10272            "foo.py",
10273            |metric| assert_eq!(metric.loc.lloc(), 3),
10274        );
10275        check_metrics::<PythonParser>(
10276            "def f(x):\n    match x:\n        case 1: return 1\n        case _: return 0\n",
10277            "foo.py",
10278            |metric| assert_eq!(metric.loc.lloc(), 3),
10279        );
10280    }
10281
10282    #[cfg(feature = "python")]
10283    #[test]
10284    fn python_type_alias_lloc() {
10285        // A `type` alias (PEP 695) is a leaf statement, counted like an
10286        // assignment. `type Alias = int` followed by `x = 1` is two LLOC;
10287        // before #462 the alias fell through to the `_` arm and the file
10288        // reported only 1.
10289        check_metrics::<PythonParser>("type Alias = int\nx = 1\n", "foo.py", |metric| {
10290            assert_eq!(metric.loc.lloc(), 2);
10291            insta::assert_json_snapshot!(metric.loc);
10292        });
10293    }
10294
10295    #[cfg(feature = "java")]
10296    #[test]
10297    fn java_local_class_in_method_lloc() {
10298        // A `class` declared inside a method body produces its own function
10299        // space, so the outer method's LLOC only sees `return new Local().v();`
10300        // and the body of `v()` contributes the second LLOC.
10301        check_metrics::<JavaParser>(
10302            "class Foo {\n    int bar() {\n        class Local {\n            int v() { return 1; }\n        }\n        return new Local().v();\n    }\n}\n",
10303            "foo.java",
10304            |metric| {
10305                assert_eq!(metric.loc.sloc(), 8);
10306                assert_eq!(metric.loc.ploc(), 8);
10307                assert_eq!(metric.loc.lloc(), 2);
10308                assert_eq!(metric.loc.cloc(), 0);
10309                assert_eq!(metric.loc.blank(), 0);
10310                insta::assert_json_snapshot!(metric.loc);
10311            },
10312        );
10313    }
10314
10315    #[cfg(feature = "java")]
10316    #[test]
10317    fn java_lambda_in_method_lloc() {
10318        // Java lambdas are expressions; the two LLOC come from the
10319        // `IntUnaryOperator f = x -> x + 1;` declaration and the
10320        // `f.applyAsInt(3);` expression statement.
10321        check_metrics::<JavaParser>(
10322            "class Foo {\n    void bar() {\n        java.util.function.IntUnaryOperator f = x -> x + 1;\n        f.applyAsInt(3);\n    }\n}\n",
10323            "foo.java",
10324            |metric| {
10325                assert_eq!(metric.loc.sloc(), 6);
10326                assert_eq!(metric.loc.ploc(), 6);
10327                assert_eq!(metric.loc.lloc(), 2);
10328                assert_eq!(metric.loc.cloc(), 0);
10329                assert_eq!(metric.loc.blank(), 0);
10330                insta::assert_json_snapshot!(metric.loc);
10331            },
10332        );
10333    }
10334
10335    #[cfg(feature = "groovy")]
10336    #[test]
10337    fn groovy_blank() {
10338        // Blank lines + simple statements. Newlines act as the
10339        // statement terminator; PLOC counts the two declaration lines.
10340        check_metrics::<GroovyParser>("int x = 1\n\n\nint y = 2", "foo.groovy", |metric| {
10341            assert_eq!(metric.loc.sloc(), 4);
10342            assert_eq!(metric.loc.ploc(), 2);
10343            assert_eq!(metric.loc.lloc(), 2);
10344            assert_eq!(metric.loc.blank(), 2);
10345        });
10346    }
10347
10348    #[cfg(feature = "groovy")]
10349    #[test]
10350    fn groovy_no_zero_blank() {
10351        // A single line with no blanks: blank() == 0.
10352        check_metrics::<GroovyParser>("int x = 1", "foo.groovy", |metric| {
10353            assert_eq!(metric.loc.sloc(), 1);
10354            assert_eq!(metric.loc.blank(), 0);
10355        });
10356    }
10357
10358    #[cfg(feature = "groovy")]
10359    #[test]
10360    fn groovy_cloc_line_comments() {
10361        check_metrics::<GroovyParser>(
10362            "// first comment
10363            int x = 1
10364            // second comment
10365            int y = 2",
10366            "foo.groovy",
10367            |metric| {
10368                assert_eq!(metric.loc.cloc(), 2);
10369                assert_eq!(metric.loc.ploc(), 2);
10370            },
10371        );
10372    }
10373
10374    #[cfg(feature = "groovy")]
10375    #[test]
10376    fn groovy_cloc_block_comment() {
10377        check_metrics::<GroovyParser>(
10378            "/* multi
10379               line
10380               comment */
10381            int x = 1",
10382            "foo.groovy",
10383            |metric| {
10384                // Block comment spans 3 lines → cloc == 3.
10385                assert_eq!(metric.loc.cloc(), 3);
10386            },
10387        );
10388    }
10389
10390    #[cfg(feature = "groovy")]
10391    #[test]
10392    fn groovy_cloc_groovydoc_comment() {
10393        // Groovy `/** … */` `groovydoc_comment` counts as CLOC. The
10394        // `Loc` arm already handled it; this pins it alongside the
10395        // restored `is_comment` parity (#697).
10396        check_metrics::<GroovyParser>(
10397            "/** groovydoc */
10398class A {
10399  int x = 1
10400}",
10401            "foo.groovy",
10402            |metric| {
10403                assert_eq!(metric.loc.cloc(), 1);
10404                assert_eq!(metric.loc.ploc(), 3);
10405            },
10406        );
10407    }
10408
10409    #[cfg(feature = "groovy")]
10410    #[test]
10411    fn groovy_simple_lloc() {
10412        // One LLOC per simple expression statement.
10413        check_metrics::<GroovyParser>(
10414            "int a = 1
10415            int b = 2
10416            int c = 3",
10417            "foo.groovy",
10418            |metric| {
10419                assert_eq!(metric.loc.lloc(), 3);
10420            },
10421        );
10422    }
10423
10424    #[cfg(feature = "groovy")]
10425    #[test]
10426    fn groovy_no_local_variable_declaration_in_for_lloc() {
10427        // The variable declaration inside a classic `for` init slot
10428        // does NOT count as an LLOC (it's an expression part of the
10429        // for-loop). Same gating as Java's `java_for_lloc`.
10430        check_metrics::<GroovyParser>(
10431            "for (int i = 0; i < 10; i++) {
10432                println(i)
10433            }",
10434            "foo.groovy",
10435            |metric| {
10436                // for-statement (1) + expression-statement `println(i)` (1) = 2
10437                assert_eq!(metric.loc.lloc(), 2);
10438            },
10439        );
10440    }
10441
10442    #[cfg(feature = "groovy")]
10443    #[test]
10444    fn groovy_lambda_in_method_lloc() {
10445        // Closures contain a statement list — the dekobon grammar wraps
10446        // a single-expression body in `expression_statement` rather than
10447        // emitting the expression directly (as Java's `lambda_expression`
10448        // does), so a one-line closure body counts as its own LLOC.
10449        // Declaration `def f = …` (1) + closure body `x + 1` (1) +
10450        // call `f(3)` (1) = 3.
10451        check_metrics::<GroovyParser>(
10452            "class Foo {
10453                void bar() {
10454                    def f = { x -> x + 1 }
10455                    f(3)
10456                }
10457            }",
10458            "foo.groovy",
10459            |metric| {
10460                assert_eq!(metric.loc.lloc(), 3);
10461            },
10462        );
10463    }
10464
10465    #[cfg(feature = "groovy")]
10466    #[test]
10467    fn groovy_try_lloc() {
10468        // try-statement counts as one LLOC; the catch body's
10469        // statements count separately.
10470        check_metrics::<GroovyParser>(
10471            "void f() {
10472                try {
10473                    risky()
10474                } catch (Exception e) {
10475                    handle(e)
10476                }
10477            }",
10478            "foo.groovy",
10479            |metric| {
10480                // try(1) + risky() expr-stmt(1) + handle() expr-stmt(1) = 3
10481                assert_eq!(metric.loc.lloc(), 3);
10482            },
10483        );
10484    }
10485
10486    #[cfg(feature = "groovy")]
10487    #[test]
10488    fn groovy_class_loc() {
10489        // Source-file-level totals across multiple methods.
10490        check_metrics::<GroovyParser>(
10491            "class A {
10492                void f() {
10493                    int x = 1
10494                }
10495                void g() {
10496                    int y = 2
10497                }
10498            }",
10499            "foo.groovy",
10500            |metric| {
10501                // 8 lines of non-comment content: `class A {`, two
10502                // `void` headers, two `int … = …` body statements,
10503                // three closing braces.
10504                assert_eq!(metric.loc.ploc(), 8);
10505                assert_eq!(metric.loc.cloc(), 0);
10506                // Two expression-statement LLOCs (`int x = 1`,
10507                // `int y = 2`).
10508                assert_eq!(metric.loc.lloc(), 2);
10509            },
10510        );
10511    }
10512
10513    #[cfg(feature = "groovy")]
10514    #[test]
10515    fn groovy_partial_parse_recovers_unit() {
10516        // Malformed input parses with ERROR but still emits a Unit
10517        // root via `spaces.rs` fallback (lesson 9). The single
10518        // source line is counted as SLOC even when the parse fails
10519        // mid-expression.
10520        check_metrics::<GroovyParser>("def x = (((", "foo.groovy", |metric| {
10521            assert_eq!(metric.loc.sloc(), 1);
10522            assert_eq!(metric.loc.blank(), 0);
10523        });
10524    }
10525
10526    #[cfg(feature = "groovy")]
10527    #[test]
10528    fn groovy_sloc() {
10529        // Mirrors `java_sloc`: basic per-line count across a mix of
10530        // statements and a blank line.
10531        check_metrics::<GroovyParser>(
10532            "int a = 1
10533            int b = 2
10534
10535            int c = 3",
10536            "foo.groovy",
10537            |metric| {
10538                assert_eq!(metric.loc.sloc(), 4);
10539                assert_eq!(metric.loc.ploc(), 3);
10540                assert_eq!(metric.loc.blank(), 1);
10541            },
10542        );
10543    }
10544
10545    #[cfg(feature = "groovy")]
10546    #[test]
10547    fn groovy_single_ploc() {
10548        // Mirrors `java_single_ploc`: one non-blank, non-comment
10549        // line of code => ploc == 1.
10550        check_metrics::<GroovyParser>("int x = 42", "foo.groovy", |metric| {
10551            assert_eq!(metric.loc.ploc(), 1);
10552            assert_eq!(metric.loc.cloc(), 0);
10553        });
10554    }
10555
10556    #[cfg(feature = "groovy")]
10557    #[test]
10558    fn groovy_multi_ploc() {
10559        // Multiple statements on separate lines all contribute to
10560        // PLOC. Mirrors `java_multi_ploc`.
10561        check_metrics::<GroovyParser>(
10562            "int a = 1
10563            int b = 2
10564            int c = 3
10565            int d = 4",
10566            "foo.groovy",
10567            |metric| {
10568                assert_eq!(metric.loc.ploc(), 4);
10569                assert_eq!(metric.loc.lloc(), 4);
10570            },
10571        );
10572    }
10573
10574    #[cfg(feature = "groovy")]
10575    #[test]
10576    fn groovy_single_statement_lloc() {
10577        // A single expression statement contributes one LLOC.
10578        // Mirrors `java_single_statement_lloc`.
10579        check_metrics::<GroovyParser>("println 'hi'", "foo.groovy", |metric| {
10580            assert_eq!(metric.loc.lloc(), 1);
10581        });
10582    }
10583
10584    #[cfg(feature = "groovy")]
10585    #[test]
10586    fn groovy_for_lloc() {
10587        // The classical `for` statement itself counts as one LLOC;
10588        // the body's `println(i)` adds another. The init-slot
10589        // var-decl is suppressed by the LocalVariableDeclaration
10590        // ancestor-check (same rule as `java_for_lloc`).
10591        check_metrics::<GroovyParser>(
10592            "for (int i = 0; i < 100; i++) {
10593                println(i)
10594            }",
10595            "foo.groovy",
10596            |metric| {
10597                // ForStatement(1) + println-expr(1) = 2
10598                assert_eq!(metric.loc.lloc(), 2);
10599            },
10600        );
10601    }
10602
10603    #[cfg(feature = "groovy")]
10604    #[test]
10605    fn groovy_foreach_lloc() {
10606        // `for (item in list)` parses as `enhanced_for_statement` —
10607        // counts as one LLOC.
10608        check_metrics::<GroovyParser>(
10609            "for (item in items) {
10610                println(item)
10611            }",
10612            "foo.groovy",
10613            |metric| {
10614                // EnhancedForStatement(1) + println(1) = 2
10615                assert_eq!(metric.loc.lloc(), 2);
10616            },
10617        );
10618    }
10619
10620    #[cfg(feature = "groovy")]
10621    #[test]
10622    fn groovy_while_lloc() {
10623        // `while` itself is one LLOC; each body statement adds
10624        // another. Mirrors `java_while_lloc`.
10625        check_metrics::<GroovyParser>(
10626            "int i = 0
10627            while (i < 10) {
10628                i++
10629                println(i)
10630            }",
10631            "foo.groovy",
10632            |metric| {
10633                // int i = 0 (1) + while (1) + i++ (1) + println (1) = 4
10634                assert_eq!(metric.loc.lloc(), 4);
10635            },
10636        );
10637    }
10638
10639    #[cfg(feature = "groovy")]
10640    #[test]
10641    fn groovy_do_while_lloc() {
10642        // `do…while` is one LLOC plus its body. Mirrors
10643        // `java_do_while_lloc`.
10644        check_metrics::<GroovyParser>(
10645            "int i = 0
10646            do {
10647                i++
10648            } while (i < 5)",
10649            "foo.groovy",
10650            |metric| {
10651                // int i = 0 (1) + do (1) + i++ (1) = 3
10652                assert_eq!(metric.loc.lloc(), 3);
10653            },
10654        );
10655    }
10656
10657    #[cfg(feature = "groovy")]
10658    #[test]
10659    fn groovy_continue_lloc() {
10660        // `continue` is an LLOC. Same gating as `java_continue_lloc`.
10661        check_metrics::<GroovyParser>(
10662            "for (int i = 0; i < 10; i++) {
10663                if (i == 5) {
10664                    continue
10665                }
10666                println(i)
10667            }",
10668            "foo.groovy",
10669            |metric| {
10670                // for(1) + if(1) + continue(1) + println(1) = 4
10671                assert_eq!(metric.loc.lloc(), 4);
10672            },
10673        );
10674    }
10675
10676    #[cfg(feature = "groovy")]
10677    #[test]
10678    fn groovy_expressions_lloc() {
10679        // A bag of expression statements: each independent
10680        // expr-stmt is one LLOC. Mirrors `java_expressions_lloc`.
10681        check_metrics::<GroovyParser>(
10682            "int a = 1
10683            a = 2
10684            a += 3
10685            println(a)
10686            doSomething()",
10687            "foo.groovy",
10688            |metric| {
10689                // 5 expression-statement lines.
10690                assert_eq!(metric.loc.lloc(), 5);
10691            },
10692        );
10693    }
10694
10695    #[cfg(feature = "groovy")]
10696    #[test]
10697    fn groovy_throw_lloc() {
10698        // `throw` is one LLOC via the `ThrowStatement` arm.
10699        check_metrics::<GroovyParser>(
10700            "throw new RuntimeException('bad')",
10701            "foo.groovy",
10702            |metric| {
10703                assert_eq!(metric.loc.lloc(), 1);
10704            },
10705        );
10706    }
10707
10708    #[cfg(feature = "groovy")]
10709    #[test]
10710    fn groovy_general_loc() {
10711        // Comprehensive mix: class + method + control flow.
10712        // Mirrors `java_general_loc`'s coverage shape.
10713        //
10714        // LLOC = 4, fully attributable:
10715        //   IfStatement (the outer if/else):     +1
10716        //   `println(x)`     (JuxtFunctionCall):  +1
10717        //   `println 'neg'` (JuxtFunctionCall):  +1
10718        //   `return`        (ReturnStatement):   +1
10719        // The else-branch's `expression_statement (closure)`
10720        // wrapper does NOT count — see the bare-Closure carve-out
10721        // in `impl Loc for GroovyCode::compute`.
10722        check_metrics::<GroovyParser>(
10723            "class A {
10724                void f(int x) {
10725                    if (x > 0) {
10726                        println(x)
10727                    } else {
10728                        println 'neg'
10729                    }
10730                    return
10731                }
10732            }",
10733            "foo.groovy",
10734            |metric| {
10735                assert_eq!(metric.loc.lloc(), 4);
10736                assert_eq!(metric.loc.cloc(), 0);
10737            },
10738        );
10739    }
10740
10741    #[cfg(feature = "csharp")]
10742    #[test]
10743    fn csharp_local_function_in_method_lloc() {
10744        // C# local functions (`int Inner(int x) { ... }` inside `Bar()`)
10745        // open their own function space, so the outer method sees only
10746        // `return Inner(2);` plus the inner body's `return x + 1;`.
10747        check_metrics::<CsharpParser>(
10748            "class Foo {\n    int Bar() {\n        int Inner(int x) { return x + 1; }\n        return Inner(2);\n    }\n}\n",
10749            "foo.cs",
10750            |metric| {
10751                assert_eq!(metric.loc.sloc(), 6);
10752                assert_eq!(metric.loc.ploc(), 6);
10753                assert_eq!(metric.loc.lloc(), 2);
10754                assert_eq!(metric.loc.cloc(), 0);
10755                assert_eq!(metric.loc.blank(), 0);
10756                insta::assert_json_snapshot!(metric.loc);
10757            },
10758        );
10759    }
10760
10761    #[cfg(feature = "csharp")]
10762    #[test]
10763    fn csharp_lambda_in_method_lloc() {
10764        // C# lambdas are expressions: the two LLOC come from the
10765        // `Func<int,int> f = x => x + 1;` declaration and the `f(3);` call.
10766        check_metrics::<CsharpParser>(
10767            "class Foo {\n    void Bar() {\n        System.Func<int, int> f = x => x + 1;\n        f(3);\n    }\n}\n",
10768            "foo.cs",
10769            |metric| {
10770                assert_eq!(metric.loc.sloc(), 6);
10771                assert_eq!(metric.loc.ploc(), 6);
10772                assert_eq!(metric.loc.lloc(), 2);
10773                assert_eq!(metric.loc.cloc(), 0);
10774                assert_eq!(metric.loc.blank(), 0);
10775                insta::assert_json_snapshot!(metric.loc);
10776            },
10777        );
10778    }
10779
10780    #[cfg(feature = "cpp")]
10781    #[test]
10782    fn cpp_lambda_in_function_lloc() {
10783        // C++11 lambdas are expressions. The outer function `bar()` produces
10784        // two LLOC for the body: `auto f = [](int x) { return x + 1; };` and
10785        // `return f(2);`. The lambda's inner `return x + 1;` is part of the
10786        // lambda body inside the same function space (lambdas do not open a
10787        // new FuncSpace in this implementation), so it adds a third LLOC.
10788        // Closes the parity gap with #195 (which covered 11 other
10789        // languages but omitted C++).
10790        check_metrics::<CppParser>(
10791            "int bar() {\n    auto f = [](int x) { return x + 1; };\n    return f(2);\n}\n",
10792            "foo.cpp",
10793            |metric| {
10794                assert_eq!(metric.loc.sloc(), 4);
10795                assert_eq!(metric.loc.ploc(), 4);
10796                assert_eq!(metric.loc.lloc(), 3);
10797                assert_eq!(metric.loc.cloc(), 0);
10798                assert_eq!(metric.loc.blank(), 0);
10799                insta::assert_json_snapshot!(metric.loc);
10800            },
10801        );
10802    }
10803
10804    #[cfg(feature = "javascript")]
10805    #[test]
10806    fn javascript_nested_function_lloc() {
10807        // Nested function_declaration: 4 LLOC = outer's `return inner();`,
10808        // inner's `return 1;`, plus the two function declarations
10809        // themselves (the JS Checker counts function declarations as LLOC).
10810        check_metrics::<JavascriptParser>(
10811            "function outer() {\n    function inner() {\n        return 1;\n    }\n    return inner();\n}\n",
10812            "foo.js",
10813            |metric| {
10814                assert_eq!(metric.loc.sloc(), 6);
10815                assert_eq!(metric.loc.ploc(), 6);
10816                assert_eq!(metric.loc.lloc(), 2);
10817                assert_eq!(metric.loc.cloc(), 0);
10818                assert_eq!(metric.loc.blank(), 0);
10819                insta::assert_json_snapshot!(metric.loc);
10820            },
10821        );
10822    }
10823
10824    #[cfg(feature = "javascript")]
10825    #[test]
10826    fn javascript_arrow_function_lloc() {
10827        // The arrow function `(x) => x + 1` is an expression: the LLOC
10828        // come from `const inner = ...;` and `return inner(2);`.
10829        check_metrics::<JavascriptParser>(
10830            "function outer() {\n    const inner = (x) => x + 1;\n    return inner(2);\n}\n",
10831            "foo.js",
10832            |metric| {
10833                assert_eq!(metric.loc.sloc(), 4);
10834                assert_eq!(metric.loc.ploc(), 4);
10835                assert_eq!(metric.loc.lloc(), 2);
10836                assert_eq!(metric.loc.cloc(), 0);
10837                assert_eq!(metric.loc.blank(), 0);
10838                insta::assert_json_snapshot!(metric.loc);
10839            },
10840        );
10841    }
10842
10843    #[cfg(feature = "kotlin")]
10844    #[test]
10845    fn kotlin_lambda_literal_in_fun_lloc() {
10846        // A lambda literal (`{ x -> x + 1 }`) assigned to a `val` plus the
10847        // following call yields two LLOC at the outer function.
10848        check_metrics::<KotlinParser>(
10849            "fun outer() {\n    val f: (Int) -> Int = { x -> x + 1 }\n    f(3)\n}\n",
10850            "foo.kt",
10851            |metric| {
10852                assert_eq!(metric.loc.sloc(), 4);
10853                assert_eq!(metric.loc.ploc(), 4);
10854                assert_eq!(metric.loc.lloc(), 2);
10855                assert_eq!(metric.loc.cloc(), 0);
10856                assert_eq!(metric.loc.blank(), 0);
10857                insta::assert_json_snapshot!(metric.loc);
10858            },
10859        );
10860    }
10861
10862    #[cfg(feature = "kotlin")]
10863    #[test]
10864    fn kotlin_local_fun_in_fun_lloc() {
10865        // Kotlin's local `fun inner(...)` is also a function_declaration,
10866        // so it opens its own space; the outer LLOC reduces to `inner(3)`,
10867        // and the inner body contributes the second LLOC.
10868        check_metrics::<KotlinParser>(
10869            "fun outer() {\n    fun inner(x: Int): Int { return x + 1 }\n    inner(3)\n}\n",
10870            "foo.kt",
10871            |metric| {
10872                assert_eq!(metric.loc.sloc(), 4);
10873                assert_eq!(metric.loc.ploc(), 4);
10874                assert_eq!(metric.loc.lloc(), 2);
10875                assert_eq!(metric.loc.cloc(), 0);
10876                assert_eq!(metric.loc.blank(), 0);
10877                insta::assert_json_snapshot!(metric.loc);
10878            },
10879        );
10880    }
10881
10882    #[cfg(feature = "kotlin")]
10883    #[test]
10884    fn kotlin_object_expression_in_fun_lloc() {
10885        // An `object : Runnable { ... }` expression with an overridden
10886        // method whose body invokes `println("hi")`. LLOC: `val r = ...`,
10887        // the override's body call, and the outer `r.run()` call = 3.
10888        check_metrics::<KotlinParser>(
10889            "fun outer() {\n    val r = object : Runnable { override fun run() { println(\"hi\") } }\n    r.run()\n}\n",
10890            "foo.kt",
10891            |metric| {
10892                assert_eq!(metric.loc.sloc(), 4);
10893                assert_eq!(metric.loc.ploc(), 4);
10894                assert_eq!(metric.loc.lloc(), 3);
10895                assert_eq!(metric.loc.cloc(), 0);
10896                assert_eq!(metric.loc.blank(), 0);
10897                insta::assert_json_snapshot!(metric.loc);
10898            },
10899        );
10900    }
10901
10902    #[cfg(feature = "go")]
10903    #[test]
10904    fn go_function_literal_initializer_lloc() {
10905        // `inner := func(x int) int { return x + 1 }` — the function
10906        // literal opens its own space; LLOC visible on the outer space:
10907        // the assignment + `return inner(2)` = 2, plus the literal's
10908        // `return x + 1` body = 3 aggregated.
10909        check_metrics::<GoParser>(
10910            "package main\nfunc outer() int {\n    inner := func(x int) int { return x + 1 }\n    return inner(2)\n}\n",
10911            "foo.go",
10912            |metric| {
10913                assert_eq!(metric.loc.sloc(), 5);
10914                assert_eq!(metric.loc.ploc(), 5);
10915                assert_eq!(metric.loc.lloc(), 3);
10916                assert_eq!(metric.loc.cloc(), 0);
10917                assert_eq!(metric.loc.blank(), 0);
10918                insta::assert_json_snapshot!(metric.loc);
10919            },
10920        );
10921    }
10922
10923    #[cfg(feature = "php")]
10924    #[test]
10925    fn php_anonymous_function_in_function_lloc() {
10926        // Anonymous function `function ($x) { return $x + 1; }`: outer
10927        // sees the assignment + `return $f(2);`, the closure body adds
10928        // `return $x + 1;` for 3 LLOC aggregated.
10929        check_metrics::<PhpParser>(
10930            "<?php\nfunction outer() {\n    $f = function ($x) { return $x + 1; };\n    return $f(2);\n}\n",
10931            "foo.php",
10932            |metric| {
10933                assert_eq!(metric.loc.sloc(), 5);
10934                assert_eq!(metric.loc.ploc(), 5);
10935                assert_eq!(metric.loc.lloc(), 3);
10936                assert_eq!(metric.loc.cloc(), 0);
10937                assert_eq!(metric.loc.blank(), 0);
10938                insta::assert_json_snapshot!(metric.loc);
10939            },
10940        );
10941    }
10942
10943    #[cfg(feature = "php")]
10944    #[test]
10945    fn php_arrow_function_in_function_lloc() {
10946        // The `fn ($x) => $x + 1` arrow function is an expression; the
10947        // outer function sees only its assignment and the `return $f(2);`.
10948        check_metrics::<PhpParser>(
10949            "<?php\nfunction outer() {\n    $f = fn ($x) => $x + 1;\n    return $f(2);\n}\n",
10950            "foo.php",
10951            |metric| {
10952                assert_eq!(metric.loc.sloc(), 5);
10953                assert_eq!(metric.loc.ploc(), 5);
10954                assert_eq!(metric.loc.lloc(), 2);
10955                assert_eq!(metric.loc.cloc(), 0);
10956                assert_eq!(metric.loc.blank(), 0);
10957                insta::assert_json_snapshot!(metric.loc);
10958            },
10959        );
10960    }
10961
10962    #[cfg(feature = "lua")]
10963    #[test]
10964    fn lua_nested_local_function_lloc() {
10965        // Two nested `local function` declarations: outer + inner both
10966        // count as `function_declaration` LLOC, plus the two `return`
10967        // statements = 4 aggregated.
10968        check_metrics::<LuaParser>(
10969            "local function outer()\n    local function inner()\n        return 1\n    end\n    return inner()\nend\n",
10970            "foo.lua",
10971            |metric| {
10972                assert_eq!(metric.loc.sloc(), 6);
10973                assert_eq!(metric.loc.ploc(), 6);
10974                assert_eq!(metric.loc.lloc(), 4);
10975                assert_eq!(metric.loc.cloc(), 0);
10976                assert_eq!(metric.loc.blank(), 0);
10977                insta::assert_json_snapshot!(metric.loc);
10978            },
10979        );
10980    }
10981
10982    #[cfg(feature = "lua")]
10983    #[test]
10984    fn lua_function_expression_in_local_decl_lloc() {
10985        // `local f = function (x) return x + 1 end` — the function
10986        // expression is its own space; aggregated LLOC: outer
10987        // declaration, the inner expression's declaration, the inner
10988        // `return x + 1`, and the outer `return f(2)` = 4.
10989        check_metrics::<LuaParser>(
10990            "local function outer()\n    local f = function (x) return x + 1 end\n    return f(2)\nend\n",
10991            "foo.lua",
10992            |metric| {
10993                assert_eq!(metric.loc.sloc(), 4);
10994                assert_eq!(metric.loc.ploc(), 4);
10995                assert_eq!(metric.loc.lloc(), 4);
10996                assert_eq!(metric.loc.cloc(), 0);
10997                assert_eq!(metric.loc.blank(), 0);
10998                insta::assert_json_snapshot!(metric.loc);
10999            },
11000        );
11001    }
11002
11003    #[cfg(feature = "tcl")]
11004    #[test]
11005    fn tcl_apply_closure_lloc() {
11006        // `apply $f 2` is a regular Tcl command, not a separate function
11007        // space — tree-sitter-tcl does not model `apply { ... }` as a
11008        // closure construct distinct from any other command. We assert
11009        // the observed LLOC (proc, set, apply, plus the nested `expr`
11010        // command substitution inside the lambda body) so any future
11011        // change to lambda-body counting is caught here.
11012        check_metrics::<TclParser>(
11013            "proc outer {} {\n    set f [list x {return [expr {$x + 1}]}]\n    apply $f 2\n}\n",
11014            "foo.tcl",
11015            |metric| {
11016                assert_eq!(metric.loc.sloc(), 4);
11017                assert_eq!(metric.loc.ploc(), 4);
11018                assert_eq!(metric.loc.lloc(), 4);
11019                assert_eq!(metric.loc.cloc(), 0);
11020                assert_eq!(metric.loc.blank(), 0);
11021                insta::assert_json_snapshot!(metric.loc);
11022            },
11023        );
11024    }
11025
11026    #[cfg(feature = "perl")]
11027    #[test]
11028    fn perl_anonymous_sub_in_sub_lloc() {
11029        // Anonymous sub `sub { ... }` opens its own function space; the
11030        // outer LLOC counts the `my $f = ...;` declaration plus
11031        // `return $f->(2);`, and the anonymous sub contributes
11032        // `return $_[0] + 1;` for 2 LLOC.
11033        //
11034        // NOTE: a prior LLOC for this construct exists as
11035        // `perl_lloc_anonymous_function` (top-level form) — this test
11036        // asserts the same shape *inside* another sub, exercising space
11037        // nesting.
11038        check_metrics::<PerlParser>(
11039            "sub outer {\n    my $f = sub { return $_[0] + 1 };\n    return $f->(2);\n}\n",
11040            "foo.pl",
11041            |metric| {
11042                assert_eq!(metric.loc.sloc(), 4);
11043                assert_eq!(metric.loc.ploc(), 4);
11044                assert_eq!(metric.loc.lloc(), 2);
11045                assert_eq!(metric.loc.cloc(), 0);
11046                assert_eq!(metric.loc.blank(), 0);
11047                insta::assert_json_snapshot!(metric.loc);
11048            },
11049        );
11050    }
11051
11052    #[cfg(feature = "perl")]
11053    #[test]
11054    fn perl_named_sub_in_sub_lloc() {
11055        // Perl `sub` declarations are not LLOC (see
11056        // `perl_lloc_function_definition_not_counted`); inside `outer`,
11057        // only `return inner();` is LLOC, and `inner`'s `return 1` is in
11058        // its own space contributing one more aggregated LLOC.
11059        // Total aggregated LLOC: 1.
11060        //
11061        // Observation: lloc=1, not 2. Perl LLOC is anchored on `;`
11062        // tokens whose parent is `SourceFile` or `Block` (see
11063        // `PerlCode::compute` in this file). The bare `return 1` inside
11064        // `sub inner { ... }` has no trailing `;`, so it does not bump
11065        // LLOC. The outer `return inner();` carries the only SEMI.
11066        // This is intentional Perl behaviour and not a bug — Perl
11067        // requires `;` between statements; a single trailing statement
11068        // before `}` is syntactically optional. Asserted as-is.
11069        check_metrics::<PerlParser>(
11070            "sub outer {\n    sub inner { return 1 }\n    return inner();\n}\n",
11071            "foo.pl",
11072            |metric| {
11073                assert_eq!(metric.loc.sloc(), 4);
11074                assert_eq!(metric.loc.ploc(), 4);
11075                assert_eq!(metric.loc.lloc(), 1);
11076                assert_eq!(metric.loc.cloc(), 0);
11077                assert_eq!(metric.loc.blank(), 0);
11078                insta::assert_json_snapshot!(metric.loc);
11079            },
11080        );
11081    }
11082
11083    #[cfg(feature = "elixir")]
11084    #[test]
11085    fn elixir_fn_inside_def_lloc() {
11086        // `fn x -> x + 1 end` inside a `def`: defmodule + def +
11087        // `f = fn ...` + `f.(2)` = 4 own LLOC for the Unit space, plus
11088        // the anonymous fn body `x + 1` = 1 nested, aggregated 5.
11089        check_metrics::<ElixirParser>(
11090            "defmodule Foo do\n  def outer do\n    f = fn x -> x + 1 end\n    f.(2)\n  end\nend\n",
11091            "foo.ex",
11092            |metric| {
11093                assert_eq!(metric.loc.sloc(), 6);
11094                assert_eq!(metric.loc.ploc(), 6);
11095                assert_eq!(metric.loc.lloc(), 5);
11096                assert_eq!(metric.loc.cloc(), 0);
11097                assert_eq!(metric.loc.blank(), 0);
11098                insta::assert_json_snapshot!(metric.loc);
11099            },
11100        );
11101    }
11102
11103    /// Regression for #437: `Loc` min/max must fold each nested function
11104    /// space's *own* min/max, not its aggregate value, so the smallest
11105    /// (and largest) leaf function propagates to the root. Before the fix,
11106    /// merge folded `other.sloc()` and a guarded `compute_minmax` skipped
11107    /// containers, so `sloc_min` reflected only top-level spaces.
11108    ///
11109    /// Layout: Unit (whole file) -> class C -> two methods of *different*
11110    /// sizes. The smaller method is the global minimum; the file/class
11111    /// spans are larger; `sloc_min` must be the small leaf, not the class
11112    /// or unit span. Verified against the pre-fix code by reverting the
11113    /// merge/compute_minmax change (it reports the unit span instead).
11114    #[cfg(feature = "rust")]
11115    #[test]
11116    fn rust_nested_min_max_propagates() {
11117        check_metrics::<RustParser>(
11118            "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",
11119            "c.rs",
11120            |metric| {
11121                // Spaces: Unit + impl C + small() + big().
11122                // small() spans 3 rows (signature .. closing brace),
11123                // big() spans 5 rows. The Unit/impl spans are larger
11124                // still. sloc_min must be the smallest leaf (3), not the
11125                // top-level span; sloc_max must be the largest space.
11126                let loc = &metric.loc;
11127                assert!(
11128                    loc.sloc_min() <= loc.sloc(),
11129                    "sloc_min {} must not exceed unit sloc {}",
11130                    loc.sloc_min(),
11131                    loc.sloc()
11132                );
11133                assert_eq!(loc.sloc_min(), 3, "smallest leaf method span");
11134                assert_eq!(loc.sloc_max(), loc.sloc(), "largest space is the unit");
11135                // The smallest leaf has one statement; min must reflect it.
11136                assert_eq!(loc.lloc_min(), 1);
11137            },
11138        );
11139    }
11140
11141    /// Java sibling of `rust_nested_min_max_propagates` (#437). The
11142    /// `java_class_loc` snapshot above showed the bug directly: a class
11143    /// with methods reported `sloc_min == sloc` (the unit span). Here we
11144    /// assert the smallest method propagates.
11145    #[cfg(feature = "java")]
11146    #[test]
11147    fn java_nested_min_max_propagates() {
11148        check_metrics::<JavaParser>(
11149            "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",
11150            "C.java",
11151            |metric| {
11152                // Spaces: Unit + class C + small() + big().
11153                let loc = &metric.loc;
11154                assert!(loc.sloc_min() <= loc.sloc());
11155                assert_eq!(loc.sloc_min(), 3, "smallest leaf method span");
11156                assert_eq!(loc.sloc_max(), loc.sloc());
11157                assert_eq!(loc.lloc_min(), 1);
11158            },
11159        );
11160    }
11161
11162    /// Python sibling of `rust_nested_min_max_propagates` (#437). Python
11163    /// nesting is class -> method, mirroring the worked example in the
11164    /// issue (file -> class C -> method m).
11165    #[cfg(feature = "python")]
11166    #[test]
11167    fn python_nested_min_max_propagates() {
11168        check_metrics::<PythonParser>(
11169            "class C:\n    def small(self):\n        x = 1\n    def big(self):\n        a = 1\n        b = 2\n        c = 3\n",
11170            "c.py",
11171            |metric| {
11172                // Spaces: Unit + class C + small() + big().
11173                let loc = &metric.loc;
11174                assert!(loc.sloc_min() <= loc.sloc());
11175                assert_eq!(loc.sloc_min(), 2, "smallest leaf method span");
11176                assert_eq!(loc.sloc_max(), loc.sloc());
11177                assert_eq!(loc.lloc_min(), 1);
11178            },
11179        );
11180    }
11181
11182    /// `blank()` is `sloc - ploc - only_comment_lines`, an f64 subtraction
11183    /// that can go negative when a space's physical and comment line
11184    /// attribution overlaps its span row count. It must clamp at 0 so the
11185    /// serialized value is never negative (#437).
11186    ///
11187    /// Built from a synthetic [`Stats`] rather than a parsed fixture: the
11188    /// current grammars do not emit a parsed space whose root `sloc` is
11189    /// smaller than `ploc + only_comment_lines`, so a source fixture cannot
11190    /// drive the subtraction negative and would pass against the pre-clamp
11191    /// code (proving nothing). Setting `sloc = ploc = only_comment_lines = 1`
11192    /// yields a pre-clamp `blank` of `1 - 1 - 1 = -1`; reverting the
11193    /// `.max(0.0)` makes this assertion fail with `-1`.
11194    #[test]
11195    fn blank_clamps_negative_to_zero() {
11196        let mut stats = Stats::default();
11197        // A single-row span ending mid-line => sloc() of 1 row.
11198        stats.sloc.start = 0;
11199        // End row 0 ending mid-line, so that row counts: sloc() == 1.
11200        stats.sloc.end_line = 1;
11201        // ploc() is the cardinality of the physical-line set => 1.
11202        stats.ploc.lines.insert(0);
11203        // One comment-only line on the same single row.
11204        stats.cloc.only_comment_line_starts.insert(0);
11205
11206        // Pre-clamp this is 1 - 1 - 1 = -1.
11207        assert_eq!(stats.sloc(), 1);
11208        assert_eq!(stats.ploc(), 1);
11209        assert_eq!(
11210            stats.blank(),
11211            0,
11212            "blank() must clamp the negative subtraction to 0"
11213        );
11214    }
11215
11216    /// A physical line shared by two *sibling* function spaces must
11217    /// count once in the parent that merges them.
11218    ///
11219    /// This is the semantics `Ploc::merge` / `Cloc::merge` encode, and
11220    /// the reason the per-space line stores are sets rather than
11221    /// counters. It is asserted here across four language families —
11222    /// Rust, the C family, the JS family, and Python's indentation-based
11223    /// spaces — because the merge is shared by all of them and each
11224    /// family reaches it through a different `Loc::compute` body.
11225    ///
11226    /// Every fixture puts both spaces on one row, so a merge that summed
11227    /// instead of unioning would report `ploc`/`cloc` of 2 against an
11228    /// `sloc` of 1 — an impossible reading that also drives `blank`
11229    /// negative.
11230    #[cfg(all(
11231        feature = "cpp",
11232        feature = "javascript",
11233        feature = "python",
11234        feature = "rust"
11235    ))]
11236    #[test]
11237    fn sibling_spaces_sharing_a_line_count_it_once() {
11238        check_metrics::<RustParser>(
11239            "fn a() { let x = 1; } fn b() { let y = 2; }",
11240            "foo.rs",
11241            |metric| {
11242                assert_eq!(metric.loc.sloc(), 1);
11243                assert_eq!(metric.loc.ploc(), 1, "one physical row, two spaces");
11244                assert_eq!(metric.loc.blank(), 0);
11245            },
11246        );
11247
11248        check_metrics::<CppParser>(
11249            "int a() { return 1; } int b() { return 2; }",
11250            "foo.cpp",
11251            |metric| {
11252                assert_eq!(metric.loc.sloc(), 1);
11253                assert_eq!(metric.loc.ploc(), 1, "one physical row, two spaces");
11254                assert_eq!(metric.loc.blank(), 0);
11255            },
11256        );
11257
11258        check_metrics::<JavascriptParser>(
11259            "function a() { let x = 1; } function b() { let y = 2; }",
11260            "foo.js",
11261            |metric| {
11262                assert_eq!(metric.loc.sloc(), 1);
11263                assert_eq!(metric.loc.ploc(), 1, "one physical row, two spaces");
11264                assert_eq!(metric.loc.blank(), 0);
11265            },
11266        );
11267
11268        // Python cannot open two spaces on one row, so the shared row is
11269        // the nested `def`'s: it belongs to the inner space's span and to
11270        // the outer space's, and the unit merges both.
11271        check_metrics::<PythonParser>(
11272            "def a():
11273    def b(): return 1",
11274            "foo.py",
11275            |metric| {
11276                assert_eq!(metric.loc.sloc(), 2);
11277                assert_eq!(metric.loc.ploc(), 2, "two physical rows, three spaces");
11278                assert_eq!(metric.loc.blank(), 0);
11279            },
11280        );
11281    }
11282
11283    /// The same union property for comment lines: two sibling spaces that
11284    /// each carry a comment on the one shared row must yield `cloc == 1`.
11285    ///
11286    /// Summing rather than unioning would report `cloc == 2` against
11287    /// `sloc == 1`, the `cloc > sloc` state that pushes MI's
11288    /// comments_percentage above 100% (the failure mode of issue #461,
11289    /// here across the space merge rather than within one space).
11290    #[cfg(all(feature = "cpp", feature = "rust"))]
11291    #[test]
11292    fn sibling_spaces_sharing_a_comment_line_count_it_once() {
11293        check_metrics::<CppParser>(
11294            "int a() { /*x*/ return 1; } int b() { /*y*/ return 2; }",
11295            "foo.cpp",
11296            |metric| {
11297                assert_eq!(metric.loc.sloc(), 1);
11298                assert_eq!(metric.loc.cloc(), 1, "one comment row, two spaces");
11299                assert!(
11300                    metric.loc.cloc() <= metric.loc.sloc(),
11301                    "cloc must never exceed sloc"
11302                );
11303            },
11304        );
11305
11306        check_metrics::<RustParser>(
11307            "fn a() { /*x*/ let p = 1; } fn b() { /*y*/ let q = 2; }",
11308            "foo.rs",
11309            |metric| {
11310                assert_eq!(metric.loc.sloc(), 1);
11311                assert_eq!(metric.loc.cloc(), 1, "one comment row, two spaces");
11312                assert!(
11313                    metric.loc.cloc() <= metric.loc.sloc(),
11314                    "cloc must never exceed sloc"
11315                );
11316            },
11317        );
11318    }
11319
11320    /// A row inside a chain of nested spaces is folded upward once per
11321    /// level, and must still count once at the top.
11322    ///
11323    /// Fifteen levels rather than two: at one level a union and a sum
11324    /// agree whenever the sets happen to be disjoint, and the point of
11325    /// #1109 is the repeated fold. The body row belongs to every level's
11326    /// span, so a merge that accumulated would report `ploc == 15`.
11327    #[cfg(feature = "rust")]
11328    #[test]
11329    fn a_row_folded_through_nested_spaces_counts_once() {
11330        const DEPTH: usize = 15;
11331        let source = format!(
11332            "{}let x = 1;{}",
11333            "fn f() { ".repeat(DEPTH),
11334            "} ".repeat(DEPTH)
11335        );
11336
11337        check_metrics::<RustParser>(&source, "foo.rs", |metric| {
11338            assert_eq!(metric.loc.sloc(), 1);
11339            assert_eq!(metric.loc.ploc(), 1, "one physical row, {DEPTH} spaces");
11340            assert_eq!(metric.loc.ploc_max(), 1);
11341            assert_eq!(metric.loc.blank(), 0);
11342        });
11343    }
11344
11345    /// Two inline block comments on a single code line must count as a
11346    /// single comment line, not one per comment node. Pre-fix this
11347    /// reported `cloc = 2` (one increment per node) for a one-line
11348    /// construct, violating `cloc <= sloc`/`cloc <= ploc` and pushing
11349    /// the MI comments_percentage above 100% (issue #461). Reverting
11350    /// the per-line de-dup in `add_code_comment_line` makes the
11351    /// `cloc == 1` assertions fail with `2`.
11352    #[cfg(feature = "cpp")]
11353    #[test]
11354    fn cloc_multiple_block_comments_one_line_cpp() {
11355        check_metrics::<CppParser>(
11356            "int f(int /*a*/, int /*b*/) { return 1; }",
11357            "foo.cpp",
11358            |metric| {
11359                assert_eq!(metric.loc.cloc(), 1, "two inline comments => 1 cloc");
11360                assert!(
11361                    metric.loc.cloc() <= metric.loc.sloc(),
11362                    "cloc must not exceed sloc"
11363                );
11364                assert!(
11365                    metric.loc.cloc() <= metric.loc.ploc(),
11366                    "cloc must not exceed ploc for a single-line construct"
11367                );
11368            },
11369        );
11370    }
11371
11372    /// Sibling-language coverage: `add_cloc_lines` is shared across
11373    /// every block-comment language, so the Rust path must behave
11374    /// identically to C++ (issue #461).
11375    #[cfg(feature = "rust")]
11376    #[test]
11377    fn cloc_multiple_block_comments_one_line_rust() {
11378        check_metrics::<RustParser>(
11379            "fn f(/*a*/ x: i32, /*b*/ y: i32) -> i32 { 1 }",
11380            "foo.rs",
11381            |metric| {
11382                assert_eq!(metric.loc.cloc(), 1, "two inline comments => 1 cloc");
11383                assert!(
11384                    metric.loc.cloc() <= metric.loc.sloc(),
11385                    "cloc must not exceed sloc"
11386                );
11387            },
11388        );
11389    }
11390
11391    /// Guard against over-de-dup: a single multi-line block comment
11392    /// must still contribute one comment line per physical line it
11393    /// spans. The de-dup keys on the start row only, so the three
11394    /// independent continuation lines are unaffected (issue #461).
11395    #[cfg(feature = "cpp")]
11396    #[test]
11397    fn cloc_multiline_block_comment_counts_each_line() {
11398        check_metrics::<CppParser>(
11399            "int g() {\n  /* l1\n     l2\n     l3 */\n  return 0;\n}",
11400            "foo.cpp",
11401            |metric| {
11402                assert_eq!(
11403                    metric.loc.cloc(),
11404                    3,
11405                    "a 3-line block comment counts 3 comment lines"
11406                );
11407            },
11408        );
11409    }
11410
11411    /// Two *standalone* block comments on a single physical line (no
11412    /// code) must count as one comment line, not one per node. #461
11413    /// deduped only inline co-located comments via the code-comment
11414    /// path; the standalone path bumped `only_comment_lines` per node,
11415    /// so `/*a*/ /*b*/` reported `cloc = 2` for a single line —
11416    /// violating `cloc <= sloc`. Reverting the per-line set in
11417    /// `add_only_comment_lines` makes the `cloc == 1` assertion fail
11418    /// with `2` (verified by reverting to `only_comment_lines += …`).
11419    #[cfg(feature = "cpp")]
11420    #[test]
11421    fn cloc_multiple_standalone_block_comments_one_line_cpp() {
11422        check_metrics::<CppParser>("/*a*/ /*b*/", "foo.cpp", |metric| {
11423            assert_eq!(
11424                metric.loc.cloc(),
11425                1,
11426                "two standalone comments on one line => 1 cloc"
11427            );
11428            assert_eq!(metric.loc.sloc(), 1);
11429            assert!(
11430                metric.loc.cloc() <= metric.loc.sloc(),
11431                "cloc must not exceed sloc"
11432            );
11433        });
11434    }
11435
11436    /// Sibling-language coverage: the standalone de-dup lives in the
11437    /// shared `add_only_comment_lines` helper, so Rust must match C++
11438    /// (issue #461 follow-up).
11439    #[cfg(feature = "rust")]
11440    #[test]
11441    fn cloc_multiple_standalone_block_comments_one_line_rust() {
11442        check_metrics::<RustParser>("/*a*/ /*b*/", "foo.rs", |metric| {
11443            assert_eq!(
11444                metric.loc.cloc(),
11445                1,
11446                "two standalone comments on one line => 1 cloc"
11447            );
11448            assert_eq!(metric.loc.sloc(), 1);
11449            assert!(
11450                metric.loc.cloc() <= metric.loc.sloc(),
11451                "cloc must not exceed sloc"
11452            );
11453        });
11454    }
11455
11456    /// File-level guard: a comment-only physical line followed by a
11457    /// real code line still counts the comment line once and never
11458    /// exceeds sloc, even after the per-space merge that previously
11459    /// summed `code_comment_lines` (and double-counted a boundary line)
11460    /// rather than reading the per-line set. Three standalone comments
11461    /// share line 1, so the whole file has exactly one comment line.
11462    #[cfg(feature = "cpp")]
11463    #[test]
11464    fn cloc_standalone_comments_then_code_no_double_count() {
11465        check_metrics::<CppParser>("/*a*/ /*b*/ /*c*/\nint x = 1;\n", "foo.cpp", |metric| {
11466            assert_eq!(metric.loc.sloc(), 2);
11467            assert_eq!(
11468                metric.loc.cloc(),
11469                1,
11470                "only line 1 carries comments => 1 cloc"
11471            );
11472            assert!(
11473                metric.loc.cloc() <= metric.loc.sloc(),
11474                "cloc must not exceed sloc"
11475            );
11476        });
11477    }
11478
11479    #[cfg(feature = "irules")]
11480    #[test]
11481    fn irules_multiline_quoted_word_credits_every_row_to_ploc() {
11482        // Regression test for #1260, the dialect half of the Tcl fix: a
11483        // `quoted_word` carries no child per row, so its interior rows
11484        // reached neither PLOC nor CLOC and
11485        // `blank = sloc - ploc - cloc` claimed them.
11486        //
11487        // expected: 6 rows — `when HTTP_REQUEST {`,
11488        // `    set msg "line one`, `line two`, `line three"`,
11489        // `    log local0. $msg`, `}` — all code, none blank. lloc 3 for
11490        // the handler, the `set` and the `log` command.
11491        check_metrics::<IrulesParser>(
11492            "when HTTP_REQUEST {\n    set msg \"line one\nline two\nline three\"\n    log local0. $msg\n}",
11493            "foo.irule",
11494            |metric| {
11495                assert_eq!(metric.loc.sloc(), 6);
11496                assert_eq!(metric.loc.ploc(), 6);
11497                assert_eq!(metric.loc.lloc(), 3);
11498                assert_eq!(metric.loc.cloc(), 0);
11499                assert_eq!(metric.loc.blank(), 0);
11500            },
11501        );
11502
11503        // expected: 3 rows whose middle row is empty *inside* the literal —
11504        // `set msg "one`, ``, `three"` — code, not blank.
11505        check_metrics::<IrulesParser>("set msg \"one\n\nthree\"", "foo.irule", |metric| {
11506            assert_eq!(metric.loc.sloc(), 3);
11507            assert_eq!(metric.loc.ploc(), 3);
11508            assert_eq!(metric.loc.cloc(), 0);
11509            assert_eq!(metric.loc.blank(), 0);
11510        });
11511    }
11512
11513    /// Interior blank lines are counted as BLANK and excluded from PLOC.
11514    /// sloc 6 (every line) / ploc 4 (4 code lines) / lloc 3 (handler + 2
11515    /// `set`s) / cloc 0 / blank 2.
11516    ///
11517    /// Also the dialect's half of the #1260 carve-out: the handler body is
11518    /// a `braced_word`, which the grammar parses as a script, so its blank
11519    /// rows stay blank — only `quoted_word` is routed to PLOC.
11520    #[cfg(feature = "irules")]
11521    #[test]
11522    fn irules_blank() {
11523        check_metrics::<IrulesParser>(
11524            "when X {\n\n    set x 1\n\n    set y 2\n}\n",
11525            "foo.irule",
11526            |metric| {
11527                assert_eq!(metric.loc.sloc(), 6);
11528                assert_eq!(metric.loc.ploc(), 4);
11529                assert_eq!(metric.loc.lloc(), 3);
11530                assert_eq!(metric.loc.cloc(), 0);
11531                assert_eq!(metric.loc.blank(), 2);
11532            },
11533        );
11534    }
11535
11536    /// A handler body with no blank lines reports zero BLANK. lloc 3 =
11537    /// handler + `set` + `log` command.
11538    #[cfg(feature = "irules")]
11539    #[test]
11540    fn irules_no_zero_blank() {
11541        check_metrics::<IrulesParser>(
11542            "when HTTP_REQUEST {\n    set x 1\n    log local0. $x\n}\n",
11543            "foo.irule",
11544            |metric| {
11545                assert_eq!(metric.loc.sloc(), 4);
11546                assert_eq!(metric.loc.ploc(), 4);
11547                assert_eq!(metric.loc.lloc(), 3);
11548                assert_eq!(metric.loc.blank(), 0);
11549            },
11550        );
11551    }
11552
11553    /// `#`-prefixed comment lines are counted as CLOC (iRules has no block
11554    /// comments, so each comment node spans exactly one line).
11555    ///
11556    /// expected: rows 0-2 are comment-only, row 3 is the sole code row.
11557    /// `ploc` and `sloc` are asserted, not just `cloc`/`blank`: without
11558    /// them this test passed all the way through #1135, which credited
11559    /// each of the three comment rows to PLOC as well (`ploc == 4`,
11560    /// `cloc + ploc == 7` against `sloc == 4`).
11561    #[cfg(feature = "irules")]
11562    #[test]
11563    fn irules_cloc() {
11564        check_metrics::<IrulesParser>(
11565            "# a\n# b\n# c\nwhen X { set x 1 }\n",
11566            "foo.irule",
11567            |metric| {
11568                assert_eq!(metric.loc.sloc(), 4);
11569                assert_eq!(metric.loc.ploc(), 1);
11570                assert_eq!(metric.loc.cloc(), 3);
11571                assert_eq!(metric.loc.blank(), 0);
11572            },
11573        );
11574    }
11575
11576    /// LLOC counts each statement once: handler header, `if`, `set`, and the
11577    /// generic `log` command = 4. The `switch_arm` headers are not counted
11578    /// (their bodies' commands are), verified in `irules_switch_lloc`.
11579    #[cfg(feature = "irules")]
11580    #[test]
11581    fn irules_lloc() {
11582        check_metrics::<IrulesParser>(
11583            "when X {\n    if { $a } {\n        set x 1\n    }\n    log local0. done\n}\n",
11584            "foo.irule",
11585            |metric| {
11586                assert_eq!(metric.loc.lloc(), 4);
11587            },
11588        );
11589    }
11590
11591    /// A command inside `[...]` (`command_substitution`) is a sub-expression,
11592    /// not a top-level statement, so it does not add to LLOC. Here lloc 2 =
11593    /// handler + `set`; the inner `expr` is NOT counted. Removing the
11594    /// `CommandSubstitution` guard would push lloc to 3 — this is the loc
11595    /// gating-decision regression test.
11596    #[cfg(feature = "irules")]
11597    #[test]
11598    fn irules_no_command_substitution_lloc() {
11599        check_metrics::<IrulesParser>(
11600            "when X {\n    set y [expr { 1 + 2 }]\n}\n",
11601            "foo.irule",
11602            |metric| {
11603                assert_eq!(metric.loc.lloc(), 2);
11604            },
11605        );
11606    }
11607
11608    /// `switch` counts once; each arm's *body* command counts, but the
11609    /// `switch_arm` pattern/body pair itself is not a logical line. lloc 4 =
11610    /// handler + `switch` + two `set`s (one per arm body).
11611    #[cfg(feature = "irules")]
11612    #[test]
11613    fn irules_switch_lloc() {
11614        check_metrics::<IrulesParser>(
11615            "when X {\n    switch $h {\n        a { set r 1 }\n        b { set r 2 }\n    }\n}\n",
11616            "foo.irule",
11617            |metric| {
11618                assert_eq!(metric.loc.lloc(), 4);
11619            },
11620        );
11621    }
11622
11623    /// A `proc` definition and its `return` command are each one logical
11624    /// line: lloc 2.
11625    #[cfg(feature = "irules")]
11626    #[test]
11627    fn irules_proc_lloc() {
11628        check_metrics::<IrulesParser>(
11629            "proc f { a } {\n    return $a\n}\n",
11630            "foo.irule",
11631            |metric| {
11632                assert_eq!(metric.loc.lloc(), 2);
11633            },
11634        );
11635    }
11636
11637    /// Objective-C blank-line accounting: two code lines separated by
11638    /// blank lines.
11639    #[cfg(feature = "objc")]
11640    #[test]
11641    fn objc_blank() {
11642        check_metrics::<ObjcParser>(
11643            "
11644
11645            int a = 42;
11646
11647            int b = 43;
11648
11649            ",
11650            "foo.m",
11651            |metric| {
11652                assert_eq!(metric.loc.blank(), 1);
11653                insta::assert_json_snapshot!(metric.loc, @r#"
11654                {
11655                  "sloc": 3,
11656                  "ploc": 2,
11657                  "lloc": 2,
11658                  "cloc": 0,
11659                  "blank": 1,
11660                  "sloc_average": 3.0,
11661                  "ploc_average": 2.0,
11662                  "lloc_average": 2.0,
11663                  "cloc_average": 0.0,
11664                  "blank_average": 1.0,
11665                  "sloc_min": 3,
11666                  "sloc_max": 3,
11667                  "cloc_min": 0,
11668                  "cloc_max": 0,
11669                  "ploc_min": 2,
11670                  "ploc_max": 2,
11671                  "lloc_min": 2,
11672                  "lloc_max": 2,
11673                  "blank_min": 1,
11674                  "blank_max": 1
11675                }
11676                "#);
11677            },
11678        );
11679    }
11680
11681    /// Objective-C comment accounting: a block comment and a line
11682    /// comment each contribute to `cloc`.
11683    #[cfg(feature = "objc")]
11684    #[test]
11685    fn objc_cloc() {
11686        check_metrics::<ObjcParser>(
11687            "/* Block comment
11688            still the block */
11689            // Line comment
11690            int a = 42; // trailing",
11691            "foo.m",
11692            |metric| {
11693                insta::assert_json_snapshot!(metric.loc, @r#"
11694                {
11695                  "sloc": 4,
11696                  "ploc": 1,
11697                  "lloc": 1,
11698                  "cloc": 4,
11699                  "blank": 0,
11700                  "sloc_average": 4.0,
11701                  "ploc_average": 1.0,
11702                  "lloc_average": 1.0,
11703                  "cloc_average": 4.0,
11704                  "blank_average": 0.0,
11705                  "sloc_min": 4,
11706                  "sloc_max": 4,
11707                  "cloc_min": 4,
11708                  "cloc_max": 4,
11709                  "ploc_min": 1,
11710                  "ploc_max": 1,
11711                  "lloc_min": 1,
11712                  "lloc_max": 1,
11713                  "blank_min": 0,
11714                  "blank_max": 0
11715                }
11716                "#);
11717            },
11718        );
11719    }
11720
11721    /// Objective-C logical-line accounting: a method whose body has three
11722    /// statements. The `method_definition` opens a function space but is
11723    /// not itself a logical line; each statement adds one.
11724    #[cfg(feature = "objc")]
11725    #[test]
11726    fn objc_lloc() {
11727        check_metrics::<ObjcParser>(
11728            "@implementation Foo
11729- (int)bar {
11730    int a = 1;
11731    int b = 2;
11732    return a + b;
11733}
11734@end
11735",
11736            "foo.m",
11737            |metric| {
11738                // expected: decl `int a` (1) + decl `int b` (1) +
11739                // `return` (1) = 3.
11740                assert_eq!(metric.loc.lloc(), 3);
11741                insta::assert_json_snapshot!(metric.loc, @r#"
11742                {
11743                  "sloc": 7,
11744                  "ploc": 7,
11745                  "lloc": 3,
11746                  "cloc": 0,
11747                  "blank": 0,
11748                  "sloc_average": 2.3333333333333335,
11749                  "ploc_average": 2.3333333333333335,
11750                  "lloc_average": 1.0,
11751                  "cloc_average": 0.0,
11752                  "blank_average": 0.0,
11753                  "sloc_min": 5,
11754                  "sloc_max": 7,
11755                  "cloc_min": 0,
11756                  "cloc_max": 0,
11757                  "ploc_min": 5,
11758                  "ploc_max": 7,
11759                  "lloc_min": 3,
11760                  "lloc_max": 3,
11761                  "blank_min": 0,
11762                  "blank_max": 0
11763                }
11764                "#);
11765            },
11766        );
11767    }
11768
11769    /// Objective-C for-header gating: the `int i = 0` declaration in a
11770    /// classic `for` init slot is part of the `for` statement's logical
11771    /// line and must NOT add a second one (mirrors the C / C++ gate).
11772    /// Reverting the `count_specific_ancestors` gate would push lloc from
11773    /// 2 to 3.
11774    #[cfg(feature = "objc")]
11775    #[test]
11776    fn objc_no_declaration_in_for_header_lloc() {
11777        check_metrics::<ObjcParser>(
11778            "@implementation Foo
11779- (void)bar {
11780    for (int i = 0; i < 10; ++i) {
11781        [self use:i];
11782    }
11783}
11784@end
11785",
11786            "foo.m",
11787            |metric| {
11788                // expected: for-statement (1) + body expression
11789                // `[self use:i]` (1) = 2. The header `int i = 0`
11790                // declaration is gated out; without the gate this is 3.
11791                assert_eq!(metric.loc.lloc(), 2);
11792                insta::assert_json_snapshot!(metric.loc, @r#"
11793                {
11794                  "sloc": 7,
11795                  "ploc": 7,
11796                  "lloc": 2,
11797                  "cloc": 0,
11798                  "blank": 0,
11799                  "sloc_average": 2.3333333333333335,
11800                  "ploc_average": 2.3333333333333335,
11801                  "lloc_average": 0.6666666666666666,
11802                  "cloc_average": 0.0,
11803                  "blank_average": 0.0,
11804                  "sloc_min": 5,
11805                  "sloc_max": 7,
11806                  "cloc_min": 0,
11807                  "cloc_max": 0,
11808                  "ploc_min": 5,
11809                  "ploc_max": 7,
11810                  "lloc_min": 2,
11811                  "lloc_max": 2,
11812                  "blank_min": 0,
11813                  "blank_max": 0
11814                }
11815                "#);
11816            },
11817        );
11818    }
11819
11820    #[cfg(feature = "objc")]
11821    #[test]
11822    fn objc_at_directives_lloc() {
11823        // The only ObjC-specific LLOC work the impl does beyond the C
11824        // inheritance: `@synchronized` is a dedicated `synchronized_statement`
11825        // node (counts as a logical line), but `@autoreleasepool` emits
11826        // only a keyword token with no wrapping node, so its *header* adds
11827        // nothing and only its inner statements count.
11828        check_metrics::<ObjcParser>(
11829            "@implementation Foo
11830- (void)bar {
11831    @synchronized (self) {
11832        [self use];
11833    }
11834    @autoreleasepool {
11835        [self use];
11836    }
11837}
11838@end
11839",
11840            "foo.m",
11841            |metric| {
11842                // expected: @synchronized statement (1) + its body
11843                // `[self use]` (1) + the @autoreleasepool body `[self use]`
11844                // (1) = 3. The `@autoreleasepool` header contributes no
11845                // logical line (it has no node); if it did, this would be 4.
11846                assert_eq!(metric.loc.lloc(), 3);
11847                insta::assert_json_snapshot!(metric.loc, @r#"
11848                {
11849                  "sloc": 10,
11850                  "ploc": 10,
11851                  "lloc": 3,
11852                  "cloc": 0,
11853                  "blank": 0,
11854                  "sloc_average": 3.3333333333333335,
11855                  "ploc_average": 3.3333333333333335,
11856                  "lloc_average": 1.0,
11857                  "cloc_average": 0.0,
11858                  "blank_average": 0.0,
11859                  "sloc_min": 8,
11860                  "sloc_max": 10,
11861                  "cloc_min": 0,
11862                  "cloc_max": 0,
11863                  "ploc_min": 8,
11864                  "ploc_max": 10,
11865                  "lloc_min": 3,
11866                  "lloc_max": 3,
11867                  "blank_min": 0,
11868                  "blank_max": 0
11869                }
11870                "#);
11871            },
11872        );
11873    }
11874
11875    /// Analyses `source` byte-for-byte as Rust.
11876    ///
11877    /// Goes through `metrics_verbatim` rather than `check_metrics`
11878    /// because the #1051 cases end at EOF; see that helper for why.
11879    #[cfg(feature = "rust")]
11880    fn rust_loc(source: &[u8]) -> Stats {
11881        metrics_verbatim(crate::LANG::Rust, source, crate::MetricsOptions::default()).loc
11882    }
11883
11884    /// #1051: a Rust doc comment ending at EOF has no trailing newline for
11885    /// the scanner to consume, so its `LineComment` node ends on its own
11886    /// start row and discounting a row underflowed. On row 0 that panicked
11887    /// (debug: the subtraction; release: a hash-table capacity overflow in
11888    /// `add_only_comment_lines`). On any later row release did not crash —
11889    /// it silently reported one `cloc` too few.
11890    #[cfg(feature = "rust")]
11891    #[test]
11892    fn rust_doc_comment_at_eof_does_not_underflow() {
11893        // `end == start == 0` — underflowed at the subtraction itself.
11894        // expected: the sole row is one comment-only line, no code.
11895        let outer = rust_loc(b"/// x");
11896        assert_eq!(outer.cloc(), 1);
11897        assert_eq!(outer.ploc(), 0);
11898
11899        let inner = rust_loc(b"//! x");
11900        assert_eq!(inner.cloc(), 1);
11901        assert_eq!(inner.ploc(), 0);
11902
11903        // `end == start > 0` — the subtraction succeeded but drove `end`
11904        // below `start`. Release silently counted `cloc == 0` here.
11905        // expected: row 0 is code, row 1 is comment-only.
11906        let after_code = rust_loc(b"fn f(){}\n/// x");
11907        assert_eq!(after_code.cloc(), 1);
11908        assert_eq!(after_code.ploc(), 1);
11909
11910        // Two doc comments, the second at EOF: both rows are comment-only.
11911        // Pre-fix release reported 1 here, not 2.
11912        assert_eq!(rust_loc(b"/// a\n/// b").cloc(), 2);
11913
11914        // A doc comment sharing its row with code. `let` is not valid at
11915        // file scope, but tree-sitter parses it as a clean `let_declaration`
11916        // + `line_comment`, which is the only way to reach the
11917        // comment-after-code branch (no *valid* Rust puts `///` after code).
11918        let trailing = rust_loc(b"let x = 1; /// d");
11919        assert_eq!(trailing.cloc(), 1);
11920        assert_eq!(trailing.ploc(), 1);
11921    }
11922
11923    /// A doc comment at EOF must count exactly like a plain line comment at
11924    /// EOF. The `DocComment` adjustment exists only to discount the newline
11925    /// the scanner consumes; at EOF there is none to discount, so the two
11926    /// shapes are indistinguishable for LOC purposes.
11927    #[cfg(feature = "rust")]
11928    #[test]
11929    fn rust_doc_comment_at_eof_matches_plain_comment() {
11930        let plain = rust_loc(b"// x");
11931        // Pin the baseline absolutely too: parity alone would still hold if
11932        // both sides moved together, and would then read as a regression
11933        // when the un-newline-terminated `sloc` accounting is corrected.
11934        assert_eq!(plain.cloc(), 1);
11935        assert_eq!(plain.ploc(), 0);
11936
11937        for doc in [&b"/// x"[..], &b"//! x"[..]] {
11938            let doc = rust_loc(doc);
11939            assert_eq!(doc.cloc(), plain.cloc());
11940            assert_eq!(doc.ploc(), plain.ploc());
11941            assert_eq!(doc.sloc(), plain.sloc());
11942            assert_eq!(doc.blank(), plain.blank());
11943        }
11944    }
11945
11946    /// The newline-terminated path must stay unchanged by the #1051 guard.
11947    /// A `DocComment` node really does span one row more than it renders
11948    /// whenever the scanner consumed a newline, and that row must still be
11949    /// excluded — otherwise the guard would silently become a no-op and
11950    /// inflate CLOC for every doc-commented Rust file.
11951    #[cfg(feature = "rust")]
11952    #[test]
11953    fn rust_doc_comment_with_trailing_newline_still_discounts_the_row() {
11954        // expected: one rendered comment row, not two.
11955        assert_eq!(rust_loc(b"/// x\n").cloc(), 1);
11956        // expected: two consecutive doc comments are two rows, not four.
11957        assert_eq!(rust_loc(b"/// a\n/// b\n").cloc(), 2);
11958
11959        // The common real-world shape: doc comment attached to an item.
11960        // expected: row 0 comment-only, row 1 code.
11961        let documented = rust_loc(b"/// doc\nfn f() {}\n");
11962        assert_eq!(documented.cloc(), 1);
11963        assert_eq!(documented.ploc(), 1);
11964    }
11965
11966    /// CRLF is the boundary the guard must *not* fire on. `\r` is ordinary
11967    /// content to `process_line_doc_content`, so it consumes the following
11968    /// `\n` and the node does span an extra row — the discount is still
11969    /// owed. A lone trailing `\r` at EOF is the opposite case. Without this,
11970    /// a future grammar bump that stops consuming the newline would leave
11971    /// every LF test passing while the discount silently became dead code.
11972    #[cfg(feature = "rust")]
11973    #[test]
11974    fn rust_doc_comment_crlf_still_discounts_the_row() {
11975        // Newline consumed despite the `\r`: discount applies.
11976        assert_eq!(rust_loc(b"/// x\r\n").cloc(), 1);
11977        // expected: row 0 code, row 1 comment-only.
11978        let after_code = rust_loc(b"fn f(){}\r\n/// x");
11979        assert_eq!(after_code.cloc(), 1);
11980        assert_eq!(after_code.ploc(), 1);
11981        // Lone `\r`, then EOF: no newline consumed, so no discount is owed.
11982        assert_eq!(rust_loc(b"/// x\r").cloc(), 1);
11983    }
11984
11985    /// One-line, deliberately un-newline-terminated source, one entry per
11986    /// language whose `Loc` implementation is not a documented no-op.
11987    ///
11988    /// `sloc` is computed once, in the shared [`Sloc`], from the unit
11989    /// span the grammar hands us — so the only per-language variable is
11990    /// where each grammar puts the root node's end position. #1067 was
11991    /// possible precisely because that position was assumed rather than
11992    /// read, so the sweep is exhaustive rather than a sample: a future
11993    /// grammar whose root does *not* run to end-of-input has to show up
11994    /// here rather than silently lose a row.
11995    ///
11996    /// `Preproc` and `Ccomment` are excluded, but not because they are
11997    /// exempt from the rule. Their `Loc` impls are no-ops
11998    /// (`implement_metric_trait!(Loc, PreprocCode, CcommentCode)`, #188),
11999    /// so the *node-accumulated* sub-metrics are 0 by design — yet
12000    /// `sloc` is not node-accumulated: the walker anchors every Unit's
12001    /// row span at finalization, their synthetic Unit root included, so
12002    /// they carry a real span and drift with #1067 exactly as the
12003    /// languages below do. What they cannot join is the second sweep, whose final
12004    /// `mi != 0` assertion is unreachable with `ploc == 0`. They get
12005    /// their own check in
12006    /// `no_op_loc_grammars_still_count_their_unterminated_row`.
12007    #[cfg(any(
12008        feature = "bash",
12009        feature = "c",
12010        feature = "c-family-helpers",
12011        feature = "cpp",
12012        feature = "csharp",
12013        feature = "elixir",
12014        feature = "go",
12015        feature = "groovy",
12016        feature = "irules",
12017        feature = "java",
12018        feature = "javascript",
12019        feature = "kotlin",
12020        feature = "lua",
12021        feature = "mozcpp",
12022        feature = "mozjs",
12023        feature = "objc",
12024        feature = "perl",
12025        feature = "php",
12026        feature = "python",
12027        feature = "ruby",
12028        feature = "rust",
12029        feature = "tcl",
12030        feature = "typescript",
12031    ))]
12032    const UNTERMINATED_ONE_LINERS: &[(crate::LANG, &[u8])] = &[
12033        (crate::LANG::Rust, b"fn main() {}"),
12034        (crate::LANG::C, b"int main(void) { return 0; }"),
12035        (crate::LANG::Cpp, b"int main() { return 0; }"),
12036        (crate::LANG::Mozcpp, b"int main() { return 0; }"),
12037        (crate::LANG::Objc, b"int main(void) { return 0; }"),
12038        (crate::LANG::Csharp, b"class C { void M() {} }"),
12039        (crate::LANG::Java, b"class C { void m() {} }"),
12040        (crate::LANG::Kotlin, b"fun main() {}"),
12041        (crate::LANG::Groovy, b"def f() {}"),
12042        (crate::LANG::Go, b"package main"),
12043        (crate::LANG::Javascript, b"function f() {}"),
12044        (crate::LANG::Mozjs, b"function f() {}"),
12045        (crate::LANG::Typescript, b"function f(): void {}"),
12046        (crate::LANG::Tsx, b"function f() {}"),
12047        (crate::LANG::Python, b"def f(): pass"),
12048        (crate::LANG::Ruby, b"def f; end"),
12049        (crate::LANG::Php, b"<?php function f() {}"),
12050        (crate::LANG::Perl, b"sub f { return 1; }"),
12051        (crate::LANG::Bash, b"f() { echo hi; }"),
12052        (crate::LANG::Lua, b"function f() end"),
12053        (crate::LANG::Tcl, b"proc f {} {}"),
12054        (crate::LANG::Irules, b"proc f {} {}"),
12055        (crate::LANG::Elixir, b"defmodule M do end"),
12056    ];
12057
12058    /// #1067: `Sloc::sloc()` derived the unit's row count as `end - start`,
12059    /// which is only right when a trailing newline pushes the root node's
12060    /// end onto a phantom extra row. Source that stops mid-line — anything
12061    /// not newline-terminated — lost its final row, so a one-line file
12062    /// reported `sloc == 0`.
12063    ///
12064    /// Uses [`metrics_verbatim`], not `check_metrics`: the latter trims and
12065    /// re-appends a trailing newline, which makes this entire input class
12066    /// unreachable and the test vacuous (the same blind spot that hid
12067    /// #1051).
12068    #[cfg(all(
12069        feature = "bash",
12070        feature = "c",
12071        feature = "cpp",
12072        feature = "csharp",
12073        feature = "elixir",
12074        feature = "go",
12075        feature = "groovy",
12076        feature = "irules",
12077        feature = "java",
12078        feature = "javascript",
12079        feature = "kotlin",
12080        feature = "lua",
12081        feature = "mozcpp",
12082        feature = "mozjs",
12083        feature = "objc",
12084        feature = "perl",
12085        feature = "php",
12086        feature = "python",
12087        feature = "ruby",
12088        feature = "rust",
12089        feature = "tcl",
12090        feature = "typescript",
12091    ))]
12092    #[test]
12093    fn unterminated_one_line_file_reports_one_source_line() {
12094        for (lang, source) in UNTERMINATED_ONE_LINERS {
12095            let text = String::from_utf8_lossy(source);
12096            let loc = metrics_verbatim(*lang, source, MetricsOptions::default()).loc;
12097            assert_eq!(loc.sloc(), 1, "{lang:?} sloc for {text:?}");
12098            // The largest space can never be bigger than the file itself.
12099            assert_eq!(loc.sloc_max(), 1, "{lang:?} sloc_max for {text:?}");
12100            // The invariant documented on `Stats::with_cloc_sloc`: every
12101            // physical line is code, comment, both, or blank, so the code
12102            // and comment-only tallies cannot together exceed the row
12103            // count. `sloc == 0` broke it for any unterminated file whose
12104            // last line carried content.
12105            assert!(
12106                loc.cloc() + loc.ploc() <= loc.sloc(),
12107                "{lang:?}: cloc {} + ploc {} exceeds sloc {} for {text:?}",
12108                loc.cloc(),
12109                loc.ploc(),
12110                loc.sloc(),
12111            );
12112        }
12113    }
12114
12115    /// The two grammars whose `Loc` impl is the macro's no-op still get a
12116    /// `sloc`, so they drift with #1067 like everything else.
12117    ///
12118    /// Their root is not a `SpaceKind::Unit`, so `metrics_inner` pushes a
12119    /// synthetic Unit, whose row span `anchor_unit_sloc_span` fills in at
12120    /// finalization — a span the no-op `compute` never touches but
12121    /// `Sloc::sloc()` still measures.
12122    /// Before #1067 an unterminated one-liner measured `0` rows here too.
12123    /// Kept apart from [`UNTERMINATED_ONE_LINERS`] only because the
12124    /// `mi != 0` half of the sweep below cannot hold with `ploc == 0`.
12125    #[cfg(any(
12126        feature = "c",
12127        feature = "c-family-helpers",
12128        feature = "cpp",
12129        feature = "mozcpp"
12130    ))]
12131    #[test]
12132    fn no_op_loc_grammars_still_count_their_unterminated_row() {
12133        for (lang, source) in [
12134            (crate::LANG::Preproc, &b"#define A 1"[..]),
12135            (crate::LANG::Ccomment, &b"/* c */"[..]),
12136        ] {
12137            let bare = metrics_verbatim(lang, source, MetricsOptions::default()).loc;
12138            assert_eq!(bare.sloc(), 1, "{lang:?} unterminated sloc");
12139            let mut terminated = source.to_vec();
12140            terminated.push(b'\n');
12141            let terminated = metrics_verbatim(lang, &terminated, MetricsOptions::default()).loc;
12142            assert_eq!(
12143                bare.sloc(),
12144                terminated.sloc(),
12145                "{lang:?} sloc must not depend on the trailing newline"
12146            );
12147            // The node-accumulated sub-metrics are the ones #188 zeroes.
12148            assert_eq!((bare.ploc(), bare.cloc(), bare.lloc()), (0, 0, 0));
12149        }
12150    }
12151
12152    /// A backslash-continued `#define` body is one `PreprocArg` node
12153    /// spanning every continuation row, so each of those rows is PLOC.
12154    ///
12155    /// The four C-family `Loc` impls carry an identical arm for this
12156    /// (`tree-sitter-cpp` does not expand macros — see the comment at
12157    /// each site), and until #1229 only C++'s copy was exercised: the
12158    /// other three were the sole uncovered lines in that PR. They are
12159    /// deliberate clones, so a fixture for one is a fixture for all
12160    /// four, and `Mozcpp` in particular owns no file extension and can
12161    /// only be reached by naming the language.
12162    ///
12163    /// Measured, and confirmed discriminating by deleting the arm from
12164    /// all four modules: `ploc` is 4 with it and 3 without, the lost row
12165    /// being the macro's last continuation line. `sloc` is 5 (three
12166    /// macro rows, one blank, one `main`) and `lloc` is 1 — the single
12167    /// `return` statement — since a `#define` declares no statement.
12168    ///
12169    /// This fixture pins how *many* rows the arm credits, not where it
12170    /// stops: its body closes on `(b))`, so the node's end column is
12171    /// above 0 and the raw end row and `Node::end_line() - 1` name the
12172    /// same last row. The bound is pinned separately by
12173    /// [`a_dangling_macro_continuation_does_not_credit_the_row_below`].
12174    #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12175    #[test]
12176    fn a_continued_macro_body_counts_every_row_it_spans() {
12177        // Rows: 0-2 are the macro, 3 is blank, 4 is `main`.
12178        const CONTINUED_MACRO: &[u8] =
12179            b"#define SUM(a, b) \\\n    ((a) + \\\n     (b))\n\nint main(void) { return SUM(1, 2); }\n";
12180
12181        // One row per language, each gated on its own feature: an
12182        // ungated table constructs a parser for a language the build
12183        // disabled and panics `LanguageDisabled`, which reads as a
12184        // defect in whatever was being changed rather than as a
12185        // configuration that cannot run this test (#1446).
12186        const LANGS: &[crate::LANG] = &[
12187            #[cfg(feature = "c")]
12188            crate::LANG::C,
12189            #[cfg(feature = "cpp")]
12190            crate::LANG::Cpp,
12191            #[cfg(feature = "mozcpp")]
12192            crate::LANG::Mozcpp,
12193            #[cfg(feature = "objc")]
12194            crate::LANG::Objc,
12195        ];
12196        crate::test_support::assert_fixtures_present(LANGS);
12197
12198        for &lang in LANGS {
12199            let loc = metrics_verbatim(lang, CONTINUED_MACRO, MetricsOptions::default()).loc;
12200            assert_eq!(
12201                loc.ploc(),
12202                4,
12203                "{lang:?}: every continuation row of the macro body is code"
12204            );
12205            assert_eq!(loc.sloc(), 5, "{lang:?} sloc");
12206            assert_eq!(loc.lloc(), 1, "{lang:?} lloc");
12207            assert_eq!(loc.cloc(), 0, "{lang:?} cloc");
12208            assert_eq!(loc.blank(), 1, "{lang:?} blank");
12209        }
12210    }
12211
12212    /// Where the `PreprocArg` arm stops: a macro body whose last
12213    /// continuation is a *dangling* backslash produces a node ending at
12214    /// column 0 of the row below, and that row is not part of the span.
12215    ///
12216    /// `Node::end_line` is what encodes the rule, so the arm derives its
12217    /// last row from `add_string_interior_ploc` rather than from the raw
12218    /// `end_row()`, which over-reads by one on exactly this shape
12219    /// (#1423). Every other multi-row PLOC path already went through the
12220    /// helper; these four arms were the last open-coded copies.
12221    ///
12222    /// Measured with `bca dump`, not read off the source text: the
12223    /// `preproc_arg` runs from row 0 column 10 to row 2 column 0. Rows
12224    /// are 0 `#define`, 1 the continuation, 2 the blank row the trailing
12225    /// backslash runs into, 3 `int x;`. So `ploc` is {0, 1, 3} = 3 and
12226    /// `blank` is 1. The raw end row credited row 2 as well, reporting
12227    /// `ploc 4, blank 0`.
12228    ///
12229    /// Two parts of the fixture are load-bearing rather than scenery:
12230    ///
12231    /// - **`int x;` after the macro.** `clamp_line_sets_to_span` already
12232    ///   drops a PLOC row past the space's own last row, so a fixture
12233    ///   ending on the macro reports the corrected numbers under either
12234    ///   spelling and discriminates nothing.
12235    /// - **The `    2 \` continuation row.** Nothing but the arm's
12236    ///   interior insert credits row 1 — `preproc_def` contributes only
12237    ///   its own start row — so `ploc == 3` fails if that row is ever
12238    ///   trimmed out of the fixture, per the fixture-decay rule in
12239    ///   `.claude/rules/testing.md`. The `blank == 1` half has no such
12240    ///   anchor available: deleting the dangling backslash also stops
12241    ///   row 2 being credited, so a revert of the bound is the only
12242    ///   evidence that half discriminates, and it was run.
12243    #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12244    #[test]
12245    fn a_dangling_macro_continuation_does_not_credit_the_row_below() {
12246        // Rows: 0-1 are the macro, 2 is the blank row its trailing
12247        // backslash runs into, 3 is `int x;`.
12248        const DANGLING_CONTINUATION: &[u8] = b"#define A 1 \\\n    2 \\\n\nint x;\n";
12249
12250        // Gated per row for the reason
12251        // `a_continued_macro_body_counts_every_row_it_spans` gives.
12252        const LANGS: &[crate::LANG] = &[
12253            #[cfg(feature = "c")]
12254            crate::LANG::C,
12255            #[cfg(feature = "cpp")]
12256            crate::LANG::Cpp,
12257            #[cfg(feature = "mozcpp")]
12258            crate::LANG::Mozcpp,
12259            #[cfg(feature = "objc")]
12260            crate::LANG::Objc,
12261        ];
12262        crate::test_support::assert_fixtures_present(LANGS);
12263
12264        for &lang in LANGS {
12265            let loc = metrics_verbatim(lang, DANGLING_CONTINUATION, MetricsOptions::default()).loc;
12266            assert_eq!(
12267                loc.ploc(),
12268                3,
12269                "{lang:?}: the row a dangling continuation ends *at* is not code"
12270            );
12271            assert_eq!(
12272                loc.blank(),
12273                1,
12274                "{lang:?}: that row is blank, not part of the macro body"
12275            );
12276            assert_eq!(loc.sloc(), 4, "{lang:?} sloc");
12277            assert_eq!(loc.lloc(), 1, "{lang:?} lloc");
12278            assert_eq!(loc.cloc(), 0, "{lang:?} cloc");
12279        }
12280    }
12281
12282    /// Whether the last line ends in a newline is a formatting detail, not
12283    /// a property of the code — no LOC sub-metric, and therefore no MI
12284    /// value, may depend on it. This is the invariant #1067 violated, and
12285    /// it pins the fix from both sides: the newline-terminated path (the
12286    /// one every in-tree harness exercises) must not move either.
12287    ///
12288    /// **The invariant is now unconditional.** It used to be scoped to
12289    /// source containing a token: whitespace-only input collapsed most
12290    /// grammars' roots to a zero-width node at end-of-input, leaving
12291    /// `sloc` no span to measure, so `b"  "` reported one row and
12292    /// `b"  \n"` reported none (#1087). #1247 anchored the unit's `sloc`
12293    /// span to the span the unit reports, which removed that dependence
12294    /// on where the root node happens to start. The whitespace-only class
12295    /// is swept separately — it cannot ride this test, whose closing
12296    /// `assert_ne!` requires a non-zero MI — in
12297    /// [`whitespace_only_input_is_uniform_across_grammars`].
12298    #[cfg(all(
12299        feature = "bash",
12300        feature = "c",
12301        feature = "cpp",
12302        feature = "csharp",
12303        feature = "elixir",
12304        feature = "go",
12305        feature = "groovy",
12306        feature = "irules",
12307        feature = "java",
12308        feature = "javascript",
12309        feature = "kotlin",
12310        feature = "lua",
12311        feature = "mozcpp",
12312        feature = "mozjs",
12313        feature = "objc",
12314        feature = "perl",
12315        feature = "php",
12316        feature = "python",
12317        feature = "ruby",
12318        feature = "rust",
12319        feature = "tcl",
12320        feature = "typescript",
12321    ))]
12322    #[test]
12323    fn trailing_newline_does_not_change_loc_or_mi() {
12324        for (lang, source) in UNTERMINATED_ONE_LINERS {
12325            let text = String::from_utf8_lossy(source);
12326            let bare = metrics_verbatim(*lang, source, MetricsOptions::default());
12327            let mut newline_terminated = source.to_vec();
12328            newline_terminated.push(b'\n');
12329            let terminated =
12330                metrics_verbatim(*lang, &newline_terminated, MetricsOptions::default());
12331
12332            assert_eq!(
12333                bare.loc.sloc(),
12334                terminated.loc.sloc(),
12335                "{lang:?} sloc {text:?}"
12336            );
12337            assert_eq!(
12338                bare.loc.ploc(),
12339                terminated.loc.ploc(),
12340                "{lang:?} ploc {text:?}"
12341            );
12342            assert_eq!(
12343                bare.loc.cloc(),
12344                terminated.loc.cloc(),
12345                "{lang:?} cloc {text:?}"
12346            );
12347            assert_eq!(
12348                bare.loc.lloc(),
12349                terminated.loc.lloc(),
12350                "{lang:?} lloc {text:?}"
12351            );
12352            assert_eq!(
12353                bare.loc.blank(),
12354                terminated.loc.blank(),
12355                "{lang:?} blank {text:?}"
12356            );
12357            // The MI knock-on: `mi::inputs_are_empty` short-circuits to
12358            // 0.0 on `sloc <= 0`, so before the fix every unterminated
12359            // one-liner reported MI 0 while its newline-terminated twin
12360            // reported a real score. Inputs are now identical, so the
12361            // three formulas agree bit-for-bit.
12362            assert_eq!(
12363                bare.mi.original(),
12364                terminated.mi.original(),
12365                "{lang:?} mi {text:?}"
12366            );
12367            assert_eq!(
12368                bare.mi.sei(),
12369                terminated.mi.sei(),
12370                "{lang:?} mi.sei {text:?}"
12371            );
12372            assert_eq!(
12373                bare.mi.visual_studio(),
12374                terminated.mi.visual_studio(),
12375                "{lang:?} mi.visual_studio {text:?}",
12376            );
12377            assert_ne!(bare.mi.original(), 0.0, "{lang:?} mi must not be zeroed");
12378        }
12379    }
12380
12381    /// The second #1067 symptom: `b"fn f(){}\n/// x"` reported `sloc == 1`
12382    /// with `ploc == 1` *and* `cloc == 1`, so `cloc + ploc > sloc`. The
12383    /// file has two rows; only the missing one made the sums disagree.
12384    #[cfg(feature = "rust")]
12385    #[test]
12386    fn unterminated_trailing_comment_upholds_the_cloc_ploc_invariant() {
12387        // expected: row 0 is code, row 1 is comment-only, nothing blank.
12388        let loc = rust_loc(b"fn f(){}\n/// x");
12389        assert_eq!(loc.sloc(), 2);
12390        assert_eq!(loc.ploc(), 1);
12391        assert_eq!(loc.cloc(), 1);
12392        assert_eq!(loc.blank(), 0);
12393    }
12394
12395    /// Degenerate inputs, pinned so the end-column rule in
12396    /// `Node::end_line` cannot drift into fabricating rows for files
12397    /// that have none.
12398    #[cfg(feature = "rust")]
12399    #[test]
12400    fn degenerate_inputs_report_their_real_row_count() {
12401        // No bytes, no rows.
12402        assert_eq!(rust_loc(b"").sloc(), 0);
12403        // One row of whitespace, unterminated: the root node is empty but
12404        // sits at column 3, so the row is real and counts as blank.
12405        let spaces = rust_loc(b"   ");
12406        assert_eq!(spaces.sloc(), 1);
12407        assert_eq!(spaces.blank(), 1);
12408        // Newline-terminated whitespace was the #1087 carve-out: most
12409        // grammars collapse the root to a zero-width node at end-of-input
12410        // (`(1, 0)..(1, 0)` for `"\n"`), so the measured span had no rows
12411        // left to attribute and `sloc` was 0 for a file that plainly has
12412        // one. #1247 retired that: the unit's `sloc` span is now anchored
12413        // to the span the unit *reports*, which #1195 already anchored at
12414        // line 1, so a collapsed root no longer costs the file its rows.
12415        let one = rust_loc(b"\n");
12416        assert_eq!((one.sloc(), one.ploc(), one.blank()), (1, 0, 1));
12417        let two = rust_loc(b"\n\n");
12418        assert_eq!((two.sloc(), two.ploc(), two.blank()), (2, 0, 2));
12419    }
12420
12421    /// Every grammar whose `Loc` behaviour this module owns, including the
12422    /// two whose `compute` is the `implement_metric_trait!` no-op.
12423    ///
12424    /// [`UNTERMINATED_ONE_LINERS`] carries a fixture per language because
12425    /// its sweeps need parseable code; the whitespace-only sweep needs
12426    /// only the language, and must not omit `Preproc`/`Ccomment` — their
12427    /// synthetic Unit root is anchored like any other, so they carry a
12428    /// real span and answer the #1087/#1247 question too.
12429    #[cfg(any(
12430        feature = "bash",
12431        feature = "c",
12432        feature = "c-family-helpers",
12433        feature = "cpp",
12434        feature = "csharp",
12435        feature = "elixir",
12436        feature = "go",
12437        feature = "groovy",
12438        feature = "irules",
12439        feature = "java",
12440        feature = "javascript",
12441        feature = "kotlin",
12442        feature = "lua",
12443        feature = "mozcpp",
12444        feature = "mozjs",
12445        feature = "objc",
12446        feature = "perl",
12447        feature = "php",
12448        feature = "python",
12449        feature = "ruby",
12450        feature = "rust",
12451        feature = "tcl",
12452        feature = "typescript",
12453    ))]
12454    fn all_loc_grammars() -> impl Iterator<Item = crate::LANG> {
12455        UNTERMINATED_ONE_LINERS
12456            .iter()
12457            .map(|(lang, _)| *lang)
12458            .chain([crate::LANG::Preproc, crate::LANG::Ccomment])
12459    }
12460
12461    /// #1087 accepted whitespace-only source as the one input class where
12462    /// a trailing newline moved `sloc` — twenty grammars collapse the root
12463    /// to a zero-width node at end-of-input, so `"  "` reported one row
12464    /// and `"  \n"` reported none, while five grammars (Elixir, Tcl,
12465    /// iRules, Preproc, Ccomment) kept the span and were newline-
12466    /// independent already. This sweep was written to pin both halves.
12467    ///
12468    /// #1247 removed the premise. The carve-out was a consequence of
12469    /// measuring the unit's `sloc` span from the root node's first token;
12470    /// once that span is anchored to the one the unit *reports* (#1195
12471    /// anchored the reported span at line 1), a collapsed root no longer
12472    /// erases the file's rows and every grammar answers alike. The sweep
12473    /// stays, with the split list retired: it now pins the *absence* of a
12474    /// per-grammar difference, which is the property a future grammar bump
12475    /// or walker change could still break.
12476    ///
12477    /// The unterminated side is unchanged and was always uniform: every
12478    /// grammar reports the row, as one blank line.
12479    #[cfg(all(
12480        feature = "bash",
12481        feature = "c",
12482        any(
12483            feature = "c",
12484            feature = "c-family-helpers",
12485            feature = "cpp",
12486            feature = "mozcpp"
12487        ),
12488        feature = "cpp",
12489        feature = "csharp",
12490        feature = "elixir",
12491        feature = "go",
12492        feature = "groovy",
12493        feature = "irules",
12494        feature = "java",
12495        feature = "javascript",
12496        feature = "kotlin",
12497        feature = "lua",
12498        feature = "mozcpp",
12499        feature = "mozjs",
12500        feature = "objc",
12501        feature = "perl",
12502        feature = "php",
12503        feature = "python",
12504        feature = "ruby",
12505        feature = "rust",
12506        feature = "tcl",
12507        feature = "typescript",
12508    ))]
12509    #[test]
12510    fn whitespace_only_input_is_uniform_across_grammars() {
12511        // Spaces and tabs both, so a grammar that lexes one as extra and
12512        // the other as an error token cannot hide behind the sweep.
12513        for bare in [&b"  "[..], b"\t\t"] {
12514            let mut newline_terminated = bare.to_vec();
12515            newline_terminated.push(b'\n');
12516            for lang in all_loc_grammars() {
12517                let text = String::from_utf8_lossy(bare);
12518                let unterminated = metrics_verbatim(lang, bare, MetricsOptions::default()).loc;
12519                let terminated =
12520                    metrics_verbatim(lang, &newline_terminated, MetricsOptions::default()).loc;
12521
12522                assert_eq!(
12523                    (unterminated.sloc(), unterminated.blank()),
12524                    (1, 1),
12525                    "{lang:?}: unterminated {text:?} is one blank row for every grammar"
12526                );
12527
12528                assert_eq!(
12529                    (terminated.sloc(), terminated.blank()),
12530                    (1, 1),
12531                    "{lang:?}: newline-terminated {text:?} is the same one blank \
12532                     row — whether the grammar collapses its root at \
12533                     end-of-input is no longer observable in loc (#1247)"
12534                );
12535
12536                // Whitespace is never code and never a comment, whichever
12537                // side of the carve-out the grammar sits on. This is the
12538                // assertion #1135 broke for Tcl and iRules, whose row
12539                // terminator used to land in their PLOC catch-all.
12540                for loc in [&unterminated, &terminated] {
12541                    assert_eq!(
12542                        (loc.ploc(), loc.cloc(), loc.lloc()),
12543                        (0, 0, 0),
12544                        "{lang:?}: whitespace is neither code nor comment"
12545                    );
12546                }
12547            }
12548        }
12549    }
12550
12551    /// #1247: the unit anchors its *reported* span at line 1 (#1195) but
12552    /// measured its `sloc` span from the root node's first token, so blank
12553    /// rows above that token counted in neither `sloc` nor `blank` — while
12554    /// byte-identical rows one line lower counted in both.
12555    ///
12556    /// Swept across four grammars because the per-language `Loc` impls
12557    /// mirror each other. Tcl is in the list because it was already
12558    /// *right*: its row terminator is a token child of the root, so the
12559    /// root already started at row 0 and Tcl reported `sloc 4` for the
12560    /// Rust fixture's `sloc 1`. A per-language fix would have had to know
12561    /// which grammars were which; the walker-level anchor does not, and
12562    /// Tcl is the case that catches one being applied twice.
12563    ///
12564    /// `space_verbatim`, not `check_metrics`: the shim trims leading and
12565    /// trailing newlines, which deletes this test's entire subject.
12566    #[cfg(all(feature = "c", feature = "python", feature = "rust", feature = "tcl"))]
12567    #[test]
12568    fn leading_blank_rows_count_in_the_units_sloc_and_blank() {
12569        #[cfg(any(feature = "c", feature = "python", feature = "rust", feature = "tcl"))]
12570        const LEADING_BLANKS: u64 = 3;
12571        for (lang, body, ploc) in [
12572            (crate::LANG::Rust, &b"fn a() {}\n"[..], 1),
12573            (crate::LANG::Python, b"def a():\n    pass\n", 2),
12574            (crate::LANG::C, b"int f() { return 0; }\n", 1),
12575            (crate::LANG::Tcl, b"puts hi\n", 1),
12576        ] {
12577            let mut source = vec![b'\n'; LEADING_BLANKS as usize];
12578            source.extend_from_slice(body);
12579            let space = space_verbatim(lang, &source, MetricsOptions::default());
12580            let loc = &space.metrics.loc;
12581            assert_eq!(
12582                (loc.sloc(), loc.ploc(), loc.cloc(), loc.blank()),
12583                (LEADING_BLANKS + ploc, ploc, 0, LEADING_BLANKS),
12584                "{lang:?}: the leading rows are blank, not absent"
12585            );
12586            // The disagreement the issue is named for: the unit's own
12587            // reported span and its `sloc` are two spellings of one
12588            // number, and were not before.
12589            assert_eq!(
12590                loc.sloc() as usize,
12591                space.end_line - space.start_line + 1,
12592                "{lang:?}: sloc equals the rows of the unit's reported span"
12593            );
12594        }
12595    }
12596
12597    /// The two controls from #1247's evidence table. Both were already
12598    /// correct, and both are how the inconsistency was visible at all: a
12599    /// comment on line 1 flipped a byte-identical file from `sloc 1` to
12600    /// `sloc 4`, because comments are in the tree and blank rows are not.
12601    /// A fix that reached past the unit would move one of these.
12602    #[cfg(feature = "rust")]
12603    #[test]
12604    fn interior_blanks_and_leading_comments_are_unmoved_by_the_anchor() {
12605        // expected: rows 1 and 3 are code, row 2 is blank.
12606        let interior = rust_loc(b"fn a() {}\n\nfn b() {}\n");
12607        assert_eq!(
12608            (
12609                interior.sloc(),
12610                interior.ploc(),
12611                interior.cloc(),
12612                interior.blank()
12613            ),
12614            (3, 2, 0, 1)
12615        );
12616        // expected: row 1 is comment-only, rows 2-3 blank, row 4 code.
12617        let leading_comment = rust_loc(b"// c\n\n\nfn a() {}\n");
12618        assert_eq!(
12619            (
12620                leading_comment.sloc(),
12621                leading_comment.ploc(),
12622                leading_comment.cloc(),
12623                leading_comment.blank()
12624            ),
12625            (4, 1, 1, 2)
12626        );
12627    }
12628
12629    /// The anchor is gated on `SpaceKind::Unit`, and that gate is the
12630    /// entire separation between "the file starts at line 1" and "every
12631    /// space starts at line 1". Asserted on the nested space's `sloc` as
12632    /// well as its span, because only the `sloc` half is new.
12633    #[cfg(feature = "rust")]
12634    #[test]
12635    fn the_unit_anchor_does_not_reach_nested_spaces() {
12636        let space = space_verbatim(
12637            crate::LANG::Rust,
12638            b"\n\n\nfn a() {\n    let x = 1;\n}\n",
12639            MetricsOptions::default(),
12640        );
12641        assert_eq!((space.start_line, space.end_line), (1, 6));
12642        assert_eq!(space.metrics.loc.sloc(), 6, "the file has six rows");
12643        let nested = &space.spaces[0];
12644        assert_eq!((nested.start_line, nested.end_line), (4, 6));
12645        assert_eq!(
12646            nested.metrics.loc.sloc(),
12647            3,
12648            "the function's own three rows, not the file's six"
12649        );
12650        assert_eq!(nested.metrics.loc.blank(), 0, "the function has no blanks");
12651    }
12652
12653    /// `Sloc::exclude_span` records each pruned subtree's rows and
12654    /// `sloc()` subtracts their cardinality from the enclosing span, so
12655    /// widening that span at the top could in principle desynchronise
12656    /// the two. It cannot: the rows the anchor adds are above the first
12657    /// token, and no pruned subtree can overlap them. Pinned rather than
12658    /// argued, since the failure mode is a silent `saturating_sub` clamp
12659    /// to 0 rather than a panic (#722, #1247, #1417).
12660    #[cfg(feature = "rust")]
12661    #[test]
12662    fn exclude_tests_pruning_composes_with_the_unit_anchor() {
12663        // Rows 1-3 blank, 4 `fn a`, 5 blank, 6 `#[test]`, 7-9 `fn t`.
12664        let source = b"\n\n\nfn a() {}\n\n#[test]\nfn t() {\n    assert!(true);\n}\n";
12665        let kept = metrics_verbatim(
12666            crate::LANG::Rust,
12667            source,
12668            MetricsOptions::default().with_exclude_tests(true),
12669        )
12670        .loc;
12671        // The prune takes the `fn t` item's rows 7-9 and, since #1431,
12672        // the `#[test]` sibling on row 6 that marks it. What the anchor
12673        // decides is the other end: `blank` is 4 rather than 1, because
12674        // rows 1-3 are now inside the span the pruning subtracts from.
12675        assert_eq!(
12676            (kept.sloc(), kept.ploc(), kept.cloc(), kept.blank()),
12677            (5, 1, 0, 4),
12678            "the four pruned rows leave; the three leading blanks stay"
12679        );
12680
12681        let unpruned = rust_loc(source);
12682        assert_eq!(
12683            (unpruned.sloc(), unpruned.ploc(), unpruned.blank()),
12684            (9, 5, 4),
12685            "the same file unpruned — the anchor is what makes both blank counts 4"
12686        );
12687    }
12688
12689    /// Analyses `source` as Rust with `exclude_tests` on, byte-for-byte.
12690    /// `check_metrics` cannot express this: it trims and re-appends the
12691    /// trailing newline (destroying the row structure these fixtures are
12692    /// about) and its macro hard-binds `MetricsOptions`.
12693    ///
12694    /// Gated to match its three callers below, so a build without the
12695    /// Rust grammar has neither the tests nor an unused helper (#1446).
12696    #[cfg(feature = "rust")]
12697    fn rust_loc_pruned(source: &[u8]) -> Stats {
12698        metrics_verbatim(
12699            crate::LANG::Rust,
12700            source,
12701            crate::MetricsOptions::default().with_exclude_tests(true),
12702        )
12703        .loc
12704    }
12705
12706    /// #1417: `Sloc::exclude_span` recorded each pruned subtree's whole
12707    /// span as a *count*, so a row a retained sibling also occupies was
12708    /// subtracted anyway and `sloc` fell below `ploc`. Both one-liner
12709    /// spellings from the issue reported `sloc 0, ploc 1` — one row of
12710    /// code in a zero-row file.
12711    ///
12712    /// The pruned and unpruned readings are identical here, and that is
12713    /// the clearest statement of why: the whole row survives the prune,
12714    /// because `fn a()` is on it. `sloc 1` for a file that is more than
12715    /// half test code reads as under-subtraction and is not — nothing
12716    /// on that row can be removed without removing `fn a()` with it.
12717    ///
12718    /// A second, separate cause used to add a row here and is worth not
12719    /// confusing with this one: `should_skip_subtree` matched only the
12720    /// *item*, and a `#[cfg(test)]` / `#[test]` attribute is an
12721    /// `AttributeItem` sibling of it, so the attribute was walked
12722    /// normally and Rust's catch-all credited its start row to PLOC — a
12723    /// file that was 100% test code still reported `ploc 1` (#1431, now
12724    /// fixed). These fixtures put the attribute on the shared row, so
12725    /// that fix changes neither reading: what holds row 0 here is
12726    /// `fn a()`, not the attribute.
12727    #[cfg(feature = "rust")]
12728    #[test]
12729    fn a_pruned_item_sharing_a_row_with_retained_code_keeps_the_row() {
12730        for source in [
12731            &b"fn a() {} #[cfg(test)] mod t { #[test] fn x() {} }\n"[..],
12732            &b"#[cfg(test)] mod t { #[test] fn x() {} } fn a() {}\n"[..],
12733        ] {
12734            let text = String::from_utf8_lossy(source);
12735            let pruned = rust_loc_pruned(source);
12736            assert_eq!(
12737                (pruned.sloc(), pruned.ploc(), pruned.cloc(), pruned.blank()),
12738                (1, 1, 0, 0),
12739                "pruned {text:?}"
12740            );
12741
12742            let kept = rust_loc(source);
12743            assert_eq!(
12744                (kept.sloc(), kept.ploc(), kept.cloc(), kept.blank()),
12745                (1, 1, 0, 0),
12746                "unpruned {text:?} — the prune can remove no row here"
12747            );
12748        }
12749    }
12750
12751    /// The discriminator against the alternative fix #1417 rejected:
12752    /// clamping the excluded *count* to `span - |retained rows|`.
12753    ///
12754    /// On `shared_row` the truth is `sloc 3, ploc 2, blank 1`; before
12755    /// the fix it read `sloc 2, blank 0`; and the clamp reads
12756    /// `min(1, 3 - 2) = 1`, so `sloc 2, blank 0` — no improvement at
12757    /// all. It buys `ploc <= sloc` by moving the error into `blank`,
12758    /// which is the outcome #1398's rationale warns against. Measured
12759    /// against the built binary before the set-based fix landed, not
12760    /// reasoned about. Without this fixture the fix is indistinguishable
12761    /// from that alternative.
12762    ///
12763    /// `own_rows` is the control, in the same test because the two must
12764    /// not be confusable: it genuinely excludes rows — both of the
12765    /// pruned item's own, since #1431 took the attribute with it — where
12766    /// `shared_row` excludes none, so a fix cannot buy the invariant by
12767    /// under-subtracting in the ordinary rustfmt layout.
12768    ///
12769    /// **`shared_row` has no fixture-decay anchor, and none exists.**
12770    /// The rule in `.claude/rules/testing.md` asks for a second axis
12771    /// only the excluded construct contributes to, so that trimming it
12772    /// out of the fixture fails this test rather than quietly hollowing
12773    /// it. Deleting `#[cfg(test)] mod t {}` from row 0 leaves all four
12774    /// numbers at `(3, 2, 0, 1)` — measured, 0 of 3,390 lib tests fail
12775    /// — and that is not an oversight but the claim restated: a pruned
12776    /// item whose only row a retained sibling also occupies is *by
12777    /// definition* metrically indistinguishable from not being there.
12778    /// Every candidate axis collapses the same way. The unpruned
12779    /// reading is also `(3, 2, 0, 1)`, because `mod t {}` adds no row
12780    /// of its own; the space tree matches too, because the prune
12781    /// removes the space the construct would have opened. The coverage
12782    /// here is the production perturbation, which was run: this test is
12783    /// among the failures for dropping the `ploc` subtraction, for
12784    /// widening `exclude_span`'s bound by one, and for both `subtract`
12785    /// mutations. `own_rows` is anchored in the ordinary way — trim its
12786    /// pruned `mod` and `sloc 2` fails.
12787    #[cfg(feature = "rust")]
12788    #[test]
12789    fn a_blank_row_does_not_absorb_a_phantom_exclusion() {
12790        // Row 0 `fn a` and the pruned `mod t`, row 1 blank, row 2 `fn b`.
12791        let shared_row = rust_loc_pruned(b"fn a() {} #[cfg(test)] mod t {}\n\nfn b() {}\n");
12792        assert_eq!(
12793            (
12794                shared_row.sloc(),
12795                shared_row.ploc(),
12796                shared_row.cloc(),
12797                shared_row.blank()
12798            ),
12799            (3, 2, 0, 1),
12800            "nothing is excluded: row 0 carries `fn a`"
12801        );
12802
12803        // Row 0 `fn a`, row 1 blank, row 2 `#[cfg(test)]`, row 3 the
12804        // pruned `mod t` — rows 2 and 3 are both the prune's to take,
12805        // the attribute along with the item it marks (#1431).
12806        let own_rows = rust_loc_pruned(b"fn a() {}\n\n#[cfg(test)]\nmod t { #[test] fn x() {} }\n");
12807        assert_eq!(
12808            (
12809                own_rows.sloc(),
12810                own_rows.ploc(),
12811                own_rows.cloc(),
12812                own_rows.blank()
12813            ),
12814            (2, 1, 0, 1),
12815            "two of four rows excluded; only `fn a` and the blank stay"
12816        );
12817    }
12818
12819    /// The *comment* half of the settle, which the fixtures above cannot
12820    /// reach because every one of them has `cloc 0`.
12821    ///
12822    /// `Stats::settle_excluded_rows` subtracts three sets from the
12823    /// pruned rows — `ploc.lines` and both `Cloc` sets — and only the
12824    /// first was covered: deleting **both** comment subtractions failed
12825    /// 0 of 3,388 lib tests, while deleting the `ploc` one failed 3. A
12826    /// comment sharing a row with a pruned item is retained text on that
12827    /// row exactly as code would be, so the row must survive the prune.
12828    #[cfg(feature = "rust")]
12829    #[test]
12830    fn a_comment_sharing_a_pruned_items_row_keeps_that_row() {
12831        // Row 0 `#[cfg(test)]`, row 1 the pruned `mod t` and a trailing
12832        // comment. Both rows are pruned — the attribute since #1431 —
12833        // but row 1 holds retained text, so only row 0 is the prune's to
12834        // take. `sloc 1` against `sloc 0` is exactly the comment
12835        // subtraction: without it both rows go and a file with a
12836        // surviving comment reports no lines at all.
12837        let loc = rust_loc_pruned(b"#[cfg(test)]\nmod t {} // note\n");
12838        assert_eq!(loc.sloc(), 1, "the comment row survives the prune");
12839        assert_eq!(loc.cloc(), 1, "the trailing comment is still counted");
12840        assert_eq!(loc.ploc(), 0, "no production code is left on either row");
12841        assert!(
12842            loc.ploc() <= loc.sloc(),
12843            "ploc {} must not exceed sloc {}",
12844            loc.ploc(),
12845            loc.sloc()
12846        );
12847    }
12848
12849    /// The non-unit half of the same off-by-one. tree-sitter-perl's
12850    /// `function_definition` swallows the newline after the closing brace
12851    /// of a file's **last** `sub`, so that node's span ends at column 0 of
12852    /// a row it does not occupy. The old unconditional `+ 1` credited that
12853    /// row, inflating the last sub of every Perl file by one line — here,
12854    /// reporting a 3-row `sub` as 4.
12855    #[cfg(feature = "perl")]
12856    #[test]
12857    fn perl_last_sub_does_not_absorb_the_trailing_newline() {
12858        // Two identical 3-row subs; only the second hits the quirk.
12859        let space = space_verbatim(
12860            crate::LANG::Perl,
12861            b"sub f {\n    return 1;\n}\nsub g {\n    return 2;\n}\n",
12862            MetricsOptions::default(),
12863        );
12864        assert_eq!(space.metrics.loc.sloc(), 6, "the file has six rows");
12865        let subs: Vec<u64> = space
12866            .spaces
12867            .iter()
12868            .map(|child| child.metrics.loc.sloc())
12869            .collect();
12870        assert_eq!(subs, vec![3, 3], "both subs occupy three rows");
12871    }
12872
12873    /// #1398, whose mechanism [`Stats::clamp_line_sets_to_span`]
12874    /// documents: a recovery token starting one row past end-of-input
12875    /// reached a catch-all's leaf branch and became a line of code, so
12876    /// `ploc` came out one above `sloc`.
12877    ///
12878    /// The assertion is exact equality with the *unchanged* `sloc`, in
12879    /// both directions. `ploc <= sloc` alone would also be satisfied by
12880    /// a clamp wide enough to delete real code rows, and every fixture
12881    /// here is all-code, so `ploc == sloc` is the only passing value.
12882    ///
12883    /// Each row additionally asserts the tree still holds a node past
12884    /// the file's last row. Without that anchor every fixture decays
12885    /// into an ordinary all-code file the moment someone edits the
12886    /// source string, and `ploc == sloc` is then true of any
12887    /// well-formed input — measured: replacing all fifteen fixtures
12888    /// with well-formed sources of the same row count leaves this test
12889    /// green. The phantom node *is* the subject, so it is the axis to
12890    /// anchor on (`.claude/rules/testing.md`, "Perturb the fixture as
12891    /// well as the production line").
12892    ///
12893    /// Writing that anchor as `has_error()` — the obvious choice — is
12894    /// wrong, and finding out why is worth the paragraph: Ruby's
12895    /// `x = <<~DOC\na\n` parses with **no error node at all**, and
12896    /// still gets a zero-width `heredoc_end` at `(3, 1)` of a two-row
12897    /// file. A grammar need not report failure to hand back a span it
12898    /// cannot honour, which is half of why this defect survived.
12899    ///
12900    /// `metrics_verbatim`, not `check_metrics`, for a duller reason
12901    /// than #1051's: a `LANG`-keyed table cannot dispatch the generic
12902    /// `check_metrics::<T>` shim, and that shim's `fn` callback returns
12903    /// nothing. Every fixture here is already a fixed point of the
12904    /// trailing-newline normalisation, so — measured — a
12905    /// `check_metrics` twin of any of these rows fails against unfixed
12906    /// code too. This is *not* the EOF class both harnesses hide.
12907    #[cfg(any(
12908        feature = "bash",
12909        feature = "c",
12910        feature = "cpp",
12911        feature = "elixir",
12912        feature = "groovy",
12913        feature = "lua",
12914        feature = "mozcpp",
12915        feature = "objc",
12916        feature = "perl",
12917        feature = "ruby",
12918    ))]
12919    #[test]
12920    fn a_recovery_token_past_the_span_is_not_a_code_row() {
12921        // Every fixture that reproduced the defect, one row per
12922        // language, as `(language, source, rows the file has, ploc
12923        // before #1398)`. That last column is what makes this a
12924        // regression test rather than a restatement of the fixed
12925        // behaviour: a reviewer can see which row the assertion is
12926        // about, and it is one above `sloc` in every case.
12927        //
12928        // Four unrelated recovery shapes are represented, which is the
12929        // argument for one clamp over ten per-arm skips: a heredoc
12930        // whose terminator never arrives (Bash, Perl, Ruby), an
12931        // unterminated string delimiter (Elixir, Lua, Groovy), an
12932        // unclosed bracket (Ruby's `%w[`), and a dangling line
12933        // continuation (the C family, where the `\` splices a row that
12934        // is not there).
12935        const FIXTURES: &[(crate::LANG, &[u8], u64, u64)] = &[
12936            #[cfg(feature = "bash")]
12937            (crate::LANG::Bash, b"cat <<DOC\na\n", 2, 3),
12938            #[cfg(feature = "c")]
12939            (crate::LANG::C, b"int x = \\\n", 1, 2),
12940            #[cfg(feature = "cpp")]
12941            (crate::LANG::Cpp, b"int x = \\\n", 1, 2),
12942            #[cfg(feature = "mozcpp")]
12943            (crate::LANG::Mozcpp, b"int x = \\\n", 1, 2),
12944            #[cfg(feature = "objc")]
12945            (crate::LANG::Objc, b"int x = \\\n", 1, 2),
12946            // The issue's reproduction spells this `x = "abc\n\n\n`;
12947            // the trailing blank rows never reach the parser, because
12948            // `normalize_line_endings` collapses a run of trailing
12949            // newlines to one before any file-based caller sees it.
12950            #[cfg(feature = "elixir")]
12951            (crate::LANG::Elixir, b"x = \"abc\n", 1, 2),
12952            #[cfg(feature = "elixir")]
12953            (crate::LANG::Elixir, b"x = \"\"\"\nabc\n", 2, 3),
12954            #[cfg(feature = "elixir")]
12955            (crate::LANG::Elixir, b"x = ~s(\n", 1, 2),
12956            // Not a truncation, and the only row here reachable from a
12957            // file nobody cut short: Elixir comments with `#`, so a
12958            // C-style block comment is a whole-line parse failure. The
12959            // shape reaches the same phantom row, which is the point —
12960            // "malformed" is not a synonym for "truncated".
12961            #[cfg(feature = "elixir")]
12962            (crate::LANG::Elixir, b"/* block */\n", 1, 2),
12963            #[cfg(feature = "groovy")]
12964            (crate::LANG::Groovy, b"def x = \"\"\"abc\n", 1, 2),
12965            #[cfg(feature = "lua")]
12966            (crate::LANG::Lua, b"x = \"abc\n", 1, 2),
12967            #[cfg(feature = "lua")]
12968            (crate::LANG::Lua, b"x = 'abc\n", 1, 2),
12969            #[cfg(feature = "perl")]
12970            (crate::LANG::Perl, b"my $x = <<DOC;\na\n", 2, 3),
12971            #[cfg(feature = "ruby")]
12972            (crate::LANG::Ruby, b"x = <<~DOC\na\n", 2, 3),
12973            #[cfg(feature = "ruby")]
12974            (crate::LANG::Ruby, b"x = %w[\n", 1, 2),
12975        ];
12976
12977        crate::test_support::assert_fixtures_present(FIXTURES);
12978        for &(lang, source, rows, ploc_before_1398) in FIXTURES {
12979            let text = String::from_utf8_lossy(source);
12980            // The anchor: the tree still contains a node starting past
12981            // the file's last row. That node *is* this test's subject,
12982            // so an edit that repaired the fixture — or a grammar bump
12983            // that stopped emitting the phantom — fails here rather
12984            // than leaving the metric assertions true for an unrelated
12985            // reason.
12986            //
12987            // Not `has_error()`, which would be the obvious anchor and
12988            // is wrong: `x = <<~DOC\na\n` parses **cleanly** in Ruby,
12989            // phantom `heredoc_end` at `(3, 1)` and all. A clean parse
12990            // is not evidence of an in-span tree, which is half of why
12991            // this defect went unnoticed.
12992            let ast = crate::Ast::parse(crate::Source::new(lang, source))
12993                .expect("an enabled language parses, error tree or not");
12994            let mut stack = vec![ast.root_node()];
12995            let mut phantom = None;
12996            while let Some(node) = stack.pop() {
12997                if node.start_row() as u64 >= rows {
12998                    phantom = Some(node.start_row());
12999                    break;
13000                }
13001                stack.extend(node.children());
13002            }
13003            assert!(
13004                phantom.is_some(),
13005                "{lang:?}: {text:?} no longer produces a node past row {rows}, \
13006                 so it no longer exercises #1398"
13007            );
13008
13009            let loc = metrics_verbatim(lang, source, MetricsOptions::default()).loc;
13010            assert_eq!(loc.sloc(), rows, "{lang:?}: {text:?} occupies {rows} rows");
13011            assert_eq!(
13012                loc.ploc(),
13013                rows,
13014                "{lang:?}: every row of {text:?} is code and no row past it is \
13015                 (was {ploc_before_1398} before #1398)"
13016            );
13017            // One-sided, unlike the two above: every fixture is all
13018            // code, so this cannot catch a clamp that over-reaches into
13019            // the comment sets — an over-reach can only leave 0 at 0.
13020            // What it does pin is the other direction, a future change
13021            // that files the phantom row as a *comment* instead. The
13022            // CLOC half of the clamp is covered by
13023            // `an_unterminated_pod_block_does_not_comment_a_row_past_eof`,
13024            // which is its only guard.
13025            assert_eq!((loc.cloc(), loc.blank()), (0, 0), "{lang:?}: {text:?}");
13026        }
13027    }
13028
13029    /// The `span == 0` arm of [`Stats::clamp_line_sets_to_span`] clears
13030    /// both line sets, so its precondition — that no walk arrives here
13031    /// holding a row — is the thing worth pinning. Nothing else can:
13032    /// the two assertions at the end of that function compare against
13033    /// `span`, which the clear forces to `0 <= 0` whatever it destroyed.
13034    ///
13035    /// This test used to assert the clearing itself, seeding row 0
13036    /// because it is the one row a `(0, 0)` spelling would wrongly keep.
13037    /// That is now covered a layer down by
13038    /// `line_set::tests::retain_range_inverted_empties_the_set`, which
13039    /// seeds `[0, 1, 400]` and asserts row 0 specifically is gone — so
13040    /// the seed here is free to become what it always described: the
13041    /// state the guard exists to reject.
13042    ///
13043    /// A fixture cannot reach this branch with a populated set at all —
13044    /// `span == 0` needs a space opened on a genuinely zero-width node
13045    /// at column 0, and every `is_func_space` matches compound
13046    /// productions — so seeding `Stats` directly is the only shape that
13047    /// can (`.claude/rules/testing.md`, "Pair any end-to-end test with a
13048    /// direct unit test on the function whose contract is verified").
13049    #[test]
13050    #[cfg(debug_assertions)]
13051    #[should_panic(expected = "zero-span space reached the clamp holding")]
13052    fn a_zero_span_holding_rows_trips_the_guard() {
13053        let mut stats = Stats::default();
13054        // The empty file's span: `0..0`, covering no row at all.
13055        stats.init_unit_span(0, 0);
13056        stats.ploc.lines.insert(0);
13057        assert_eq!((stats.sloc(), stats.ploc()), (0, 1));
13058
13059        stats.clamp_line_sets_to_span();
13060    }
13061
13062    /// The companion to the guard above: an *empty* zero-span space is
13063    /// the state every real walk arrives in, and it must pass through
13064    /// the clamp untouched rather than trip the assertion.
13065    #[test]
13066    fn a_zero_span_without_rows_clamps_cleanly() {
13067        let mut stats = Stats::default();
13068        stats.init_unit_span(0, 0);
13069
13070        stats.clamp_line_sets_to_span();
13071
13072        assert_eq!(
13073            (stats.sloc(), stats.ploc(), stats.cloc()),
13074            (0, 0, 0),
13075            "a span of no rows retains no row"
13076        );
13077    }
13078
13079    /// The phantom row lands on whichever space was open when the
13080    /// recovery token was visited, so the contract has to hold per
13081    /// space and not only on the file. Elixir's unterminated `do` block
13082    /// is the case that separates the two: before #1398 the `defmodule`
13083    /// space reported `ploc 2` against its own `sloc 1`, and so did the
13084    /// unit, and a unit-only clamp would have left the nested space
13085    /// wrong while the file read as fixed.
13086    ///
13087    /// `space_verbatim`, not `metrics_verbatim`: the claim is about the
13088    /// nested space, whose numbers the root aggregate unions away.
13089    #[cfg(feature = "elixir")]
13090    #[test]
13091    fn the_clamp_reaches_a_nested_space_not_only_the_unit() {
13092        let space = space_verbatim(
13093            crate::LANG::Elixir,
13094            b"defmodule M do\n",
13095            MetricsOptions::default(),
13096        );
13097        assert_eq!(
13098            (space.metrics.loc.sloc(), space.metrics.loc.ploc()),
13099            (1, 1),
13100            "the unit is one row, all code"
13101        );
13102        let module = space
13103            .spaces
13104            .first()
13105            .expect("the `defmodule` opens a space even unterminated");
13106        assert_eq!(module.name.as_deref(), Some("M"));
13107        assert_eq!(
13108            (module.metrics.loc.sloc(), module.metrics.loc.ploc()),
13109            (1, 1),
13110            "the module's own space is one row too — `ploc` was 2 before #1398"
13111        );
13112    }
13113
13114    /// The same phantom row reaches CLOC, which has its own contract:
13115    /// `cloc > sloc` pushes MI's comments percentage above 100%, the
13116    /// defect #461 fixed for co-located comments and left open for a
13117    /// comment node ending past end-of-input. An unterminated Perl POD
13118    /// block is the reproduction the #1398 sweep turned up — two rows,
13119    /// `cloc 3`.
13120    ///
13121    /// Pinned alongside the PLOC sweep because the clamp covers all
13122    /// three line sets in one pass; without this row the CLOC half of
13123    /// `Stats::clamp_line_sets_to_span` has no test.
13124    #[cfg(feature = "perl")]
13125    #[test]
13126    fn an_unterminated_pod_block_does_not_comment_a_row_past_eof() {
13127        let loc =
13128            metrics_verbatim(crate::LANG::Perl, b"=pod\nabc\n", MetricsOptions::default()).loc;
13129        // expected: both rows are POD, so both are comment-only and
13130        // neither is code or blank. `cloc` was 3 before #1398.
13131        assert_eq!((loc.sloc(), loc.cloc()), (2, 2));
13132        assert_eq!((loc.ploc(), loc.blank()), (0, 0));
13133    }
13134
13135    /// #1135: Tcl and its iRules dialect are the only grammars here that
13136    /// surface the row terminator as a token child of the root. `LF`'s
13137    /// start row is the row it *terminates*, so the `_` catch-all in
13138    /// their `Loc` impls inserted that row into PLOC — turning every
13139    /// comment-only and whitespace-only row into a line of code.
13140    ///
13141    /// A wholly empty row never showed the defect: the `LF` that starts
13142    /// on it is the one tree-sitter collapses at end-of-input. A row of
13143    /// *whitespace* does, and trailing whitespace on an otherwise blank
13144    /// line is ordinary in real source — which is what makes the two
13145    /// spellings' disagreement the sharpest assertion here.
13146    ///
13147    /// Uses [`metrics_verbatim`] so the fixtures reach the parser
13148    /// byte-for-byte; `check_metrics` rewrites the trailing newline.
13149    #[cfg(all(feature = "irules", feature = "tcl"))]
13150    #[test]
13151    fn tcl_family_does_not_count_terminator_rows_as_code() {
13152        for lang in [crate::LANG::Tcl, crate::LANG::Irules] {
13153            // Three rows: code, whitespace-only, code.
13154            let padded = metrics_verbatim(
13155                lang,
13156                b"proc f {} {}\n   \nproc g {} {}\n",
13157                MetricsOptions::default(),
13158            )
13159            .loc;
13160            assert_eq!(padded.sloc(), 3, "{lang:?} sloc");
13161            assert_eq!(padded.ploc(), 2, "{lang:?} ploc");
13162            assert_eq!(padded.blank(), 1, "{lang:?} blank");
13163
13164            // Whether a blank row carries spaces is not a property of the
13165            // code, so the empty-row spelling must agree exactly. Pre-fix
13166            // this side stayed correct while the padded side reported
13167            // `ploc 3 / blank 0`.
13168            let empty = metrics_verbatim(
13169                lang,
13170                b"proc f {} {}\n\nproc g {} {}\n",
13171                MetricsOptions::default(),
13172            )
13173            .loc;
13174            assert_eq!(
13175                (padded.sloc(), padded.ploc(), padded.blank()),
13176                (empty.sloc(), empty.ploc(), empty.blank()),
13177                "{lang:?}: trailing whitespace on a blank row must not make it code"
13178            );
13179
13180            // The comment-only half of the same defect. It escaped
13181            // `unterminated_one_line_file_reports_one_source_line`
13182            // because that sweep's fixtures are one-liners with no
13183            // comment row. The cross-language version of this case is
13184            // `a_comment_row_is_never_counted_as_code`; the two rows here
13185            // stay so the Tcl-family regression reads in one place.
13186            let commented = metrics_verbatim(
13187                lang,
13188                b"# lead-in\nproc f {} {}\n",
13189                MetricsOptions::default(),
13190            )
13191            .loc;
13192            assert_eq!(commented.sloc(), 2, "{lang:?} commented sloc");
13193            assert_eq!(commented.ploc(), 1, "{lang:?} commented ploc");
13194            assert_eq!(commented.cloc(), 1, "{lang:?} commented cloc");
13195            assert_eq!(commented.blank(), 0, "{lang:?} commented blank");
13196        }
13197    }
13198
13199    /// The comment spellings to sweep for each language: the line form
13200    /// every language has, then the block and doc forms where one exists.
13201    ///
13202    /// Nothing here needs to be exhaustive per language — the defect this
13203    /// guards against is a stray *token* inside (or terminating) a
13204    /// comment node reaching a PLOC catch-all, which any one spelling of
13205    /// a comment exposes. The block and doc entries are there because
13206    /// those nodes have child tokens the line form does not.
13207    #[cfg(any(
13208        feature = "bash",
13209        feature = "c",
13210        feature = "cpp",
13211        feature = "csharp",
13212        feature = "elixir",
13213        feature = "go",
13214        feature = "groovy",
13215        feature = "irules",
13216        feature = "java",
13217        feature = "javascript",
13218        feature = "kotlin",
13219        feature = "lua",
13220        feature = "mozcpp",
13221        feature = "mozjs",
13222        feature = "objc",
13223        feature = "perl",
13224        feature = "php",
13225        feature = "python",
13226        feature = "ruby",
13227        feature = "rust",
13228        feature = "tcl",
13229        feature = "typescript",
13230    ))]
13231    fn comment_spellings(lang: crate::LANG) -> &'static [&'static str] {
13232        use crate::LANG::*;
13233        match lang {
13234            Python | Ruby | Bash | Elixir | Tcl | Irules | Perl => &["# c"],
13235            Lua => &["-- c", "--[[ c ]]"],
13236            Php => &["# c", "/* c */", "/** c */"],
13237            Rust => &["// c", "/* c */", "/// c"],
13238            _ => &["// c", "/* c */"],
13239        }
13240    }
13241
13242    /// A comment-only row is never a physical line of code — in any
13243    /// language, in any comment spelling, on either side of the code.
13244    ///
13245    /// Two separate defects broke this, both by letting a token reach the
13246    /// `_` catch-all that ends `stats.ploc.lines.insert(start)`. In Tcl
13247    /// and iRules it was the row terminator, whose start row is the row
13248    /// it terminates (#1135). In Perl it was the `#` *inside* the
13249    /// `comments` node, which additionally tripped
13250    /// `check_comment_ends_on_code_line` into reclassifying the row from
13251    /// comment-only to code-and-comment (#1137).
13252    ///
13253    /// Neither was visible to the per-language `*_cloc` tests — several
13254    /// assert `cloc` and `blank` and leave `ploc` unpinned — nor to
13255    /// `unterminated_one_line_file_reports_one_source_line`, whose
13256    /// fixtures carry no comment row at all. Since the failure mode is
13257    /// structural rather than language-specific, the sweep is per
13258    /// language rather than a sample.
13259    #[cfg(all(
13260        feature = "bash",
13261        feature = "c",
13262        feature = "cpp",
13263        feature = "csharp",
13264        feature = "elixir",
13265        feature = "go",
13266        feature = "groovy",
13267        feature = "irules",
13268        feature = "java",
13269        feature = "javascript",
13270        feature = "kotlin",
13271        feature = "lua",
13272        feature = "mozcpp",
13273        feature = "mozjs",
13274        feature = "objc",
13275        feature = "perl",
13276        feature = "php",
13277        feature = "python",
13278        feature = "ruby",
13279        feature = "rust",
13280        feature = "tcl",
13281        feature = "typescript",
13282    ))]
13283    #[test]
13284    fn a_comment_row_is_never_counted_as_code() {
13285        for (lang, code) in UNTERMINATED_ONE_LINERS {
13286            // PHP is the one language whose code fixture must open the
13287            // file: outside `<?php` every row is inline HTML, so a
13288            // comment placed before it is not a comment at all.
13289            let must_lead = *lang == crate::LANG::Php;
13290            for &comment in comment_spellings(*lang) {
13291                let orders: &[bool] = if must_lead { &[false] } else { &[true, false] };
13292                for &comment_first in orders {
13293                    let mut src = Vec::new();
13294                    let (first, second): (&[u8], &[u8]) = if comment_first {
13295                        (comment.as_bytes(), code)
13296                    } else {
13297                        (code, comment.as_bytes())
13298                    };
13299                    src.extend_from_slice(first);
13300                    src.push(b'\n');
13301                    src.extend_from_slice(second);
13302                    src.push(b'\n');
13303
13304                    let loc = metrics_verbatim(*lang, &src, MetricsOptions::default()).loc;
13305                    let text = String::from_utf8_lossy(&src);
13306                    assert_eq!(loc.sloc(), 2, "{lang:?} sloc for {text:?}");
13307                    assert_eq!(
13308                        loc.ploc(),
13309                        1,
13310                        "{lang:?} ploc for {text:?} — the comment row is not code"
13311                    );
13312                    assert_eq!(loc.cloc(), 1, "{lang:?} cloc for {text:?}");
13313                    assert_eq!(loc.blank(), 0, "{lang:?} blank for {text:?}");
13314                }
13315            }
13316        }
13317    }
13318
13319    /// A `var` / `let` / `const` declaration is an executable statement and
13320    /// counts one LLOC, the same as Java's `LocalVariableDeclaration` and
13321    /// Rust's `let` (#1283 — before the fix a declarations-only file
13322    /// reported `lloc 0`). The fourth row is one `variable_declaration`
13323    /// carrying two declarators, so it counts once, not twice. The fifth row
13324    /// is a `using_declaration` — the grammar's third executable declaration
13325    /// kind, which TypeScript and TSX do not have.
13326    #[cfg(feature = "javascript")]
13327    #[test]
13328    fn javascript_declaration_lloc() {
13329        check_metrics::<JavascriptParser>(
13330            "var a = 1;\nlet b = 2;\nconst c = 3;\nvar d = 4, e = 5;\nusing r = open();\n",
13331            "foo.js",
13332            |metric| {
13333                assert_eq!(metric.loc.sloc(), 5);
13334                assert_eq!(metric.loc.ploc(), 5);
13335                assert_eq!(metric.loc.lloc(), 5);
13336                assert_eq!(metric.loc.cloc(), 0);
13337                assert_eq!(metric.loc.blank(), 0);
13338            },
13339        );
13340    }
13341
13342    /// The classic `for (var i = 0; …)` header is part of the `ForStatement`,
13343    /// which already counts its own LLOC, so the header declaration must not
13344    /// add a second one. `for (const x of …)` and `for (var k in …)` need no
13345    /// carve-out at all: the grammar inlines the `const` / `var` keyword into
13346    /// `for_in_statement` and emits no declaration node — they are here so a
13347    /// carve-out wrongly widened to `ForInStatement` still has an input that
13348    /// notices. `var s = i;` in the loop *body* is a real logical line: the
13349    /// `StatementBlock` stops the ancestor walk (#1283).
13350    ///
13351    /// expected: for-statement 1 + body declaration 1 + for-of 1 + for-in 1 = 4
13352    #[cfg(feature = "javascript")]
13353    #[test]
13354    fn javascript_for_header_declaration_not_double_counted() {
13355        check_metrics::<JavascriptParser>(
13356            "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",
13357            "foo.js",
13358            |metric| {
13359                assert_eq!(metric.loc.sloc(), 7);
13360                assert_eq!(metric.loc.ploc(), 7);
13361                assert_eq!(metric.loc.lloc(), 4);
13362                assert_eq!(metric.loc.cloc(), 0);
13363                assert_eq!(metric.loc.blank(), 0);
13364            },
13365        );
13366    }
13367
13368    /// `export const a = 1;` is one logical line, not two: the declaration
13369    /// nests inside the `ExportStatement`, whose arm already counted the row.
13370    /// The declaration inside the exported function body still counts — the
13371    /// `StatementBlock` stops the ancestor walk before the `ExportStatement`
13372    /// is reached (#1283).
13373    ///
13374    /// expected: 4 export statements + the `const c = 4;` in `f`'s body = 5
13375    #[cfg(feature = "javascript")]
13376    #[test]
13377    fn javascript_exported_declaration_counts_once() {
13378        check_metrics::<JavascriptParser>(
13379            "export const a = 1;\nexport let b = 2;\nexport default 3;\nexport function f() { const c = 4; }\n",
13380            "foo.js",
13381            |metric| {
13382                assert_eq!(metric.loc.sloc(), 4);
13383                assert_eq!(metric.loc.ploc(), 4);
13384                assert_eq!(metric.loc.lloc(), 5);
13385                assert_eq!(metric.loc.cloc(), 0);
13386                assert_eq!(metric.loc.blank(), 0);
13387            },
13388        );
13389    }
13390
13391    /// A `var` / `let` / `const` declaration is an executable statement and
13392    /// counts one LLOC, the same as Java's `LocalVariableDeclaration` and
13393    /// Rust's `let` (#1283 — before the fix a declarations-only file
13394    /// reported `lloc 0`). The fourth row is one `variable_declaration`
13395    /// carrying two declarators, so it counts once, not twice. The fifth row
13396    /// is a `using_declaration` — the grammar's third executable declaration
13397    /// kind, which TypeScript and TSX do not have.
13398    #[cfg(feature = "mozjs")]
13399    #[test]
13400    fn mozjs_declaration_lloc() {
13401        check_metrics::<MozjsParser>(
13402            "var a = 1;\nlet b = 2;\nconst c = 3;\nvar d = 4, e = 5;\nusing r = open();\n",
13403            "foo.js",
13404            |metric| {
13405                assert_eq!(metric.loc.sloc(), 5);
13406                assert_eq!(metric.loc.ploc(), 5);
13407                assert_eq!(metric.loc.lloc(), 5);
13408                assert_eq!(metric.loc.cloc(), 0);
13409                assert_eq!(metric.loc.blank(), 0);
13410            },
13411        );
13412    }
13413
13414    /// The classic `for (var i = 0; …)` header is part of the `ForStatement`,
13415    /// which already counts its own LLOC, so the header declaration must not
13416    /// add a second one. `for (const x of …)` and `for (var k in …)` need no
13417    /// carve-out at all: the grammar inlines the `const` / `var` keyword into
13418    /// `for_in_statement` and emits no declaration node — they are here so a
13419    /// carve-out wrongly widened to `ForInStatement` still has an input that
13420    /// notices. `var s = i;` in the loop *body* is a real logical line: the
13421    /// `StatementBlock` stops the ancestor walk (#1283).
13422    ///
13423    /// expected: for-statement 1 + body declaration 1 + for-of 1 + for-in 1 = 4
13424    #[cfg(feature = "mozjs")]
13425    #[test]
13426    fn mozjs_for_header_declaration_not_double_counted() {
13427        check_metrics::<MozjsParser>(
13428            "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",
13429            "foo.js",
13430            |metric| {
13431                assert_eq!(metric.loc.sloc(), 7);
13432                assert_eq!(metric.loc.ploc(), 7);
13433                assert_eq!(metric.loc.lloc(), 4);
13434                assert_eq!(metric.loc.cloc(), 0);
13435                assert_eq!(metric.loc.blank(), 0);
13436            },
13437        );
13438    }
13439
13440    /// `export const a = 1;` is one logical line, not two: the declaration
13441    /// nests inside the `ExportStatement`, whose arm already counted the row.
13442    /// The declaration inside the exported function body still counts — the
13443    /// `StatementBlock` stops the ancestor walk before the `ExportStatement`
13444    /// is reached (#1283).
13445    ///
13446    /// expected: 4 export statements + the `const c = 4;` in `f`'s body = 5
13447    #[cfg(feature = "mozjs")]
13448    #[test]
13449    fn mozjs_exported_declaration_counts_once() {
13450        check_metrics::<MozjsParser>(
13451            "export const a = 1;\nexport let b = 2;\nexport default 3;\nexport function f() { const c = 4; }\n",
13452            "foo.js",
13453            |metric| {
13454                assert_eq!(metric.loc.sloc(), 4);
13455                assert_eq!(metric.loc.ploc(), 4);
13456                assert_eq!(metric.loc.lloc(), 5);
13457                assert_eq!(metric.loc.cloc(), 0);
13458                assert_eq!(metric.loc.blank(), 0);
13459            },
13460        );
13461    }
13462
13463    /// A `var` / `let` / `const` declaration is an executable statement and
13464    /// counts one LLOC, the same as Java's `LocalVariableDeclaration` and
13465    /// Rust's `let` (#1283 — before the fix a declarations-only file
13466    /// reported `lloc 0`). The fourth row is one `variable_declaration`
13467    /// carrying two declarators, so it counts once, not twice.
13468    #[cfg(feature = "typescript")]
13469    #[test]
13470    fn typescript_declaration_lloc() {
13471        check_metrics::<TypescriptParser>(
13472            "var a: number = 1;\nlet b = 2;\nconst c = 3;\nvar d = 4, e = 5;\n",
13473            "foo.ts",
13474            |metric| {
13475                assert_eq!(metric.loc.sloc(), 4);
13476                assert_eq!(metric.loc.ploc(), 4);
13477                assert_eq!(metric.loc.lloc(), 4);
13478                assert_eq!(metric.loc.cloc(), 0);
13479                assert_eq!(metric.loc.blank(), 0);
13480            },
13481        );
13482    }
13483
13484    /// The classic `for (var i = 0; …)` header is part of the `ForStatement`,
13485    /// which already counts its own LLOC, so the header declaration must not
13486    /// add a second one. `for (const x of …)` and `for (var k in …)` need no
13487    /// carve-out at all: the grammar inlines the `const` / `var` keyword into
13488    /// `for_in_statement` and emits no declaration node — they are here so a
13489    /// carve-out wrongly widened to `ForInStatement` still has an input that
13490    /// notices. `var s = i;` in the loop *body* is a real logical line: the
13491    /// `StatementBlock` stops the ancestor walk (#1283).
13492    ///
13493    /// expected: for-statement 1 + body declaration 1 + for-of 1 + for-in 1 = 4
13494    #[cfg(feature = "typescript")]
13495    #[test]
13496    fn typescript_for_header_declaration_not_double_counted() {
13497        check_metrics::<TypescriptParser>(
13498            "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",
13499            "foo.ts",
13500            |metric| {
13501                assert_eq!(metric.loc.sloc(), 7);
13502                assert_eq!(metric.loc.ploc(), 7);
13503                assert_eq!(metric.loc.lloc(), 4);
13504                assert_eq!(metric.loc.cloc(), 0);
13505                assert_eq!(metric.loc.blank(), 0);
13506            },
13507        );
13508    }
13509
13510    /// `export const a = 1;` is one logical line, not two: the declaration
13511    /// nests inside the `ExportStatement`, whose arm already counted the row.
13512    /// The declaration inside the exported function body still counts — the
13513    /// `StatementBlock` stops the ancestor walk before the `ExportStatement`
13514    /// is reached (#1283).
13515    ///
13516    /// expected: 4 export statements + the `const c = 4;` in `f`'s body = 5
13517    ///
13518    /// The TypeScript spelling also pins `export declare const …`, where an
13519    /// `ambient_declaration` sits between the export and the declaration: the
13520    /// carve-out walks the ancestor chain rather than checking the parent, so
13521    /// it still sees the enclosing `ExportStatement`.
13522    #[cfg(feature = "typescript")]
13523    #[test]
13524    fn typescript_exported_declaration_counts_once() {
13525        check_metrics::<TypescriptParser>(
13526            "export const a: number = 1;\nexport declare const b: string;\nexport default 3;\nexport function f(): void { const c = 4; }\n",
13527            "foo.ts",
13528            |metric| {
13529                assert_eq!(metric.loc.sloc(), 4);
13530                assert_eq!(metric.loc.ploc(), 4);
13531                assert_eq!(metric.loc.lloc(), 5);
13532                assert_eq!(metric.loc.cloc(), 0);
13533                assert_eq!(metric.loc.blank(), 0);
13534            },
13535        );
13536    }
13537
13538    /// A `var` / `let` / `const` declaration is an executable statement and
13539    /// counts one LLOC, the same as Java's `LocalVariableDeclaration` and
13540    /// Rust's `let` (#1283 — before the fix a declarations-only file
13541    /// reported `lloc 0`). The fourth row is one `variable_declaration`
13542    /// carrying two declarators, so it counts once, not twice.
13543    #[cfg(feature = "typescript")]
13544    #[test]
13545    fn tsx_declaration_lloc() {
13546        check_metrics::<TsxParser>(
13547            "var a: number = 1;\nlet b = 2;\nconst c = 3;\nvar d = 4, e = 5;\n",
13548            "foo.tsx",
13549            |metric| {
13550                assert_eq!(metric.loc.sloc(), 4);
13551                assert_eq!(metric.loc.ploc(), 4);
13552                assert_eq!(metric.loc.lloc(), 4);
13553                assert_eq!(metric.loc.cloc(), 0);
13554                assert_eq!(metric.loc.blank(), 0);
13555            },
13556        );
13557    }
13558
13559    /// The classic `for (var i = 0; …)` header is part of the `ForStatement`,
13560    /// which already counts its own LLOC, so the header declaration must not
13561    /// add a second one. `for (const x of …)` and `for (var k in …)` need no
13562    /// carve-out at all: the grammar inlines the `const` / `var` keyword into
13563    /// `for_in_statement` and emits no declaration node — they are here so a
13564    /// carve-out wrongly widened to `ForInStatement` still has an input that
13565    /// notices. `var s = i;` in the loop *body* is a real logical line: the
13566    /// `StatementBlock` stops the ancestor walk (#1283).
13567    ///
13568    /// expected: for-statement 1 + body declaration 1 + for-of 1 + for-in 1 = 4
13569    #[cfg(feature = "typescript")]
13570    #[test]
13571    fn tsx_for_header_declaration_not_double_counted() {
13572        check_metrics::<TsxParser>(
13573            "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",
13574            "foo.tsx",
13575            |metric| {
13576                assert_eq!(metric.loc.sloc(), 7);
13577                assert_eq!(metric.loc.ploc(), 7);
13578                assert_eq!(metric.loc.lloc(), 4);
13579                assert_eq!(metric.loc.cloc(), 0);
13580                assert_eq!(metric.loc.blank(), 0);
13581            },
13582        );
13583    }
13584
13585    /// `export const a = 1;` is one logical line, not two: the declaration
13586    /// nests inside the `ExportStatement`, whose arm already counted the row.
13587    /// The declaration inside the exported function body still counts — the
13588    /// `StatementBlock` stops the ancestor walk before the `ExportStatement`
13589    /// is reached (#1283).
13590    ///
13591    /// expected: 4 export statements + the `const c = 4;` in `f`'s body = 5
13592    ///
13593    /// The TypeScript spelling also pins `export declare const …`, where an
13594    /// `ambient_declaration` sits between the export and the declaration: the
13595    /// carve-out walks the ancestor chain rather than checking the parent, so
13596    /// it still sees the enclosing `ExportStatement`.
13597    #[cfg(feature = "typescript")]
13598    #[test]
13599    fn tsx_exported_declaration_counts_once() {
13600        check_metrics::<TsxParser>(
13601            "export const a: number = 1;\nexport declare const b: string;\nexport default 3;\nexport function f(): void { const c = 4; }\n",
13602            "foo.tsx",
13603            |metric| {
13604                assert_eq!(metric.loc.sloc(), 4);
13605                assert_eq!(metric.loc.ploc(), 4);
13606                assert_eq!(metric.loc.lloc(), 5);
13607                assert_eq!(metric.loc.cloc(), 0);
13608                assert_eq!(metric.loc.blank(), 0);
13609            },
13610        );
13611    }
13612
13613    /// A brace-less `for` body is a declaration with the *same* parent as
13614    /// the header, so a carve-out keyed on the enclosing kind alone
13615    /// dropped it (#1283 review): `for (…) var s = i;` counted one
13616    /// logical line where the braced spelling and `for (…) x++;` count
13617    /// two. The header is identified by the `initializer` field instead.
13618    /// The nested spelling — a `switch` as the brace-less body — is the
13619    /// same miss one level down.
13620    ///
13621    /// expected: for 1 + body declaration 1 = 2; for 1 + switch 1 +
13622    /// case declaration 1 = 3.
13623    #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
13624    #[test]
13625    fn js_family_braceless_for_body_declaration_counts() {
13626        #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
13627        const BODY: &str = "for (var i = 0; i < 3; i++) var s = i;\n";
13628        #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
13629        const NESTED: &str = "for (let i = 0; i < 2; i++) switch (i) { case 0: let y = 1; }\n";
13630        check_metrics::<JavascriptParser>(BODY, "foo.js", |m| assert_eq!(m.loc.lloc(), 2));
13631        check_metrics::<MozjsParser>(BODY, "foo.js", |m| assert_eq!(m.loc.lloc(), 2));
13632        check_metrics::<TypescriptParser>(BODY, "foo.ts", |m| assert_eq!(m.loc.lloc(), 2));
13633        check_metrics::<TsxParser>(BODY, "foo.tsx", |m| assert_eq!(m.loc.lloc(), 2));
13634        check_metrics::<JavascriptParser>(NESTED, "foo.js", |m| assert_eq!(m.loc.lloc(), 3));
13635        check_metrics::<MozjsParser>(NESTED, "foo.js", |m| assert_eq!(m.loc.lloc(), 3));
13636        check_metrics::<TypescriptParser>(NESTED, "foo.ts", |m| assert_eq!(m.loc.lloc(), 3));
13637        check_metrics::<TsxParser>(NESTED, "foo.tsx", |m| assert_eq!(m.loc.lloc(), 3));
13638    }
13639
13640    /// Ambient declarations execute nothing — `declare const x: T;` has
13641    /// no initializer to run — so they are no logical line, whether the
13642    /// `declare` is top-level or the declaration sits inside a
13643    /// `declare namespace` / `declare module` body (#1283 review: only
13644    /// the `export declare` spelling was carved out, so a `.d.ts` file
13645    /// reported one LLOC per `declare const`).
13646    ///
13647    /// expected: 0 — every row is ambient.
13648    #[cfg(feature = "typescript")]
13649    #[test]
13650    fn typescript_ambient_declarations_are_not_logical_lines() {
13651        const SRC: &str = "declare const VERSION: string;\ndeclare let mutable: number;\ndeclare namespace NS { const inner: number; }\ndeclare module \"m\" { let y: string; }\n";
13652        check_metrics::<TypescriptParser>(SRC, "foo.ts", |m| assert_eq!(m.loc.lloc(), 0));
13653        check_metrics::<TsxParser>(SRC, "foo.tsx", |m| assert_eq!(m.loc.lloc(), 0));
13654    }
13655}