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 dloc;
90mod line_set;
91
92use dloc::Dloc;
93use line_set::LineSet;
94
95/// The `SLoc` metric suite.
96#[derive(Debug, Clone, PartialEq)]
97pub struct Sloc {
98    start: usize,
99    // 1-based inclusive last line of the span, from `Node::end_line`.
100    // Storing the resolved line rather than the raw end row plus its
101    // column keeps the "does the final row count" rule in one place.
102    end_line: usize,
103    // Physical rows removed from this space's span by `exclude_tests`
104    // pruning. `sloc` is the lone loc sub-metric computed by span
105    // subtraction rather than node-by-node accumulation, so a pruned
106    // subtree (which a `continue` in the walk suppresses for every
107    // accumulated metric) leaves the span untouched. We record each
108    // pruned subtree's rows here and subtract the cardinality in
109    // `sloc()` so SLOC drops in step with `ploc`/`cloc`/`lloc`
110    // (issue #722).
111    //
112    // A set rather than the counter this was until #1417, because rows
113    // shared with retained code have to be identifiable and removed
114    // before the subtraction — see `Stats::settle_excluded_rows`. It is
115    // the single source of truth; there is no cached count to fall out
116    // of step with it, matching what `ploc`/`cloc` already gave up
117    // when they became bitsets (#1109).
118    excluded_rows: LineSet,
119    sloc_min: usize,
120    sloc_max: usize,
121}
122
123impl Default for Sloc {
124    fn default() -> Self {
125        Self {
126            start: 0,
127            end_line: 0,
128            excluded_rows: LineSet::default(),
129            sloc_min: usize::MAX,
130            sloc_max: 0,
131        }
132    }
133}
134
135impl Sloc {
136    /// The `SLOC` metric value for this space (source lines, including blanks and comments).
137    #[inline]
138    #[must_use]
139    pub fn sloc(&self) -> u64 {
140        // This metric counts the number of physical lines this space
141        // occupies, including blanks and comments.
142        let span = span_rows(self.start, self.end_line);
143        // Subtract the rows belonging to `exclude_tests`-pruned subtrees
144        // (issue #722). `saturating_sub` is defensive: the set is
145        // clamped to the span and its rows are a subset of it, but a
146        // future caller that records a row outside the span must not
147        // wrap to `u64::MAX`.
148        span.saturating_sub(self.excluded_rows.len()) as u64
149    }
150
151    /// Records the rows of a pruned (`exclude_tests`) subtree so that
152    /// `sloc()` drops in step with the node-accumulated loc sub-metrics.
153    /// The arguments are the pruned node's own start row and
154    /// `Node::end_line`, so the rows recorded follow the same rule the
155    /// enclosing span was measured with.
156    ///
157    /// A *set* rather than the running count this kept until #1417. A
158    /// pruned item can share its first or last physical row with a
159    /// retained sibling — `fn a() {} #[cfg(test)] mod t { … }` is one
160    /// row, of which nothing is removed — and a count cannot tell that
161    /// row from one the prune really took, so `sloc()` fell below
162    /// `ploc()`. The set also makes two pruned siblings on one row
163    /// subtract that row once instead of twice.
164    ///
165    /// The rows recorded here are the pruned node's *whole* span;
166    /// [`Stats::settle_excluded_rows`] is what takes the shared ones
167    /// back out, once the retained sets for this space are complete.
168    #[inline]
169    pub(crate) fn exclude_span(&mut self, start_row: usize, end_line: usize) {
170        // `end_line` is the 1-based inclusive last row, so the last
171        // 0-based row is `end_line - 1`. Guarding here rather than
172        // leaning on `insert_range`'s inverted-span return, which would
173        // need `end_line - 1` computed first: at `end_line == 0` that
174        // saturates to row 0 and records a row the node does not
175        // occupy, and an empty span is not an inverted one.
176        if end_line <= start_row {
177            debug_assert!(
178                end_line == start_row,
179                "exclude_span: end_line {end_line} < start_row {start_row}"
180            );
181            return;
182        }
183        // Exact under the guard — `end_line > start_row >= 0` gives
184        // `end_line >= 1` — and saturating for the module's
185        // `arithmetic_side_effects` warning, which cannot see that.
186        // The row count inserted is `span_rows(start_row, end_line)`,
187        // byte-identical to the count this used to add.
188        self.excluded_rows
189            .insert_range(start_row, end_line.saturating_sub(1));
190    }
191
192    /// The `Sloc` metric minimum value. See `min_or_zero` for the
193    /// `usize::MAX` sentinel guard.
194    #[inline]
195    #[must_use]
196    pub fn sloc_min(&self) -> u64 {
197        min_or_zero(self.sloc_min)
198    }
199
200    /// The `Sloc` metric maximum value.
201    #[inline]
202    #[must_use]
203    pub fn sloc_max(&self) -> u64 {
204        self.sloc_max as u64
205    }
206
207    /// Folds `other` into `self`, updating the min/max accumulators and
208    /// unioning the child's `exclude_tests`-pruned rows.
209    #[inline]
210    pub fn merge(&mut self, other: &Sloc) {
211        // Fold the child's own min/max (not its aggregate `sloc()`), so the
212        // granularity of deeply nested function spaces propagates to the
213        // root. This matches every sibling metric (cyclomatic, cognitive,
214        // exit, nargs, nom, tokens, abc) and fixed issue #437.
215        self.sloc_min = self.sloc_min.min(other.sloc_min);
216        self.sloc_max = self.sloc_max.max(other.sloc_max);
217
218        // Propagate the child's pruned rows upward so an ancestor's
219        // span-based `sloc()` drops by the same rows, mirroring how `Ploc`
220        // unions its line-set upward (`Ploc::merge`). The prune hook records
221        // each pruned subtree's span only on its innermost enclosing
222        // func-space; without this fold a `#[test] fn` inside a retained
223        // `impl`/`trait`/closure would shrink only that space's `sloc`,
224        // leaving every enclosing space (including the unit, which feeds
225        // MI's SLOC term) inflated (issue #741, #722 follow-up). Each
226        // ancestor's span already includes the pruned rows exactly once, so
227        // subtracting them once per level cannot double-count: pruned
228        // subtrees never descend, so a nested pruned item is recorded on a
229        // single space and folded up one altitude at a time.
230        //
231        // Union rather than the `saturating_add` this was until #1417.
232        // Addition double-counted two pruned siblings sharing one
233        // physical row, and — the reason the fold is safe to leave
234        // exactly as it is — the child arrives here already settled
235        // against its own retained rows, which the parent's later
236        // `settle_excluded_rows` can only narrow further:
237        // `child_retained ⊆ parent_retained`, so
238        // `(X \ child_retained) \ parent_retained == X \ parent_retained`.
239        // The order the two settle in therefore does not matter.
240        self.excluded_rows.union_with(&other.excluded_rows);
241    }
242
243    #[inline]
244    pub(crate) fn compute_minmax(&mut self) {
245        // Fold this space's own span unconditionally so containers (Unit,
246        // classes) participate in min/max, matching the sibling metrics'
247        // convention. Each space runs this before being merged upward, so
248        // the guarded form previously here dropped nested leaves (#437).
249        self.sloc_min = self.sloc_min.min(self.sloc() as usize);
250        self.sloc_max = self.sloc_max.max(self.sloc() as usize);
251    }
252}
253
254/// The `PLoc` metric suite.
255#[derive(Debug, Clone, PartialEq)]
256pub struct Ploc {
257    lines: LineSet,
258    ploc_min: usize,
259    ploc_max: usize,
260}
261
262impl Default for Ploc {
263    fn default() -> Self {
264        Self {
265            lines: LineSet::default(),
266            ploc_min: usize::MAX,
267            ploc_max: 0,
268        }
269    }
270}
271
272impl Ploc {
273    /// The `PLOC` metric value for this space (physical lines of code, excluding blanks and comments).
274    #[inline]
275    #[must_use]
276    pub fn ploc(&self) -> u64 {
277        // This metric counts the number of instruction lines in a code
278        // https://en.wikipedia.org/wiki/Source_lines_of_code
279        self.lines.len() as u64
280    }
281
282    /// The `Ploc` metric minimum value. See `min_or_zero` for the
283    /// `usize::MAX` sentinel guard.
284    #[inline]
285    #[must_use]
286    pub fn ploc_min(&self) -> u64 {
287        min_or_zero(self.ploc_min)
288    }
289
290    /// The `Ploc` metric maximum value.
291    #[inline]
292    #[must_use]
293    pub fn ploc_max(&self) -> u64 {
294        self.ploc_max as u64
295    }
296
297    /// Folds `other` into `self`, unioning the line set and updating min/max.
298    #[inline]
299    pub fn merge(&mut self, other: &Ploc) {
300        // Union the child's physical lines in, so a line shared with a
301        // sibling space is counted once. A word-wise OR rather than an
302        // insert per row: a line inside D nested spaces is folded upward
303        // D times (#1109).
304        self.lines.union_with(&other.lines);
305
306        // Fold the child's own min/max so nested spaces propagate (#437).
307        self.ploc_min = self.ploc_min.min(other.ploc_min);
308        self.ploc_max = self.ploc_max.max(other.ploc_max);
309    }
310
311    #[inline]
312    pub(crate) fn compute_minmax(&mut self) {
313        // Fold this space's own value unconditionally so containers
314        // participate, matching the sibling metrics' convention (#437).
315        // Bound once: `ploc()` is a popcount over the whole word array
316        // since #1109, not the O(1) `HashSet::len` it used to be.
317        let ploc = self.ploc() as usize;
318        self.ploc_min = self.ploc_min.min(ploc);
319        self.ploc_max = self.ploc_max.max(ploc);
320    }
321}
322
323/// The `CLoc` metric suite.
324#[derive(Debug, Clone, PartialEq)]
325pub struct Cloc {
326    // Physical lines that are comment-only (no code). Feeds both
327    // `cloc()` and the `blank` metric (`sloc - ploc - only.len()`).
328    // A set rather than a counter so two standalone block comments on
329    // one physical line (`/*a*/ /*b*/`) contribute a single comment
330    // line, not one per node (issue #461 follow-up). Each spanned row
331    // of a genuine multi-line block comment is a distinct key, so it
332    // still counts once per line.
333    only_comment_line_starts: LineSet,
334    // Physical lines carrying both code and comment (`int x; /*c*/`).
335    // A line with several inline block comments (`f(int /*a*/, int
336    // /*b*/)`) must contribute a single comment line, not one per
337    // node, otherwise cloc can exceed sloc/ploc and push the MI
338    // comment ratio above 1 (issue #461). Mirrors `Ploc`'s
339    // per-line de-dup via `Ploc::lines`; disjoint from
340    // `only_comment_line_starts` by construction.
341    code_comment_line_starts: LineSet,
342    comment_line_end: Option<usize>,
343    cloc_min: usize,
344    cloc_max: usize,
345}
346
347impl Default for Cloc {
348    fn default() -> Self {
349        Self {
350            only_comment_line_starts: LineSet::default(),
351            code_comment_line_starts: LineSet::default(),
352            comment_line_end: Option::default(),
353            cloc_min: usize::MAX,
354            cloc_max: 0,
355        }
356    }
357}
358
359impl Cloc {
360    /// The `CLOC` metric value for this space (comment lines, standalone + trailing).
361    #[inline]
362    #[must_use]
363    pub fn cloc(&self) -> u64 {
364        // Comments are counted regardless of their placement
365        // https://en.wikipedia.org/wiki/Source_lines_of_code
366        //
367        // Derive from the per-physical-line sets rather than summed
368        // counters so co-located comments (standalone or inline) count
369        // their shared line once and a comment line shared across
370        // merged spaces is not double-counted (issue #461). The two
371        // sets are disjoint by construction, but a union is used
372        // defensively so a stray overlap cannot inflate the count.
373        self.only_comment_line_starts
374            .union_len(&self.code_comment_line_starts) as u64
375    }
376
377    /// The `Cloc` metric minimum value. See `min_or_zero` for the
378    /// `usize::MAX` sentinel guard.
379    #[inline]
380    #[must_use]
381    pub fn cloc_min(&self) -> u64 {
382        min_or_zero(self.cloc_min)
383    }
384
385    /// The `Cloc` metric maximum value.
386    #[inline]
387    #[must_use]
388    pub fn cloc_max(&self) -> u64 {
389        self.cloc_max as u64
390    }
391
392    /// Folds `other` into `self`, summing comment counts and updating min/max.
393    #[inline]
394    pub fn merge(&mut self, other: &Cloc) {
395        // Union both per-line sets so a comment line shared across
396        // merged spaces is counted once (mirrors `Ploc`'s line union).
397        self.only_comment_line_starts
398            .union_with(&other.only_comment_line_starts);
399        self.code_comment_line_starts
400            .union_with(&other.code_comment_line_starts);
401
402        // Fold the child's own min/max so nested spaces propagate (#437).
403        self.cloc_min = self.cloc_min.min(other.cloc_min);
404        self.cloc_max = self.cloc_max.max(other.cloc_max);
405    }
406
407    #[inline]
408    pub(crate) fn compute_minmax(&mut self) {
409        // Fold this space's own value unconditionally so containers
410        // participate, matching the sibling metrics' convention (#437).
411        // Bound once: `cloc()` is a `union_len` over both word arrays
412        // since #1109, so calling it twice is six array scans per space.
413        let cloc = self.cloc() as usize;
414        self.cloc_min = self.cloc_min.min(cloc);
415        self.cloc_max = self.cloc_max.max(cloc);
416    }
417}
418
419/// The `LLoc` metric suite.
420#[derive(Debug, Clone, PartialEq)]
421pub struct Lloc {
422    logical_lines: usize,
423    lloc_min: usize,
424    lloc_max: usize,
425}
426
427impl Default for Lloc {
428    fn default() -> Self {
429        Self {
430            logical_lines: 0,
431            lloc_min: usize::MAX,
432            lloc_max: 0,
433        }
434    }
435}
436
437impl Lloc {
438    /// The `LLOC` metric value for this space (logical statements).
439    #[inline]
440    #[must_use]
441    pub fn lloc(&self) -> u64 {
442        // This metric counts the number of statements in a code
443        // https://en.wikipedia.org/wiki/Source_lines_of_code
444        self.logical_lines as u64
445    }
446
447    /// The `Lloc` metric minimum value. See `min_or_zero` for the
448    /// `usize::MAX` sentinel guard.
449    #[inline]
450    #[must_use]
451    pub fn lloc_min(&self) -> u64 {
452        min_or_zero(self.lloc_min)
453    }
454
455    /// The `Lloc` metric maximum value.
456    #[inline]
457    #[must_use]
458    pub fn lloc_max(&self) -> u64 {
459        self.lloc_max as u64
460    }
461
462    /// Records one logical statement.
463    ///
464    /// Exists so the 23 per-language `Loc` impls name the operation
465    /// instead of each reaching into a private field, which also keeps
466    /// the module's `arithmetic_side_effects` carve-out to this one
467    /// line rather than 36 of them (#1152). Saturating is unreachable —
468    /// the count is bounded by the AST's node count — and is the right
469    /// answer if it ever were: a pinned `usize::MAX` is a visibly broken
470    /// LLOC, where a wrap to 0 reads as a legitimately empty space.
471    #[inline]
472    fn count_logical_line(&mut self) {
473        self.logical_lines = self.logical_lines.saturating_add(1);
474    }
475
476    /// Folds `other` into `self`, summing statement counts and updating min/max.
477    #[inline]
478    pub fn merge(&mut self, other: &Lloc) {
479        // Merge lloc lines
480        self.logical_lines = self.logical_lines.saturating_add(other.logical_lines);
481        // Fold the child's own min/max so nested spaces propagate (#437).
482        self.lloc_min = self.lloc_min.min(other.lloc_min);
483        self.lloc_max = self.lloc_max.max(other.lloc_max);
484    }
485
486    #[inline]
487    pub(crate) fn compute_minmax(&mut self) {
488        // Fold this space's own value unconditionally so containers
489        // participate, matching the sibling metrics' convention (#437).
490        self.lloc_min = self.lloc_min.min(self.lloc() as usize);
491        self.lloc_max = self.lloc_max.max(self.lloc() as usize);
492    }
493}
494
495/// The `Loc` metric suite.
496#[derive(Debug, Clone, PartialEq)]
497#[non_exhaustive]
498pub struct Stats {
499    sloc: Sloc,
500    ploc: Ploc,
501    cloc: Cloc,
502    lloc: Lloc,
503    dloc: Dloc,
504    space_count: usize,
505    blank_min: usize,
506    blank_max: usize,
507}
508
509impl Default for Stats {
510    fn default() -> Self {
511        Self {
512            sloc: Sloc::default(),
513            ploc: Ploc::default(),
514            cloc: Cloc::default(),
515            lloc: Lloc::default(),
516            dloc: Dloc::default(),
517            space_count: 1,
518            blank_min: usize::MAX,
519            blank_max: 0,
520        }
521    }
522}
523
524impl fmt::Display for Stats {
525    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
526        write!(
527            f,
528            "sloc: {}, ploc: {}, lloc: {}, cloc: {}, blank: {}, dloc: {}, sloc_average: {}, ploc_average: {}, lloc_average: {}, cloc_average: {}, blank_average: {}, dloc_average: {}, sloc_min: {}, sloc_max: {}, cloc_min: {}, cloc_max: {}, ploc_min: {}, ploc_max: {}, lloc_min: {}, lloc_max: {}, blank_min: {}, blank_max: {}, dloc_min: {}, dloc_max: {}",
529            self.sloc(),
530            self.ploc(),
531            self.lloc(),
532            self.cloc(),
533            self.blank(),
534            self.dloc(),
535            self.sloc_average(),
536            self.ploc_average(),
537            self.lloc_average(),
538            self.cloc_average(),
539            self.blank_average(),
540            self.dloc_average(),
541            self.sloc_min(),
542            self.sloc_max(),
543            self.cloc_min(),
544            self.cloc_max(),
545            self.ploc_min(),
546            self.ploc_max(),
547            self.lloc_min(),
548            self.lloc_max(),
549            self.blank_min(),
550            self.blank_max(),
551            self.dloc_min(),
552            self.dloc_max(),
553        )
554    }
555}
556
557impl Stats {
558    /// Test-only constructor that forces a degenerate `cloc`/`sloc`
559    /// pair (here `cloc > sloc`) so callers in other metric modules can
560    /// exercise downstream clamps without depending on the parsing
561    /// pipeline. `cloc <= sloc` always holds for parsed input after
562    /// issue #461, so this state is unreachable through normal use.
563    #[cfg(test)]
564    pub(crate) fn with_cloc_sloc(code_comment_lines: usize, sloc_end_row: usize) -> Self {
565        let mut stats = Stats::default();
566        stats.sloc.start = 0;
567        // `end_row + 1`: the synthetic span models a real one ending
568        // mid-line, so the final row counts and `sloc == sloc_end_row + 1`.
569        stats.sloc.end_line = sloc_end_row.saturating_add(1);
570        // Inject `code_comment_lines` distinct synthetic code-comment
571        // rows. An offset past `sloc_end_row` keeps them disjoint from
572        // any real span row, so `cloc()` (the set's cardinality) equals
573        // the requested count without colliding with sloc attribution.
574        if code_comment_lines > 0 {
575            let synthetic_base = sloc_end_row.saturating_add(1);
576            // Explicit rather than leaning on `insert_range`'s inverted-span
577            // guard: that guard exists to survive a bug, not to serve as a
578            // caller's empty case. The `- 1` is exact under the `> 0` test
579            // above, which is also what makes the inclusive end well-formed.
580            let synthetic_end = synthetic_base
581                .saturating_add(code_comment_lines)
582                .saturating_sub(1);
583            stats
584                .cloc
585                .code_comment_line_starts
586                .insert_range(synthetic_base, synthetic_end);
587        }
588        stats
589    }
590
591    /// Merges a second `Loc` metric suite into the first one
592    pub fn merge(&mut self, other: &Stats) {
593        self.sloc.merge(&other.sloc);
594        self.ploc.merge(&other.ploc);
595        self.cloc.merge(&other.cloc);
596        self.lloc.merge(&other.lloc);
597        self.dloc.merge(&other.dloc);
598
599        // Count spaces
600        self.space_count = self.space_count.saturating_add(other.space_count);
601
602        // Fold the child's own min/max so nested spaces propagate (#437).
603        self.blank_min = self.blank_min.min(other.blank_min);
604        self.blank_max = self.blank_max.max(other.blank_max);
605    }
606
607    /// Records an `exclude_tests`-pruned subtree's span so this space's
608    /// `sloc()` excludes those physical lines, matching the
609    /// node-accumulated loc sub-metrics that the pruning already drops
610    /// (issue #722). Called from the walker for the space enclosing each
611    /// skipped subtree.
612    #[inline]
613    pub(crate) fn exclude_test_span(&mut self, start_row: usize, end_line: usize) {
614        self.sloc.exclude_span(start_row, end_line);
615    }
616
617    /// The `Sloc` metric.
618    ///
619    /// Counts the number of lines in a scope
620    #[inline]
621    #[must_use]
622    pub fn sloc(&self) -> u64 {
623        self.sloc.sloc()
624    }
625
626    /// The `Ploc` metric.
627    ///
628    /// Counts the number of instruction lines in a scope
629    #[inline]
630    #[must_use]
631    pub fn ploc(&self) -> u64 {
632        self.ploc.ploc()
633    }
634
635    /// The `Lloc` metric.
636    ///
637    /// Counts the number of statements in a scope
638    #[inline]
639    #[must_use]
640    pub fn lloc(&self) -> u64 {
641        self.lloc.lloc()
642    }
643
644    /// The `Cloc` metric.
645    ///
646    /// Counts the number of comments in a scope
647    #[inline]
648    #[must_use]
649    pub fn cloc(&self) -> u64 {
650        self.cloc.cloc()
651    }
652
653    /// The `Dloc` metric: Documentation Lines Of Code.
654    ///
655    /// Counts the rows a language treats as documentation — a doc
656    /// comment its own tool recognises (`Checker::is_doc_comment`), a
657    /// Python docstring, a Perl POD block. Those rows are also comment
658    /// lines, so `dloc() <= cloc()`; a language without a documentation
659    /// syntax reports `0` (#1597).
660    #[inline]
661    #[must_use]
662    pub fn dloc(&self) -> u64 {
663        self.dloc.dloc()
664    }
665
666    /// Documentation rows that carry no code: `dloc()` less the rows
667    /// `ploc` also holds. A docstring that opens on its `def` row, or
668    /// whose closing quote is followed by a statement, shares that row
669    /// with code, and the denominator of `mi.sei`'s comment ratio must
670    /// keep counting it (#1597).
671    #[inline]
672    #[must_use]
673    pub(crate) fn dloc_without_code(&self) -> u64 {
674        let shared = self.dloc.lines.intersection_len(&self.ploc.lines);
675        self.dloc.lines.len().saturating_sub(shared) as u64
676    }
677
678    /// The `Blank` metric.
679    ///
680    /// Counts the number of blank lines in a scope
681    #[inline]
682    #[must_use]
683    pub fn blank(&self) -> u64 {
684        // `sloc - ploc - only_comment_lines` can go negative when a space's
685        // physical and comment line attribution overlaps the span row count
686        // (e.g. single-line bodies). `saturating_sub` clamps at 0 (matching
687        // the prior `.max(0.0)` on the f64 form) so the serialized value is
688        // never negative (#437).
689        self.sloc()
690            .saturating_sub(self.ploc())
691            .saturating_sub(self.cloc.only_comment_line_starts.len() as u64)
692    }
693
694    /// The `Sloc` metric average value.
695    ///
696    /// This value is computed dividing the `Sloc` value for the number of spaces
697    #[inline]
698    #[must_use]
699    pub fn sloc_average(&self) -> f64 {
700        crate::metrics::average(self.sloc() as f64, self.space_count)
701    }
702
703    /// The `Ploc` metric average value.
704    ///
705    /// This value is computed dividing the `Ploc` value for the number of spaces
706    #[inline]
707    #[must_use]
708    pub fn ploc_average(&self) -> f64 {
709        crate::metrics::average(self.ploc() as f64, self.space_count)
710    }
711
712    /// The `Lloc` metric average value.
713    ///
714    /// This value is computed dividing the `Lloc` value for the number of spaces
715    #[inline]
716    #[must_use]
717    pub fn lloc_average(&self) -> f64 {
718        crate::metrics::average(self.lloc() as f64, self.space_count)
719    }
720
721    /// The `Cloc` metric average value.
722    ///
723    /// This value is computed dividing the `Cloc` value for the number of spaces
724    #[inline]
725    #[must_use]
726    pub fn cloc_average(&self) -> f64 {
727        crate::metrics::average(self.cloc() as f64, self.space_count)
728    }
729
730    /// The `Dloc` metric average value.
731    ///
732    /// This value is computed dividing the `Dloc` value for the number of spaces
733    #[inline]
734    #[must_use]
735    pub fn dloc_average(&self) -> f64 {
736        crate::metrics::average(self.dloc() as f64, self.space_count)
737    }
738
739    /// The `Blank` metric average value.
740    ///
741    /// This value is computed dividing the `Blank` value for the number of spaces
742    #[inline]
743    #[must_use]
744    pub fn blank_average(&self) -> f64 {
745        crate::metrics::average(self.blank() as f64, self.space_count)
746    }
747
748    /// The `Sloc` metric minimum value.
749    #[inline]
750    #[must_use]
751    pub fn sloc_min(&self) -> u64 {
752        self.sloc.sloc_min()
753    }
754
755    /// The `Sloc` metric maximum value.
756    #[inline]
757    #[must_use]
758    pub fn sloc_max(&self) -> u64 {
759        self.sloc.sloc_max()
760    }
761
762    /// The `Cloc` metric minimum value.
763    #[inline]
764    #[must_use]
765    pub fn cloc_min(&self) -> u64 {
766        self.cloc.cloc_min()
767    }
768
769    /// The `Cloc` metric maximum value.
770    #[inline]
771    #[must_use]
772    pub fn cloc_max(&self) -> u64 {
773        self.cloc.cloc_max()
774    }
775
776    /// The `Dloc` metric minimum value.
777    #[inline]
778    #[must_use]
779    pub fn dloc_min(&self) -> u64 {
780        self.dloc.dloc_min()
781    }
782
783    /// The `Dloc` metric maximum value.
784    #[inline]
785    #[must_use]
786    pub fn dloc_max(&self) -> u64 {
787        self.dloc.dloc_max()
788    }
789
790    /// The `Ploc` metric minimum value.
791    #[inline]
792    #[must_use]
793    pub fn ploc_min(&self) -> u64 {
794        self.ploc.ploc_min()
795    }
796
797    /// The `Ploc` metric maximum value.
798    #[inline]
799    #[must_use]
800    pub fn ploc_max(&self) -> u64 {
801        self.ploc.ploc_max()
802    }
803
804    /// The `Lloc` metric minimum value.
805    #[inline]
806    #[must_use]
807    pub fn lloc_min(&self) -> u64 {
808        self.lloc.lloc_min()
809    }
810
811    /// The `Lloc` metric maximum value.
812    #[inline]
813    #[must_use]
814    pub fn lloc_max(&self) -> u64 {
815        self.lloc.lloc_max()
816    }
817
818    /// The `Blank` metric minimum value. See `min_or_zero` for the
819    /// `usize::MAX` sentinel guard.
820    #[inline]
821    #[must_use]
822    pub fn blank_min(&self) -> u64 {
823        min_or_zero(self.blank_min)
824    }
825
826    /// The `Blank` metric maximum value.
827    #[inline]
828    #[must_use]
829    pub fn blank_max(&self) -> u64 {
830        self.blank_max as u64
831    }
832
833    #[inline]
834    pub(crate) fn compute_minmax(&mut self) {
835        self.sloc.compute_minmax();
836        self.ploc.compute_minmax();
837        self.cloc.compute_minmax();
838        self.lloc.compute_minmax();
839        self.dloc.compute_minmax();
840
841        // Fold this space's own blank value unconditionally so containers
842        // participate, matching the sibling metrics' convention (#437).
843        // `blank()` returns a `u64` already clamped at 0 by `saturating_sub`,
844        // so the widening `as usize` cast is lossless (64-bit) and cannot
845        // introduce a spurious value here. Bound once: `blank()` popcounts
846        // two word arrays since #1109.
847        let blank = self.blank() as usize;
848        self.blank_min = self.blank_min.min(blank);
849        self.blank_max = self.blank_max.max(blank);
850    }
851
852    pub(crate) fn init_unit_span(&mut self, start: usize, end_line: usize) {
853        self.sloc.start = start;
854        self.sloc.end_line = end_line;
855    }
856
857    /// Drops every recorded PLOC / CLOC row that lies outside this
858    /// space's own `sloc` row span.
859    ///
860    /// That bounds `ploc` and `cloc` by the span, which is what the
861    /// `debug_assert!` below states. It is *not* the same as pinning
862    /// the public `ploc() <= sloc()`: `sloc()` subtracts
863    /// `exclude_tests`-pruned rows, and can still come out below `ploc`
864    /// — see the assertion's comment and #1417.
865    ///
866    /// A grammar's error recovery synthesises **zero-width tokens one
867    /// row past the last row the file has**: an unterminated Bash
868    /// heredoc gets a `heredoc_end` at `(3, 1)..(3, 1)` in a two-row
869    /// file, an unterminated Elixir / Lua / Groovy string gets its
870    /// closing quote the same way. Those tokens are childless, so they
871    /// reach the leaf branch of a language's catch-all arm, which
872    /// inserts a raw `start` row — and here that `start` *is* the
873    /// phantom row. [`Node::end_line`] already encodes "a node whose
874    /// end column is 0 does not occupy the row it ends on"; nothing
875    /// encoded the same rule for a node that *begins* past the span, nor
876    /// applied it to the raw end row `init` hands the comment arms — so a
877    /// comment ending at column 0 of the row past the last one (Perl POD
878    /// running to end-of-file without `=cut`) counted that row as a
879    /// comment. The clamp removes both.
880    ///
881    /// `ploc > sloc` is a contract violation rather than a rounding
882    /// artifact: [`Stats::blank`] saturates at 0, so the clamp there
883    /// hides it while a consumer computing `ploc / sloc` gets a ratio
884    /// above 1. Ten of the twenty-odd languages reproduced it on
885    /// truncated input before #1398 — a per-arm skip would have been
886    /// ten edits that still could not cover a grammar whose recovery
887    /// shape nobody sampled, so the rule lives once, here, keyed on the
888    /// span every language already reports.
889    ///
890    /// **Input whose tree stays inside the file's rows is untouched, and
891    /// a clean parse does not guarantee that**: Perl POD at end of file
892    /// and Ruby's unterminated `<<~` heredoc both parse without an error
893    /// node and still move. Over the `pdf.js`, `DeepSpeech` and
894    /// `serde` corpora exactly one of 1,876 files moves, and it is a
895    /// real bug fixed rather than a real row lost:
896    /// `DeepSpeech/parse_valgrind_suppressions.sh` leaves a MISSING `}`
897    /// at `(58, 1)` of a 57-row file, so its `ploc` drops 36 → 35 and
898    /// `blank` rises 6 → 7. No snapshot covers it — `snapshots/` holds
899    /// only the C-family files — so the integration suite is silent
900    /// about the one case that proves the fix reaches ordinary trees.
901    /// Do not read a green snapshot run as evidence of no effect.
902    ///
903    /// The clamp's one latent hazard is the mirror image: a unit whose
904    /// span is *shorter* than the file. That happens today for a file
905    /// ending in a blank row (`"package m\n\n"` reports `unit 1..1`),
906    /// and is harmless only because a trailing blank row reaches
907    /// neither line set. Nothing pins `unit.end_line >= line_count` —
908    /// `tests/parity/space_span_containment.rs` asserts
909    /// `unit == (1, line_count)`, but none of its fixtures ends in a
910    /// blank row — so a grammar that ever credited such a row to PLOC
911    /// would have it deleted here silently.
912    pub(crate) fn clamp_line_sets_to_span(&mut self) {
913        let start = self.sloc.start;
914        let span = span_rows(start, self.sloc.end_line);
915        // `end_line` is the 1-based inclusive last row of the span, so
916        // the last 0-based row it covers is `end_line - 1` — exact
917        // wherever the span is non-empty, which is what `span == 0`
918        // selects against. `(1, 0)` is the inverted range
919        // `LineSet::retain_range` reads as "keep nothing", and it is
920        // what the empty file's `0..0` needs: retaining `0..=0` there
921        // would keep a row the file does not have.
922        //
923        // That last branch is defensive rather than live. The three
924        // walks that reach `span == 0` (an empty file, and the two
925        // whitespace-only root contracts) all arrive with both line
926        // sets already empty, so `(0, 0)` here is byte-identical over
927        // every corpus input — measured. `a_zero_span_keeps_no_row`
928        // below is therefore a direct `Stats` test rather than a
929        // fixture, per `.claude/rules/testing.md`: it is the only shape
930        // that can tell the two spellings apart.
931        //
932        // The corpus measurement is the weaker half of that claim, and
933        // `line_set`'s module header retracts one of exactly this shape
934        // as having been wrong *and* load-bearing (#1398). The stronger
935        // half is structural: `span == 0` needs a space opened on a
936        // genuinely zero-width node at column 0, and every language's
937        // `is_func_space` matches compound productions — never a raw
938        // token, never `is_error` / `is_missing` — while a MISSING node
939        // is always a single terminal.
940        //
941        // Either way the absence is not what should hold the branch,
942        // because `retain_range(1, 0)` *clears* both line sets: whatever
943        // arrived is gone, and the parent never recovers it, since a
944        // child is clamped before `Ploc::merge` lifts it. That clear is
945        // also what makes the two assertions at the end of this function
946        // vacuous here — they compare against `span`, so both read
947        // `0 <= 0` no matter what was destroyed. Assert before the
948        // clear, where the question can still be answered.
949        let (first, last) = if span == 0 {
950            debug_assert!(
951                self.ploc() == 0 && self.cloc() == 0,
952                "zero-span space reached the clamp holding {} ploc / {} cloc \
953                 row(s); clearing them here would lose them silently",
954                self.ploc(),
955                self.cloc()
956            );
957            (1, 0)
958        } else {
959            (start, self.sloc.end_line.saturating_sub(1))
960        };
961        self.ploc.lines.retain_range(first, last);
962        self.cloc.only_comment_line_starts.retain_range(first, last);
963        self.cloc.code_comment_line_starts.retain_range(first, last);
964        // Retained with the two cloc sets it is a subset of.
965        self.dloc.lines.retain_range(first, last);
966        // The last set is the `exclude_tests`-pruned rows, and it is
967        // clamped for the same reason as the other four rather than
968        // because anything is known to put a row outside the span
969        // there: a pruned node lies inside the space that recorded it,
970        // so this is expected to be a no-op. What it buys is the
971        // premise `X ⊆ span` that makes `settle_excluded_rows`'s
972        // `ploc() <= sloc()` a theorem rather than an observation —
973        // without it a stray row would turn `sloc()` negative-by-
974        // saturation and fire an assertion in the wrong pass (#1417).
975        self.sloc.excluded_rows.retain_range(first, last);
976
977        // The invariant the clamp establishes, asserted on the path
978        // every walk takes for every space rather than only on the
979        // fixtures the regression tests name — 821 of the workspace's
980        // tests reach it.
981        //
982        // Against the span, deliberately, and kept that way now that
983        // [`Stats::settle_excluded_rows`] asserts the stronger
984        // `ploc() <= sloc()` form (#1417). The two say different
985        // things: this pair localises a failure to the clamp itself,
986        // where `sloc()`'s subtrahend is not yet settled and could not
987        // be blamed.
988        //
989        // `ploc()` and `cloc()` popcount their word arrays (#1109):
990        // O(words) per space, the same order as the `compute_minmax` that
991        // follows, where per node it would be the quadratic shape #1122
992        // removed. The message repeats the call, but an assertion's
993        // message arguments are evaluated only on the failing branch.
994        debug_assert!(
995            self.ploc() <= span as u64,
996            "ploc {} exceeds the {span} row span it was clamped to",
997            self.ploc()
998        );
999        debug_assert!(
1000            self.cloc() <= span as u64,
1001            "cloc {} exceeds the {span} row span it was clamped to",
1002            self.cloc()
1003        );
1004    }
1005
1006    /// Removes from the `exclude_tests`-pruned row set every row that
1007    /// retained code or comments also occupy, so `sloc()` subtracts only
1008    /// the rows the prune genuinely took (#1417).
1009    ///
1010    /// `Sloc::exclude_span` records a pruned subtree's whole span, and
1011    /// its first or last physical row can carry a retained sibling —
1012    /// `fn a() {} #[cfg(test)] mod t { … }` is one row that stays. The
1013    /// walk cannot know that when it prunes, because the retained rows
1014    /// of the enclosing space are still arriving; this pass runs once
1015    /// per space at finalization, when they are all in.
1016    ///
1017    /// All three retained sets are subtracted, but only two of them can
1018    /// move the answer, and that is worth stating rather than hedging:
1019    /// `code_comment_line_starts` is a subset of `ploc.lines` by
1020    /// construction. Its only writers are the two `add_cloc_lines` arms,
1021    /// both gated on `ploc.lines.contains(start)` already being true,
1022    /// and `check_comment_ends_on_code_line`, whose per-language call
1023    /// sites every one insert that same row into `ploc.lines` on the
1024    /// next statement. `Stats::merge` unions both sets and
1025    /// [`Stats::clamp_line_sets_to_span`] retains both over one range,
1026    /// so no later pass can separate them. Deleting the third
1027    /// subtraction therefore fails no test and *cannot* — measured at 0
1028    /// of 3,390 lib tests, where dropping either of the other two fails
1029    /// 3 and 1. It stays as defence against a future call site that
1030    /// forgets the PLOC insert, and the `debug_assert_eq!` opening the
1031    /// body is what keeps that defence honest: without it the line is
1032    /// unreachable weight whose loss nothing would ever notice.
1033    ///
1034    /// Ordered after [`Stats::clamp_line_sets_to_span`], which
1035    /// establishes the premise: with `P`, `O`, `C` and the pruned set
1036    /// `X` all inside the span `S`, the residual `X' = X \ (P ∪ O ∪ C)`
1037    /// is disjoint from `P ∪ O ∪ C`, so `|X'| + |P ∪ O ∪ C| <= |S|` and
1038    /// `sloc() = |S| - |X'| >= max(ploc(), cloc())`. That is what the
1039    /// assertions below pin — on every space of every walk, so they are
1040    /// the real evidence the fix holds across all twenty languages
1041    /// rather than only on the fixtures the regression tests name.
1042    ///
1043    /// [`Stats::blank`] keeps its `saturating_sub`: `P` and `O ∪ C` may
1044    /// overlap on a code-and-comment row, so no lower bound on
1045    /// `sloc - ploc - |O|` follows from the above.
1046    pub(crate) fn settle_excluded_rows(&mut self) {
1047        // The premise that makes the third subtraction below redundant,
1048        // pinned on every space of every walk because it is a property
1049        // of all the `check_comment_ends_on_code_line` call sites at
1050        // once and no single fixture can reach them.
1051        debug_assert_eq!(
1052            self.ploc
1053                .lines
1054                .union_len(&self.cloc.code_comment_line_starts),
1055            self.ploc.lines.len(),
1056            "a row carrying both code and a comment must also be a code row"
1057        );
1058
1059        // Disjoint field paths, so the shorthand borrows cleanly.
1060        let excluded = &mut self.sloc.excluded_rows;
1061        excluded.subtract(&self.ploc.lines);
1062        excluded.subtract(&self.cloc.only_comment_line_starts);
1063        excluded.subtract(&self.cloc.code_comment_line_starts);
1064
1065        debug_assert!(
1066            self.ploc() <= self.sloc(),
1067            "ploc {} exceeds sloc {} after settling",
1068            self.ploc(),
1069            self.sloc()
1070        );
1071        let cloc = self.cloc();
1072        debug_assert!(
1073            cloc <= self.sloc(),
1074            "cloc {} exceeds sloc {} after settling",
1075            cloc,
1076            self.sloc()
1077        );
1078        // Every documentation row is a cloc row (#1597) — `add_comment_rows`
1079        // bills both, `add_leading_doc_run` re-bills billed comment rows —
1080        // and MI's subtractions rely on it. Compared as sets, not counts: a
1081        // stray row would grow the union while leaving `dloc() <= cloc()`.
1082        #[cfg(debug_assertions)]
1083        {
1084            let mut comment_rows = self.cloc.only_comment_line_starts.clone();
1085            comment_rows.union_with(&self.cloc.code_comment_line_starts);
1086            debug_assert_eq!(
1087                comment_rows.union_len(&self.dloc.lines),
1088                comment_rows.len(),
1089                "a dloc row outside the cloc sets after settling (dloc {}, cloc {})",
1090                self.dloc(),
1091                cloc
1092            );
1093        }
1094    }
1095}
1096
1097#[doc(hidden)]
1098/// Per-language computation of the lines-of-code metrics.
1099pub(crate) trait Loc
1100where
1101    Self: Checker,
1102{
1103    /// Walk `node` and update `stats` with this metric for the language
1104    /// implementing the trait.
1105    /// `code` is the source, for the arms that classify a node by its
1106    /// bytes — a doc comment is told from a plain one by its opening
1107    /// marker in most grammars (#1597). `ancestors` is the chain the
1108    /// walker descended through: the C-family and JVM-family arms read
1109    /// it to tell a declaration in a loop header from one in the loop
1110    /// body (#1084).
1111    fn compute<'a>(
1112        node: &Node<'a>,
1113        code: &[u8],
1114        ancestors: Ancestors<'a, '_>,
1115        stats: &mut Stats,
1116        is_func_space: bool,
1117    );
1118}
1119
1120mod shared;
1121pub(crate) use shared::*;
1122
1123// Real defaults — Loc counts on these "languages" would conflate
1124// comments / preproc directives with executable code; treating them
1125// as 0 is the documented behaviour. Audited in #188.
1126implement_metric_trait!(Loc, PreprocCode, CcommentCode);
1127
1128mod bash;
1129mod c;
1130mod cpp;
1131mod csharp;
1132mod elixir;
1133mod go;
1134mod groovy;
1135mod irules;
1136mod java;
1137mod javascript;
1138mod kotlin;
1139mod lua;
1140mod mozcpp;
1141mod mozjs;
1142mod objc;
1143mod perl;
1144mod php;
1145mod python;
1146mod ruby;
1147mod rust;
1148mod tcl;
1149mod tsx;
1150mod typescript;
1151
1152#[cfg(test)]
1153#[allow(
1154    clippy::float_cmp,
1155    clippy::cast_precision_loss,
1156    clippy::cast_possible_truncation,
1157    clippy::cast_sign_loss,
1158    clippy::similar_names,
1159    clippy::doc_markdown,
1160    clippy::needless_raw_string_hashes,
1161    clippy::too_many_lines
1162)]
1163mod tests {
1164    use crate::test_support::{check_metrics_only_shim, metrics_verbatim, space_verbatim};
1165
1166    use super::*;
1167
1168    check_metrics_only_shim!(check_metrics, Loc);
1169
1170    /// A `Stats::default()` that never sees an observation must not leak
1171    /// the `usize::MAX` sentinel for any of the LOC `_min` accumulators
1172    /// (`sloc_min`, `ploc_min`, `lloc_min`, `cloc_min`, `blank_min`,
1173    /// `dloc_min`).
1174    /// The getters collapse the sentinel to `0.0` so JSON never emits
1175    /// `1.8446744e19`.
1176    #[test]
1177    fn loc_empty_file_min_is_zero() {
1178        let stats = Stats::default();
1179        assert_eq!(stats.sloc_min(), 0);
1180        assert_eq!(stats.ploc_min(), 0);
1181        assert_eq!(stats.lloc_min(), 0);
1182        assert_eq!(stats.cloc_min(), 0);
1183        assert_eq!(stats.blank_min(), 0);
1184        assert_eq!(stats.dloc_min(), 0);
1185    }
1186
1187    /// Parses `source` with `PerlParser` and asserts the resulting tree has
1188    /// no `ERROR` nodes. Use alongside metric assertions whose expected
1189    /// values would happen to match what an error tree produces — a parse
1190    /// regression in tree-sitter-perl could otherwise leave such tests
1191    /// silently green.
1192    #[cfg(feature = "perl")]
1193    #[cfg(test)]
1194    fn assert_perl_parses_cleanly(source: &str) {
1195        use crate::traits::ParserTrait;
1196        // Mirror the trailing-newline normalisation `check_func_space` does
1197        // before handing input to the parser, so this helper sees the same
1198        // bytes the metric tests do.
1199        let path = std::path::PathBuf::from("foo.pl");
1200        let mut bytes = source.trim_end_matches('\n').as_bytes().to_vec();
1201        bytes.push(b'\n');
1202        let parser = PerlParser::new(bytes, &path, None);
1203        assert!(
1204            !parser.root().has_error(),
1205            "tree-sitter-perl returned an error tree for snippet:\n{source}"
1206        );
1207    }
1208
1209    #[cfg(feature = "python")]
1210    #[test]
1211    fn python_sloc() {
1212        check_metrics::<PythonParser>(
1213            "
1214
1215            a = 42
1216
1217            ",
1218            "foo.py",
1219            |metric| {
1220                // Spaces: 1
1221                insta::assert_json_snapshot!(
1222                    metric.loc,
1223                    @r#"
1224                {
1225                  "sloc": 1,
1226                  "ploc": 1,
1227                  "lloc": 1,
1228                  "cloc": 0,
1229                  "blank": 0,
1230                  "dloc": 0,
1231                  "sloc_average": 1.0,
1232                  "ploc_average": 1.0,
1233                  "lloc_average": 1.0,
1234                  "cloc_average": 0.0,
1235                  "blank_average": 0.0,
1236                  "dloc_average": 0.0,
1237                  "sloc_min": 1,
1238                  "sloc_max": 1,
1239                  "cloc_min": 0,
1240                  "cloc_max": 0,
1241                  "ploc_min": 1,
1242                  "ploc_max": 1,
1243                  "lloc_min": 1,
1244                  "lloc_max": 1,
1245                  "blank_min": 0,
1246                  "blank_max": 0,
1247                  "dloc_min": 0,
1248                  "dloc_max": 0
1249                }
1250                "#
1251                );
1252            },
1253        );
1254    }
1255
1256    #[cfg(feature = "python")]
1257    #[test]
1258    fn python_blank() {
1259        check_metrics::<PythonParser>(
1260            "
1261            a = 42
1262
1263            b = 43
1264
1265            ",
1266            "foo.py",
1267            |metric| {
1268                // Spaces: 1
1269                insta::assert_json_snapshot!(
1270                    metric.loc,
1271                    @r#"
1272                {
1273                  "sloc": 3,
1274                  "ploc": 2,
1275                  "lloc": 2,
1276                  "cloc": 0,
1277                  "blank": 1,
1278                  "dloc": 0,
1279                  "sloc_average": 3.0,
1280                  "ploc_average": 2.0,
1281                  "lloc_average": 2.0,
1282                  "cloc_average": 0.0,
1283                  "blank_average": 1.0,
1284                  "dloc_average": 0.0,
1285                  "sloc_min": 3,
1286                  "sloc_max": 3,
1287                  "cloc_min": 0,
1288                  "cloc_max": 0,
1289                  "ploc_min": 2,
1290                  "ploc_max": 2,
1291                  "lloc_min": 2,
1292                  "lloc_max": 2,
1293                  "blank_min": 1,
1294                  "blank_max": 1,
1295                  "dloc_min": 0,
1296                  "dloc_max": 0
1297                }
1298                "#
1299                );
1300            },
1301        );
1302    }
1303
1304    #[cfg(feature = "rust")]
1305    #[test]
1306    fn rust_blank() {
1307        check_metrics::<RustParser>(
1308            "
1309
1310            let a = 42;
1311
1312            let b = 43;
1313
1314            ",
1315            "foo.rs",
1316            |metric| {
1317                // Spaces: 1
1318                insta::assert_json_snapshot!(
1319                    metric.loc,
1320                    @r#"
1321                {
1322                  "sloc": 3,
1323                  "ploc": 2,
1324                  "lloc": 2,
1325                  "cloc": 0,
1326                  "blank": 1,
1327                  "dloc": 0,
1328                  "sloc_average": 3.0,
1329                  "ploc_average": 2.0,
1330                  "lloc_average": 2.0,
1331                  "cloc_average": 0.0,
1332                  "blank_average": 1.0,
1333                  "dloc_average": 0.0,
1334                  "sloc_min": 3,
1335                  "sloc_max": 3,
1336                  "cloc_min": 0,
1337                  "cloc_max": 0,
1338                  "ploc_min": 2,
1339                  "ploc_max": 2,
1340                  "lloc_min": 2,
1341                  "lloc_max": 2,
1342                  "blank_min": 1,
1343                  "blank_max": 1,
1344                  "dloc_min": 0,
1345                  "dloc_max": 0
1346                }
1347                "#
1348                );
1349            },
1350        );
1351
1352        check_metrics::<RustParser>("fn func() { /* comment */ }", "foo.rs", |metric| {
1353            // Spaces: 2
1354            insta::assert_json_snapshot!(
1355                metric.loc,
1356                @r#"
1357            {
1358              "sloc": 1,
1359              "ploc": 1,
1360              "lloc": 0,
1361              "cloc": 1,
1362              "blank": 0,
1363              "dloc": 0,
1364              "sloc_average": 0.5,
1365              "ploc_average": 0.5,
1366              "lloc_average": 0.0,
1367              "cloc_average": 0.5,
1368              "blank_average": 0.0,
1369              "dloc_average": 0.0,
1370              "sloc_min": 1,
1371              "sloc_max": 1,
1372              "cloc_min": 1,
1373              "cloc_max": 1,
1374              "ploc_min": 1,
1375              "ploc_max": 1,
1376              "lloc_min": 0,
1377              "lloc_max": 0,
1378              "blank_min": 0,
1379              "blank_max": 0,
1380              "dloc_min": 0,
1381              "dloc_max": 0
1382            }
1383            "#
1384            );
1385        });
1386    }
1387
1388    #[cfg(feature = "c")]
1389    #[test]
1390    fn c_blank() {
1391        check_metrics::<CParser>(
1392            "
1393
1394            int a = 42;
1395
1396            int b = 43;
1397
1398            ",
1399            "foo.c",
1400            |metric| {
1401                // Spaces: 1
1402                insta::assert_json_snapshot!(
1403                    metric.loc,
1404                    @r#"
1405                {
1406                  "sloc": 3,
1407                  "ploc": 2,
1408                  "lloc": 2,
1409                  "cloc": 0,
1410                  "blank": 1,
1411                  "dloc": 0,
1412                  "sloc_average": 3.0,
1413                  "ploc_average": 2.0,
1414                  "lloc_average": 2.0,
1415                  "cloc_average": 0.0,
1416                  "blank_average": 1.0,
1417                  "dloc_average": 0.0,
1418                  "sloc_min": 3,
1419                  "sloc_max": 3,
1420                  "cloc_min": 0,
1421                  "cloc_max": 0,
1422                  "ploc_min": 2,
1423                  "ploc_max": 2,
1424                  "lloc_min": 2,
1425                  "lloc_max": 2,
1426                  "blank_min": 1,
1427                  "blank_max": 1,
1428                  "dloc_min": 0,
1429                  "dloc_max": 0
1430                }
1431                "#
1432                );
1433            },
1434        );
1435    }
1436
1437    #[cfg(feature = "python")]
1438    #[test]
1439    fn python_no_zero_blank() {
1440        // Checks that the blank metric is not equal to 0 when there are some
1441        // comments next to code lines.
1442        check_metrics::<PythonParser>(
1443            "def ConnectToUpdateServer():
1444                 pool = 4
1445
1446                 updateServer = -42
1447                 isConnected = False
1448                 currTry = 0
1449                 numRetries = 10 # Number of IPC connection retries before
1450                                 # giving up.
1451                 numTries = 20 # Number of IPC connection tries before
1452                               # giving up.",
1453            "foo.py",
1454            |metric| {
1455                // Spaces: 2
1456                insta::assert_json_snapshot!(
1457                    metric.loc,
1458                    @r#"
1459                {
1460                  "sloc": 10,
1461                  "ploc": 7,
1462                  "lloc": 6,
1463                  "cloc": 4,
1464                  "blank": 1,
1465                  "dloc": 0,
1466                  "sloc_average": 5.0,
1467                  "ploc_average": 3.5,
1468                  "lloc_average": 3.0,
1469                  "cloc_average": 2.0,
1470                  "blank_average": 0.5,
1471                  "dloc_average": 0.0,
1472                  "sloc_min": 10,
1473                  "sloc_max": 10,
1474                  "cloc_min": 4,
1475                  "cloc_max": 4,
1476                  "ploc_min": 7,
1477                  "ploc_max": 7,
1478                  "lloc_min": 6,
1479                  "lloc_max": 6,
1480                  "blank_min": 1,
1481                  "blank_max": 1,
1482                  "dloc_min": 0,
1483                  "dloc_max": 0
1484                }
1485                "#
1486                );
1487            },
1488        );
1489    }
1490
1491    #[cfg(feature = "python")]
1492    #[test]
1493    fn python_no_blank() {
1494        // Checks that the blank metric is equal to 0 when there are no blank
1495        // lines and there are comments next to code lines.
1496        check_metrics::<PythonParser>(
1497            "def ConnectToUpdateServer():
1498                 pool = 4
1499                 updateServer = -42
1500                 isConnected = False
1501                 currTry = 0
1502                 numRetries = 10 # Number of IPC connection retries before
1503                                 # giving up.
1504                 numTries = 20 # Number of IPC connection tries before
1505                               # giving up.",
1506            "foo.py",
1507            |metric| {
1508                // Spaces: 2
1509                insta::assert_json_snapshot!(
1510                    metric.loc,
1511                    @r#"
1512                {
1513                  "sloc": 9,
1514                  "ploc": 7,
1515                  "lloc": 6,
1516                  "cloc": 4,
1517                  "blank": 0,
1518                  "dloc": 0,
1519                  "sloc_average": 4.5,
1520                  "ploc_average": 3.5,
1521                  "lloc_average": 3.0,
1522                  "cloc_average": 2.0,
1523                  "blank_average": 0.0,
1524                  "dloc_average": 0.0,
1525                  "sloc_min": 9,
1526                  "sloc_max": 9,
1527                  "cloc_min": 4,
1528                  "cloc_max": 4,
1529                  "ploc_min": 7,
1530                  "ploc_max": 7,
1531                  "lloc_min": 6,
1532                  "lloc_max": 6,
1533                  "blank_min": 0,
1534                  "blank_max": 0,
1535                  "dloc_min": 0,
1536                  "dloc_max": 0
1537                }
1538                "#
1539                );
1540            },
1541        );
1542    }
1543
1544    #[cfg(feature = "python")]
1545    #[test]
1546    fn python_no_zero_blank_more_comments() {
1547        // Checks that the blank metric is not equal to 0 when there are more
1548        // comments next to code lines compared to the previous tests.
1549        check_metrics::<PythonParser>(
1550            "def ConnectToUpdateServer():
1551                 pool = 4
1552
1553                 updateServer = -42
1554                 isConnected = False
1555                 currTry = 0 # Set this variable to 0
1556                 numRetries = 10 # Number of IPC connection retries before
1557                                 # giving up.
1558                 numTries = 20 # Number of IPC connection tries before
1559                               # giving up.",
1560            "foo.py",
1561            |metric| {
1562                // Spaces: 2
1563                insta::assert_json_snapshot!(
1564                    metric.loc,
1565                    @r#"
1566                {
1567                  "sloc": 10,
1568                  "ploc": 7,
1569                  "lloc": 6,
1570                  "cloc": 5,
1571                  "blank": 1,
1572                  "dloc": 0,
1573                  "sloc_average": 5.0,
1574                  "ploc_average": 3.5,
1575                  "lloc_average": 3.0,
1576                  "cloc_average": 2.5,
1577                  "blank_average": 0.5,
1578                  "dloc_average": 0.0,
1579                  "sloc_min": 10,
1580                  "sloc_max": 10,
1581                  "cloc_min": 5,
1582                  "cloc_max": 5,
1583                  "ploc_min": 7,
1584                  "ploc_max": 7,
1585                  "lloc_min": 6,
1586                  "lloc_max": 6,
1587                  "blank_min": 1,
1588                  "blank_max": 1,
1589                  "dloc_min": 0,
1590                  "dloc_max": 0
1591                }
1592                "#
1593                );
1594            },
1595        );
1596    }
1597
1598    #[cfg(feature = "rust")]
1599    #[test]
1600    fn rust_no_zero_blank() {
1601        // Checks that the blank metric is not equal to 0 when there are some
1602        // comments next to code lines.
1603        check_metrics::<RustParser>(
1604            "fn ConnectToUpdateServer() {
1605              let pool = 0;
1606
1607              let updateServer = -42;
1608              let isConnected = false;
1609              let currTry = 0;
1610              let numRetries = 10;  // Number of IPC connection retries before
1611                                    // giving up.
1612              let numTries = 20;    // Number of IPC connection tries before
1613                                    // giving up.
1614            }",
1615            "foo.rs",
1616            |metric| {
1617                // Spaces: 2
1618                insta::assert_json_snapshot!(
1619                    metric.loc,
1620                    @r#"
1621                {
1622                  "sloc": 11,
1623                  "ploc": 8,
1624                  "lloc": 6,
1625                  "cloc": 4,
1626                  "blank": 1,
1627                  "dloc": 0,
1628                  "sloc_average": 5.5,
1629                  "ploc_average": 4.0,
1630                  "lloc_average": 3.0,
1631                  "cloc_average": 2.0,
1632                  "blank_average": 0.5,
1633                  "dloc_average": 0.0,
1634                  "sloc_min": 11,
1635                  "sloc_max": 11,
1636                  "cloc_min": 4,
1637                  "cloc_max": 4,
1638                  "ploc_min": 8,
1639                  "ploc_max": 8,
1640                  "lloc_min": 6,
1641                  "lloc_max": 6,
1642                  "blank_min": 1,
1643                  "blank_max": 1,
1644                  "dloc_min": 0,
1645                  "dloc_max": 0
1646                }
1647                "#
1648                );
1649            },
1650        );
1651    }
1652
1653    #[cfg(feature = "javascript")]
1654    #[test]
1655    fn javascript_no_zero_blank() {
1656        // Checks that the blank metric is not equal to 0 when there are some
1657        // comments next to code lines.
1658        check_metrics::<JavascriptParser>(
1659            "function ConnectToUpdateServer() {
1660              var pool = 0;
1661
1662              var updateServer = -42;
1663              var isConnected = false;
1664              var currTry = 0;
1665              var numRetries = 10;  // Number of IPC connection retries before
1666                                    // giving up.
1667              var numTries = 20;    // Number of IPC connection tries before
1668                                    // giving up.
1669            }",
1670            "foo.js",
1671            |metric| {
1672                // Spaces: 2
1673                insta::assert_json_snapshot!(
1674                    metric.loc,
1675                    @r#"
1676                {
1677                  "sloc": 11,
1678                  "ploc": 8,
1679                  "lloc": 6,
1680                  "cloc": 4,
1681                  "blank": 1,
1682                  "dloc": 0,
1683                  "sloc_average": 5.5,
1684                  "ploc_average": 4.0,
1685                  "lloc_average": 3.0,
1686                  "cloc_average": 2.0,
1687                  "blank_average": 0.5,
1688                  "dloc_average": 0.0,
1689                  "sloc_min": 11,
1690                  "sloc_max": 11,
1691                  "cloc_min": 4,
1692                  "cloc_max": 4,
1693                  "ploc_min": 8,
1694                  "ploc_max": 8,
1695                  "lloc_min": 6,
1696                  "lloc_max": 6,
1697                  "blank_min": 1,
1698                  "blank_max": 1,
1699                  "dloc_min": 0,
1700                  "dloc_max": 0
1701                }
1702                "#
1703                );
1704            },
1705        );
1706    }
1707
1708    #[cfg(feature = "cpp")]
1709    #[test]
1710    fn cpp_no_zero_blank() {
1711        // Checks that the blank metric is not equal to 0 when there are some
1712        // comments next to code lines.
1713        check_metrics::<CppParser>(
1714            "void ConnectToUpdateServer() {
1715              int pool;
1716
1717              int updateServer = -42;
1718              bool isConnected = false;
1719              int currTry = 0;
1720              const int numRetries = 10; // Number of IPC connection retries before
1721                                         // giving up.
1722              const int numTries = 20; // Number of IPC connection tries before
1723                                       // giving up.
1724            }",
1725            "foo.cpp",
1726            |metric| {
1727                // Spaces: 2
1728                insta::assert_json_snapshot!(
1729                    metric.loc,
1730                    @r#"
1731                {
1732                  "sloc": 11,
1733                  "ploc": 8,
1734                  "lloc": 6,
1735                  "cloc": 4,
1736                  "blank": 1,
1737                  "dloc": 0,
1738                  "sloc_average": 5.5,
1739                  "ploc_average": 4.0,
1740                  "lloc_average": 3.0,
1741                  "cloc_average": 2.0,
1742                  "blank_average": 0.5,
1743                  "dloc_average": 0.0,
1744                  "sloc_min": 11,
1745                  "sloc_max": 11,
1746                  "cloc_min": 4,
1747                  "cloc_max": 4,
1748                  "ploc_min": 8,
1749                  "ploc_max": 8,
1750                  "lloc_min": 6,
1751                  "lloc_max": 6,
1752                  "blank_min": 1,
1753                  "blank_max": 1,
1754                  "dloc_min": 0,
1755                  "dloc_max": 0
1756                }
1757                "#
1758                );
1759            },
1760        );
1761    }
1762
1763    #[cfg(feature = "cpp")]
1764    #[test]
1765    fn cpp_code_line_start_block_blank() {
1766        // Checks that the blank metric is equal to 1 when there are
1767        // block comments starting next to code lines.
1768        check_metrics::<CppParser>(
1769            "void ConnectToUpdateServer() {
1770              int pool;
1771
1772              int updateServer = -42;
1773              bool isConnected = false;
1774              int currTry = 0;
1775              const int numRetries = 10; /* Number of IPC connection retries
1776              before
1777              giving up. */
1778              const int numTries = 20; // Number of IPC connection tries before
1779                                       // giving up.
1780            }",
1781            "foo.cpp",
1782            |metric| {
1783                // Spaces: 2
1784                insta::assert_json_snapshot!(
1785                    metric.loc,
1786                    @r#"
1787                {
1788                  "sloc": 12,
1789                  "ploc": 8,
1790                  "lloc": 6,
1791                  "cloc": 5,
1792                  "blank": 1,
1793                  "dloc": 0,
1794                  "sloc_average": 6.0,
1795                  "ploc_average": 4.0,
1796                  "lloc_average": 3.0,
1797                  "cloc_average": 2.5,
1798                  "blank_average": 0.5,
1799                  "dloc_average": 0.0,
1800                  "sloc_min": 12,
1801                  "sloc_max": 12,
1802                  "cloc_min": 5,
1803                  "cloc_max": 5,
1804                  "ploc_min": 8,
1805                  "ploc_max": 8,
1806                  "lloc_min": 6,
1807                  "lloc_max": 6,
1808                  "blank_min": 1,
1809                  "blank_max": 1,
1810                  "dloc_min": 0,
1811                  "dloc_max": 0
1812                }
1813                "#
1814                );
1815            },
1816        );
1817    }
1818
1819    #[cfg(feature = "cpp")]
1820    #[test]
1821    fn cpp_block_comment_blank() {
1822        // Checks that the blank metric is equal to 1 when there are
1823        // block comments on independent lines.
1824        check_metrics::<CppParser>(
1825            "void ConnectToUpdateServer() {
1826              int pool;
1827
1828              int updateServer = -42;
1829              bool isConnected = false;
1830              int currTry = 0;
1831              /* Number of IPC connection retries
1832              before
1833              giving up. */
1834              const int numRetries = 10;
1835              const int numTries = 20; // Number of IPC connection tries before
1836                                       // giving up.
1837            }",
1838            "foo.cpp",
1839            |metric| {
1840                // Spaces: 2
1841                insta::assert_json_snapshot!(
1842                    metric.loc,
1843                    @r#"
1844                {
1845                  "sloc": 13,
1846                  "ploc": 8,
1847                  "lloc": 6,
1848                  "cloc": 5,
1849                  "blank": 1,
1850                  "dloc": 0,
1851                  "sloc_average": 6.5,
1852                  "ploc_average": 4.0,
1853                  "lloc_average": 3.0,
1854                  "cloc_average": 2.5,
1855                  "blank_average": 0.5,
1856                  "dloc_average": 0.0,
1857                  "sloc_min": 13,
1858                  "sloc_max": 13,
1859                  "cloc_min": 5,
1860                  "cloc_max": 5,
1861                  "ploc_min": 8,
1862                  "ploc_max": 8,
1863                  "lloc_min": 6,
1864                  "lloc_max": 6,
1865                  "blank_min": 1,
1866                  "blank_max": 1,
1867                  "dloc_min": 0,
1868                  "dloc_max": 0
1869                }
1870                "#
1871                );
1872            },
1873        );
1874    }
1875
1876    #[cfg(feature = "cpp")]
1877    #[test]
1878    fn cpp_code_line_block_one_line_blank() {
1879        // Checks that the blank metric is equal to 1 when there are
1880        // block comments before the same code line.
1881        check_metrics::<CppParser>(
1882            "void ConnectToUpdateServer() {
1883              int pool;
1884
1885              int updateServer = -42;
1886              bool isConnected = false;
1887              int currTry = 0;
1888              /* Number of IPC connection retries before giving up. */ const int numRetries = 10;
1889              const int numTries = 20; // Number of IPC connection tries before
1890                                       // giving up.
1891            }",
1892            "foo.cpp",
1893            |metric| {
1894                // Spaces: 2
1895                insta::assert_json_snapshot!(
1896                    metric.loc,
1897                    @r#"
1898                {
1899                  "sloc": 10,
1900                  "ploc": 8,
1901                  "lloc": 6,
1902                  "cloc": 3,
1903                  "blank": 1,
1904                  "dloc": 0,
1905                  "sloc_average": 5.0,
1906                  "ploc_average": 4.0,
1907                  "lloc_average": 3.0,
1908                  "cloc_average": 1.5,
1909                  "blank_average": 0.5,
1910                  "dloc_average": 0.0,
1911                  "sloc_min": 10,
1912                  "sloc_max": 10,
1913                  "cloc_min": 3,
1914                  "cloc_max": 3,
1915                  "ploc_min": 8,
1916                  "ploc_max": 8,
1917                  "lloc_min": 6,
1918                  "lloc_max": 6,
1919                  "blank_min": 1,
1920                  "blank_max": 1,
1921                  "dloc_min": 0,
1922                  "dloc_max": 0
1923                }
1924                "#
1925                );
1926            },
1927        );
1928    }
1929
1930    #[cfg(feature = "cpp")]
1931    #[test]
1932    fn cpp_code_line_end_block_blank() {
1933        // Checks that the blank metric is equal to 1 when there are
1934        // block comments ending next to code lines.
1935        check_metrics::<CppParser>(
1936            "void ConnectToUpdateServer() {
1937              int pool;
1938
1939              int updateServer = -42;
1940              bool isConnected = false;
1941              int currTry = 0;
1942              /* Number of IPC connection retries
1943              before
1944              giving up. */ const int numRetries = 10;
1945              const int numTries = 20; // Number of IPC connection tries before
1946                                       // giving up.
1947            }",
1948            "foo.cpp",
1949            |metric| {
1950                // Spaces: 2
1951                insta::assert_json_snapshot!(
1952                    metric.loc,
1953                    @r#"
1954                {
1955                  "sloc": 12,
1956                  "ploc": 8,
1957                  "lloc": 6,
1958                  "cloc": 5,
1959                  "blank": 1,
1960                  "dloc": 0,
1961                  "sloc_average": 6.0,
1962                  "ploc_average": 4.0,
1963                  "lloc_average": 3.0,
1964                  "cloc_average": 2.5,
1965                  "blank_average": 0.5,
1966                  "dloc_average": 0.0,
1967                  "sloc_min": 12,
1968                  "sloc_max": 12,
1969                  "cloc_min": 5,
1970                  "cloc_max": 5,
1971                  "ploc_min": 8,
1972                  "ploc_max": 8,
1973                  "lloc_min": 6,
1974                  "lloc_max": 6,
1975                  "blank_min": 1,
1976                  "blank_max": 1,
1977                  "dloc_min": 0,
1978                  "dloc_max": 0
1979                }
1980                "#
1981                );
1982            },
1983        );
1984    }
1985
1986    #[cfg(feature = "python")]
1987    #[test]
1988    fn python_cloc() {
1989        check_metrics::<PythonParser>(
1990            "\"\"\"Block comment
1991            Block comment
1992            \"\"\"
1993            # Line Comment
1994            a = 42 # Line Comment",
1995            "foo.py",
1996            |metric| {
1997                // Spaces: 1
1998                insta::assert_json_snapshot!(
1999                    metric.loc,
2000                    @r#"
2001                {
2002                  "sloc": 5,
2003                  "ploc": 1,
2004                  "lloc": 2,
2005                  "cloc": 5,
2006                  "blank": 0,
2007                  "dloc": 3,
2008                  "sloc_average": 5.0,
2009                  "ploc_average": 1.0,
2010                  "lloc_average": 2.0,
2011                  "cloc_average": 5.0,
2012                  "blank_average": 0.0,
2013                  "dloc_average": 3.0,
2014                  "sloc_min": 5,
2015                  "sloc_max": 5,
2016                  "cloc_min": 5,
2017                  "cloc_max": 5,
2018                  "ploc_min": 1,
2019                  "ploc_max": 1,
2020                  "lloc_min": 2,
2021                  "lloc_max": 2,
2022                  "blank_min": 0,
2023                  "blank_max": 0,
2024                  "dloc_min": 3,
2025                  "dloc_max": 3
2026                }
2027                "#
2028                );
2029            },
2030        );
2031    }
2032
2033    #[cfg(feature = "rust")]
2034    #[test]
2035    fn rust_cloc() {
2036        check_metrics::<RustParser>(
2037            "/*Block comment
2038            Block Comment*/
2039            //Line Comment
2040            /*Block Comment*/ let a = 42; // Line Comment",
2041            "foo.rs",
2042            |metric| {
2043                // Spaces: 1
2044                // expected: cloc = 4 — the 2-line block (lines 1-2) and
2045                // the standalone `//Line Comment` (line 3) give 3
2046                // only-comment lines; line 4 carries a leading block
2047                // comment AND a trailing line comment but is one
2048                // physical code line, so it adds a single code-comment
2049                // line, not two (issue #461). Pre-fix this reported 5,
2050                // violating cloc <= sloc (sloc = 4).
2051                insta::assert_json_snapshot!(
2052                    metric.loc,
2053                    @r#"
2054                {
2055                  "sloc": 4,
2056                  "ploc": 1,
2057                  "lloc": 1,
2058                  "cloc": 4,
2059                  "blank": 0,
2060                  "dloc": 0,
2061                  "sloc_average": 4.0,
2062                  "ploc_average": 1.0,
2063                  "lloc_average": 1.0,
2064                  "cloc_average": 4.0,
2065                  "blank_average": 0.0,
2066                  "dloc_average": 0.0,
2067                  "sloc_min": 4,
2068                  "sloc_max": 4,
2069                  "cloc_min": 4,
2070                  "cloc_max": 4,
2071                  "ploc_min": 1,
2072                  "ploc_max": 1,
2073                  "lloc_min": 1,
2074                  "lloc_max": 1,
2075                  "blank_min": 0,
2076                  "blank_max": 0,
2077                  "dloc_min": 0,
2078                  "dloc_max": 0
2079                }
2080                "#
2081                );
2082            },
2083        );
2084    }
2085
2086    #[cfg(feature = "c")]
2087    #[test]
2088    fn c_cloc() {
2089        check_metrics::<CParser>(
2090            "/*Block comment
2091            Block Comment*/
2092            //Line Comment
2093            /*Block Comment*/ int a = 42; // Line Comment",
2094            "foo.c",
2095            |metric| {
2096                // Spaces: 1
2097                // expected: cloc = 4 — see `rust_cloc`; line 4's leading
2098                // block comment and trailing line comment share one
2099                // physical code line and add a single code-comment line
2100                // (issue #461). Pre-fix reported 5 (cloc > sloc = 4).
2101                insta::assert_json_snapshot!(
2102                    metric.loc,
2103                    @r#"
2104                {
2105                  "sloc": 4,
2106                  "ploc": 1,
2107                  "lloc": 1,
2108                  "cloc": 4,
2109                  "blank": 0,
2110                  "dloc": 0,
2111                  "sloc_average": 4.0,
2112                  "ploc_average": 1.0,
2113                  "lloc_average": 1.0,
2114                  "cloc_average": 4.0,
2115                  "blank_average": 0.0,
2116                  "dloc_average": 0.0,
2117                  "sloc_min": 4,
2118                  "sloc_max": 4,
2119                  "cloc_min": 4,
2120                  "cloc_max": 4,
2121                  "ploc_min": 1,
2122                  "ploc_max": 1,
2123                  "lloc_min": 1,
2124                  "lloc_max": 1,
2125                  "blank_min": 0,
2126                  "blank_max": 0,
2127                  "dloc_min": 0,
2128                  "dloc_max": 0
2129                }
2130                "#
2131                );
2132            },
2133        );
2134    }
2135
2136    #[cfg(feature = "python")]
2137    #[test]
2138    fn python_lloc() {
2139        check_metrics::<PythonParser>(
2140            "for x in range(0,42):
2141                if x % 2 == 0:
2142                    print(x)",
2143            "foo.py",
2144            |metric| {
2145                // Spaces: 1
2146                insta::assert_json_snapshot!(
2147                    metric.loc,
2148                    @r#"
2149                {
2150                  "sloc": 3,
2151                  "ploc": 3,
2152                  "lloc": 3,
2153                  "cloc": 0,
2154                  "blank": 0,
2155                  "dloc": 0,
2156                  "sloc_average": 3.0,
2157                  "ploc_average": 3.0,
2158                  "lloc_average": 3.0,
2159                  "cloc_average": 0.0,
2160                  "blank_average": 0.0,
2161                  "dloc_average": 0.0,
2162                  "sloc_min": 3,
2163                  "sloc_max": 3,
2164                  "cloc_min": 0,
2165                  "cloc_max": 0,
2166                  "ploc_min": 3,
2167                  "ploc_max": 3,
2168                  "lloc_min": 3,
2169                  "lloc_max": 3,
2170                  "blank_min": 0,
2171                  "blank_max": 0,
2172                  "dloc_min": 0,
2173                  "dloc_max": 0
2174                }
2175                "#
2176                );
2177            },
2178        );
2179    }
2180
2181    #[cfg(feature = "rust")]
2182    #[test]
2183    fn rust_lloc() {
2184        check_metrics::<RustParser>(
2185            "for x in 0..42 {
2186                if x % 2 == 0 {
2187                    println!(\"{}\", x);
2188                }
2189             }",
2190            "foo.rs",
2191            |metric| {
2192                // Spaces: 1
2193                insta::assert_json_snapshot!(
2194                    metric.loc,
2195                    @r#"
2196                {
2197                  "sloc": 5,
2198                  "ploc": 5,
2199                  "lloc": 3,
2200                  "cloc": 0,
2201                  "blank": 0,
2202                  "dloc": 0,
2203                  "sloc_average": 5.0,
2204                  "ploc_average": 5.0,
2205                  "lloc_average": 3.0,
2206                  "cloc_average": 0.0,
2207                  "blank_average": 0.0,
2208                  "dloc_average": 0.0,
2209                  "sloc_min": 5,
2210                  "sloc_max": 5,
2211                  "cloc_min": 0,
2212                  "cloc_max": 0,
2213                  "ploc_min": 5,
2214                  "ploc_max": 5,
2215                  "lloc_min": 3,
2216                  "lloc_max": 3,
2217                  "blank_min": 0,
2218                  "blank_max": 0,
2219                  "dloc_min": 0,
2220                  "dloc_max": 0
2221                }
2222                "#
2223                );
2224            },
2225        );
2226
2227        // LLOC returns three because there is an empty Rust statement
2228        check_metrics::<RustParser>(
2229            "let a = 42;
2230             if true {
2231                42
2232             } else {
2233                43
2234             };",
2235            "foo.rs",
2236            |metric| {
2237                // Spaces: 1
2238                insta::assert_json_snapshot!(
2239                    metric.loc,
2240                    @r#"
2241                {
2242                  "sloc": 6,
2243                  "ploc": 6,
2244                  "lloc": 3,
2245                  "cloc": 0,
2246                  "blank": 0,
2247                  "dloc": 0,
2248                  "sloc_average": 6.0,
2249                  "ploc_average": 6.0,
2250                  "lloc_average": 3.0,
2251                  "cloc_average": 0.0,
2252                  "blank_average": 0.0,
2253                  "dloc_average": 0.0,
2254                  "sloc_min": 6,
2255                  "sloc_max": 6,
2256                  "cloc_min": 0,
2257                  "cloc_max": 0,
2258                  "ploc_min": 6,
2259                  "ploc_max": 6,
2260                  "lloc_min": 3,
2261                  "lloc_max": 3,
2262                  "blank_min": 0,
2263                  "blank_max": 0,
2264                  "dloc_min": 0,
2265                  "dloc_max": 0
2266                }
2267                "#
2268                );
2269            },
2270        );
2271    }
2272
2273    #[cfg(feature = "c")]
2274    #[test]
2275    fn c_lloc() {
2276        check_metrics::<CParser>(
2277            "for (;;)
2278                break;",
2279            "foo.c",
2280            |metric| {
2281                // Spaces: 1
2282                insta::assert_json_snapshot!(
2283                    metric.loc,
2284                    @r#"
2285                {
2286                  "sloc": 2,
2287                  "ploc": 2,
2288                  "lloc": 2,
2289                  "cloc": 0,
2290                  "blank": 0,
2291                  "dloc": 0,
2292                  "sloc_average": 2.0,
2293                  "ploc_average": 2.0,
2294                  "lloc_average": 2.0,
2295                  "cloc_average": 0.0,
2296                  "blank_average": 0.0,
2297                  "dloc_average": 0.0,
2298                  "sloc_min": 2,
2299                  "sloc_max": 2,
2300                  "cloc_min": 0,
2301                  "cloc_max": 0,
2302                  "ploc_min": 2,
2303                  "ploc_max": 2,
2304                  "lloc_min": 2,
2305                  "lloc_max": 2,
2306                  "blank_min": 0,
2307                  "blank_max": 0,
2308                  "dloc_min": 0,
2309                  "dloc_max": 0
2310                }
2311                "#
2312                );
2313            },
2314        );
2315    }
2316
2317    #[cfg(feature = "cpp")]
2318    #[test]
2319    fn cpp_lloc() {
2320        check_metrics::<CppParser>(
2321            "nsTArray<xpcGCCallback> callbacks(extraGCCallbacks.Clone());
2322             for (uint32_t i = 0; i < callbacks.Length(); ++i) {
2323                 callbacks[i](status);
2324             }",
2325            "foo.cpp",
2326            |metric| {
2327                // Spaces: 1
2328                // lloc: nsTArray, for, callbacks
2329                insta::assert_json_snapshot!(
2330                    metric.loc,
2331                    @r#"
2332                {
2333                  "sloc": 4,
2334                  "ploc": 4,
2335                  "lloc": 3,
2336                  "cloc": 0,
2337                  "blank": 0,
2338                  "dloc": 0,
2339                  "sloc_average": 4.0,
2340                  "ploc_average": 4.0,
2341                  "lloc_average": 3.0,
2342                  "cloc_average": 0.0,
2343                  "blank_average": 0.0,
2344                  "dloc_average": 0.0,
2345                  "sloc_min": 4,
2346                  "sloc_max": 4,
2347                  "cloc_min": 0,
2348                  "cloc_max": 0,
2349                  "ploc_min": 4,
2350                  "ploc_max": 4,
2351                  "lloc_min": 3,
2352                  "lloc_max": 3,
2353                  "blank_min": 0,
2354                  "blank_max": 0,
2355                  "dloc_min": 0,
2356                  "dloc_max": 0
2357                }
2358                "#
2359                );
2360            },
2361        );
2362    }
2363
2364    #[cfg(feature = "cpp")]
2365    #[test]
2366    fn cpp_return_lloc() {
2367        check_metrics::<CppParser>(
2368            "uint8_t* pixel_data = frame.GetFrameDataAtPos(DesktopVector(x, y));
2369             return RgbaColor(pixel_data) == blank_pixel_;",
2370            "foo.cpp",
2371            |metric| {
2372                // Spaces: 1
2373                // lloc: pixel_data, return
2374                insta::assert_json_snapshot!(
2375                    metric.loc,
2376                    @r#"
2377                {
2378                  "sloc": 2,
2379                  "ploc": 2,
2380                  "lloc": 2,
2381                  "cloc": 0,
2382                  "blank": 0,
2383                  "dloc": 0,
2384                  "sloc_average": 2.0,
2385                  "ploc_average": 2.0,
2386                  "lloc_average": 2.0,
2387                  "cloc_average": 0.0,
2388                  "blank_average": 0.0,
2389                  "dloc_average": 0.0,
2390                  "sloc_min": 2,
2391                  "sloc_max": 2,
2392                  "cloc_min": 0,
2393                  "cloc_max": 0,
2394                  "ploc_min": 2,
2395                  "ploc_max": 2,
2396                  "lloc_min": 2,
2397                  "lloc_max": 2,
2398                  "blank_min": 0,
2399                  "blank_max": 0,
2400                  "dloc_min": 0,
2401                  "dloc_max": 0
2402                }
2403                "#
2404                );
2405            },
2406        );
2407    }
2408
2409    #[cfg(feature = "cpp")]
2410    #[test]
2411    fn cpp_for_lloc() {
2412        check_metrics::<CppParser>(
2413            "for (; start != end; ++start) {
2414                 const unsigned char idx = *start;
2415                 if (idx > 127 || !kValidTokenMap[idx]) return false;
2416             }",
2417            "foo.cpp",
2418            |metric| {
2419                // Spaces: 1
2420                // lloc: for, idx, if, return
2421                insta::assert_json_snapshot!(
2422                    metric.loc,
2423                    @r#"
2424                {
2425                  "sloc": 4,
2426                  "ploc": 4,
2427                  "lloc": 4,
2428                  "cloc": 0,
2429                  "blank": 0,
2430                  "dloc": 0,
2431                  "sloc_average": 4.0,
2432                  "ploc_average": 4.0,
2433                  "lloc_average": 4.0,
2434                  "cloc_average": 0.0,
2435                  "blank_average": 0.0,
2436                  "dloc_average": 0.0,
2437                  "sloc_min": 4,
2438                  "sloc_max": 4,
2439                  "cloc_min": 0,
2440                  "cloc_max": 0,
2441                  "ploc_min": 4,
2442                  "ploc_max": 4,
2443                  "lloc_min": 4,
2444                  "lloc_max": 4,
2445                  "blank_min": 0,
2446                  "blank_max": 0,
2447                  "dloc_min": 0,
2448                  "dloc_max": 0
2449                }
2450                "#
2451                );
2452            },
2453        );
2454    }
2455
2456    #[cfg(feature = "cpp")]
2457    #[test]
2458    fn cpp_while_lloc() {
2459        check_metrics::<CppParser>(
2460            "while (sHeapAtoms) {
2461                 HttpHeapAtom* next = sHeapAtoms->next;
2462                 free(sHeapAtoms);
2463            }",
2464            "foo.cpp",
2465            |metric| {
2466                // Spaces: 1
2467                // lloc: while, next, free
2468                insta::assert_json_snapshot!(
2469                    metric.loc,
2470                    @r#"
2471                {
2472                  "sloc": 4,
2473                  "ploc": 4,
2474                  "lloc": 3,
2475                  "cloc": 0,
2476                  "blank": 0,
2477                  "dloc": 0,
2478                  "sloc_average": 4.0,
2479                  "ploc_average": 4.0,
2480                  "lloc_average": 3.0,
2481                  "cloc_average": 0.0,
2482                  "blank_average": 0.0,
2483                  "dloc_average": 0.0,
2484                  "sloc_min": 4,
2485                  "sloc_max": 4,
2486                  "cloc_min": 0,
2487                  "cloc_max": 0,
2488                  "ploc_min": 4,
2489                  "ploc_max": 4,
2490                  "lloc_min": 3,
2491                  "lloc_max": 3,
2492                  "blank_min": 0,
2493                  "blank_max": 0,
2494                  "dloc_min": 0,
2495                  "dloc_max": 0
2496                }
2497                "#
2498                );
2499            },
2500        );
2501    }
2502
2503    #[cfg(feature = "python")]
2504    #[test]
2505    fn python_string_on_new_line() {
2506        // More lines of the same instruction were counted as blank lines
2507        check_metrics::<PythonParser>(
2508            "capabilities[\"goog:chromeOptions\"][\"androidPackage\"] = \\
2509                \"org.chromium.weblayer.shell\"",
2510            "foo.py",
2511            |metric| {
2512                // Spaces: 1
2513                insta::assert_json_snapshot!(
2514                    metric.loc,
2515                    @r#"
2516                {
2517                  "sloc": 2,
2518                  "ploc": 2,
2519                  "lloc": 1,
2520                  "cloc": 0,
2521                  "blank": 0,
2522                  "dloc": 0,
2523                  "sloc_average": 2.0,
2524                  "ploc_average": 2.0,
2525                  "lloc_average": 1.0,
2526                  "cloc_average": 0.0,
2527                  "blank_average": 0.0,
2528                  "dloc_average": 0.0,
2529                  "sloc_min": 2,
2530                  "sloc_max": 2,
2531                  "cloc_min": 0,
2532                  "cloc_max": 0,
2533                  "ploc_min": 2,
2534                  "ploc_max": 2,
2535                  "lloc_min": 1,
2536                  "lloc_max": 1,
2537                  "blank_min": 0,
2538                  "blank_max": 0,
2539                  "dloc_min": 0,
2540                  "dloc_max": 0
2541                }
2542                "#
2543                );
2544            },
2545        );
2546    }
2547
2548    #[cfg(feature = "python")]
2549    #[test]
2550    fn python_multiline_string_assignment_ploc() {
2551        // Regression test for issue #415: interior rows of a multi-line,
2552        // non-docstring string literal were mis-counted as blank lines.
2553        check_metrics::<PythonParser>(
2554            "QUERY = \"\"\"
2555SELECT id, name
2556FROM users
2557WHERE active = 1
2558ORDER BY name
2559\"\"\"",
2560            "foo.py",
2561            |metric| {
2562                // Spaces: 1. Six physical lines, all code, no blanks. An
2563                // assigned literal is not a docstring, so its rows reach
2564                // neither `cloc` nor `dloc`.
2565                assert_eq!(metric.loc.sloc(), 6);
2566                assert_eq!(metric.loc.ploc(), 6);
2567                assert_eq!(metric.loc.cloc(), 0);
2568                assert_eq!(metric.loc.dloc(), 0);
2569                assert_eq!(metric.loc.blank(), 0);
2570                insta::assert_json_snapshot!(
2571                    metric.loc,
2572                    @r#"
2573                {
2574                  "sloc": 6,
2575                  "ploc": 6,
2576                  "lloc": 1,
2577                  "cloc": 0,
2578                  "blank": 0,
2579                  "dloc": 0,
2580                  "sloc_average": 6.0,
2581                  "ploc_average": 6.0,
2582                  "lloc_average": 1.0,
2583                  "cloc_average": 0.0,
2584                  "blank_average": 0.0,
2585                  "dloc_average": 0.0,
2586                  "sloc_min": 6,
2587                  "sloc_max": 6,
2588                  "cloc_min": 0,
2589                  "cloc_max": 0,
2590                  "ploc_min": 6,
2591                  "ploc_max": 6,
2592                  "lloc_min": 1,
2593                  "lloc_max": 1,
2594                  "blank_min": 0,
2595                  "blank_max": 0,
2596                  "dloc_min": 0,
2597                  "dloc_max": 0
2598                }
2599                "#
2600                );
2601            },
2602        );
2603    }
2604
2605    #[cfg(feature = "python")]
2606    #[test]
2607    fn python_multiline_string_argument_ploc() {
2608        // Regression test for issue #415: a multi-line string passed as a call
2609        // argument must have all its rows counted as code, not blank.
2610        check_metrics::<PythonParser>(
2611            "print(\"\"\"
2612line one
2613line two
2614\"\"\")",
2615            "foo.py",
2616            |metric| {
2617                // Spaces: 1. Four physical lines, all code, no blanks.
2618                assert_eq!(metric.loc.sloc(), 4);
2619                assert_eq!(metric.loc.ploc(), 4);
2620                assert_eq!(metric.loc.cloc(), 0);
2621                assert_eq!(metric.loc.blank(), 0);
2622            },
2623        );
2624    }
2625
2626    #[cfg(feature = "python")]
2627    #[test]
2628    fn python_single_line_string_assignment_ploc() {
2629        // Single-line, non-docstring string: behaviour must be unchanged by
2630        // the issue #415 fix (start == end means no extra rows are inserted).
2631        check_metrics::<PythonParser>("QUERY = \"SELECT 1\"", "foo.py", |metric| {
2632            // Spaces: 1.
2633            assert_eq!(metric.loc.sloc(), 1);
2634            assert_eq!(metric.loc.ploc(), 1);
2635            assert_eq!(metric.loc.cloc(), 0);
2636            assert_eq!(metric.loc.blank(), 0);
2637        });
2638    }
2639
2640    #[cfg(feature = "python")]
2641    #[test]
2642    fn python_multiline_docstring_still_cloc() {
2643        // The fix for issue #415 must leave docstring classification unchanged:
2644        // a bare triple-quoted string statement is still counted as comments.
2645        check_metrics::<PythonParser>(
2646            "def f():
2647    \"\"\"Docstring line one
2648    Docstring line two
2649    \"\"\"
2650    return 1",
2651            "foo.py",
2652            |metric| {
2653                // Spaces: 2 (module + function). The three docstring rows are
2654                // comment lines, not code or blank lines — and since #1597
2655                // they are also reported as `dloc`, so a consumer can
2656                // read CLOC either way.
2657                assert_eq!(metric.loc.cloc(), 3);
2658                assert_eq!(metric.loc.dloc(), 3);
2659                assert_eq!(metric.loc.blank(), 0);
2660            },
2661        );
2662    }
2663
2664    #[cfg(feature = "python")]
2665    #[test]
2666    fn python_single_line_docstring_is_one_docstring_row() {
2667        // expected: 3 rows — `def`, the docstring, `return`. The docstring
2668        // row is a comment row and a docstring row; neither code nor blank.
2669        check_metrics::<PythonParser>(
2670            "def f():\n    \"\"\"One line.\"\"\"\n    return 1",
2671            "foo.py",
2672            |metric| {
2673                assert_eq!(metric.loc.sloc(), 3);
2674                assert_eq!(metric.loc.ploc(), 2);
2675                assert_eq!(metric.loc.cloc(), 1);
2676                assert_eq!(metric.loc.dloc(), 1);
2677                assert_eq!(metric.loc.blank(), 0);
2678            },
2679        );
2680    }
2681
2682    #[cfg(feature = "python")]
2683    #[test]
2684    fn python_docstring_sharing_the_def_row_counts_that_row() {
2685        // A docstring on the `def` row is a code-and-comment row for CLOC
2686        // (`add_cloc_lines`'s first branch), and the `dloc` set records
2687        // the same row: `dloc <= cloc` must hold on this shape too.
2688        // expected: 1 row that is code, comment and docstring at once.
2689        check_metrics::<PythonParser>("def f(): \"\"\"x\"\"\"", "foo.py", |metric| {
2690            assert_eq!(metric.loc.sloc(), 1);
2691            assert_eq!(metric.loc.ploc(), 1);
2692            assert_eq!(metric.loc.cloc(), 1);
2693            assert_eq!(metric.loc.dloc(), 1);
2694            assert_eq!(metric.loc.blank(), 0);
2695        });
2696    }
2697
2698    #[cfg(feature = "python")]
2699    #[test]
2700    fn python_docstring_is_a_subset_of_cloc() {
2701        // Anchors the subset on a second axis (`.claude/rules/testing.md`):
2702        // the comment row is in `cloc` and not in `dloc`, so trimming
2703        // either construct out of the fixture fails a different assertion.
2704        // expected: 5 rows — `def`, two docstring rows, `# a comment`,
2705        // `return`; cloc 3 = 2 docstring + 1 comment.
2706        check_metrics::<PythonParser>(
2707            "def f():\n    \"\"\"Doc one.\n    Doc two.\"\"\"\n    # a comment\n    return 1",
2708            "foo.py",
2709            |metric| {
2710                assert_eq!(metric.loc.sloc(), 5);
2711                assert_eq!(metric.loc.ploc(), 2);
2712                assert_eq!(metric.loc.cloc(), 3);
2713                assert_eq!(metric.loc.dloc(), 2);
2714                assert_eq!(metric.loc.blank(), 0);
2715            },
2716        );
2717    }
2718
2719    #[cfg(feature = "python")]
2720    #[test]
2721    fn python_bare_string_out_of_docstring_position_is_a_comment_not_documentation() {
2722        // Any bare string statement is a comment row (#415), but only one in
2723        // a PEP 257 docstring or attribute-docstring position is
2724        // documentation (#1597). A string after `x = 1` inside a function is
2725        // neither: only a class's `__init__` has attribute docstrings, and
2726        // only after an assignment to `self.<name>`.
2727        // expected: 4 rows — `def`, `x = 1`, the bare string, `return`.
2728        check_metrics::<PythonParser>(
2729            "def f():\n    x = 1\n    \"\"\"Not first, still bare.\"\"\"\n    return x",
2730            "foo.py",
2731            |metric| {
2732                assert_eq!(metric.loc.sloc(), 4);
2733                assert_eq!(metric.loc.ploc(), 3);
2734                assert_eq!(metric.loc.cloc(), 1);
2735                assert_eq!(metric.loc.dloc(), 0);
2736            },
2737        );
2738    }
2739
2740    /// Each position the PEP 257 rule accepts or rejects (#1597). Every
2741    /// fixture's bare string is one row, so `cloc` counts the bare strings
2742    /// and `dloc` the ones in a documentation position.
2743    #[cfg(feature = "python")]
2744    #[test]
2745    fn python_documentation_positions_follow_pep_257() {
2746        let cases: &[(&str, &[u8], u64, u64)] = &[
2747            // (name, source, cloc, dloc)
2748            ("module docstring", b"\"\"\"M.\"\"\"\nx = 1\n", 1, 1),
2749            ("module docstring after a comment", b"# c\n\"\"\"M.\"\"\"\nx = 1\n", 2, 1),
2750            ("class docstring", b"class C:\n    \"\"\"C.\"\"\"\n    x = 1\n", 1, 1),
2751            ("function docstring", b"def f():\n    \"\"\"F.\"\"\"\n    return 1\n", 1, 1),
2752            ("additional docstring", b"def f():\n    \"\"\"F.\"\"\"\n    \"\"\"More.\"\"\"\n    return 1\n", 2, 2),
2753            ("successive additional docstrings", b"def f():\n    \"\"\"F.\"\"\"\n    \"\"\"More.\"\"\"\n    \"\"\"Still more.\"\"\"\n    return 1\n", 3, 3),
2754            ("additional attribute docstring", b"x = 1\n\"\"\"X.\"\"\"\n\"\"\"More.\"\"\"\n", 2, 2),
2755            ("additional class attribute docstring", b"class C:\n    x = 1\n    \"\"\"X.\"\"\"\n    # comment\n    \"\"\"More.\"\"\"\n", 3, 2),
2756            ("successive strings after a call", b"print(1)\n\"\"\"No.\"\"\"\n\"\"\"Still no.\"\"\"\n", 2, 0),
2757            // The statement a run follows is found past the comments
2758            // before the run's first string, not just inside the run.
2759            ("attribute docstring run after comments", b"x = 1\n# a\n\"\"\"X.\"\"\"\n# b\n\"\"\"More.\"\"\"\n", 4, 2),
2760            ("string run after comments after a call", b"print(1)\n# a\n\"\"\"No.\"\"\"\n# b\n\"\"\"Still no.\"\"\"\n", 4, 0),
2761            ("module attribute docstring", b"x = 1\n\"\"\"X.\"\"\"\n", 1, 1),
2762            ("annotated attribute docstring", b"x: int = 1\n\"\"\"X.\"\"\"\n", 1, 1),
2763            ("class attribute docstring", b"class C:\n    x = 1\n    \"\"\"X.\"\"\"\n", 1, 1),
2764            ("after an assignment in a function", b"def f():\n    x = 1\n    \"\"\"No.\"\"\"\n", 1, 0),
2765            ("after a call at module level", b"print(1)\n\"\"\"No.\"\"\"\n", 1, 0),
2766            ("after the imports", b"import os\n\"\"\"No.\"\"\"\n", 1, 0),
2767            ("inside an if body", b"if True:\n    \"\"\"No.\"\"\"\n", 1, 0),
2768            ("concatenated docstring", b"def f():\n    \"\"\"F.\"\"\" \"more\"\n    return 1\n", 1, 1),
2769            // PEP 257's third place for attribute docstrings, PEP 258's
2770            // instance attribute: `self.<name>` at the top level of a class's
2771            // `__init__`, as Sphinx records it.
2772            ("attribute docstring in __init__", b"class C:\n    def __init__(self):\n        self.x = 1\n        \"\"\"X.\"\"\"\n", 1, 1),
2773            ("additional attribute docstring in __init__", b"class C:\n    def __init__(self):\n        self.x = 1\n        \"\"\"X.\"\"\"\n        \"\"\"More.\"\"\"\n", 2, 2),
2774            ("annotated attribute in __init__", b"class C:\n    def __init__(self):\n        self.x: int = 1\n        \"\"\"X.\"\"\"\n", 1, 1),
2775            ("decorated __init__", b"class C:\n    @wraps\n    def __init__(self):\n        self.x = 1\n        \"\"\"X.\"\"\"\n", 1, 1),
2776            ("receiver named otherwise", b"class C:\n    def __init__(this):\n        this.x = 1\n        \"\"\"X.\"\"\"\n", 1, 1),
2777            ("local in __init__", b"class C:\n    def __init__(self):\n        x = 1\n        \"\"\"No.\"\"\"\n", 1, 0),
2778            ("other object in __init__", b"class C:\n    def __init__(self, o):\n        o.x = 1\n        \"\"\"No.\"\"\"\n", 1, 0),
2779            ("self attribute in another method", b"class C:\n    def m(self):\n        self.x = 1\n        \"\"\"No.\"\"\"\n", 1, 0),
2780            ("__init__ outside a class", b"def __init__(self):\n    self.x = 1\n    \"\"\"No.\"\"\"\n", 1, 0),
2781            // A module or class attribute docstring follows a name, or a
2782            // tuple holding one; Sphinx records none after a subscript or
2783            // another object's attribute.
2784            ("tuple target", b"a, b = 1, 2\n\"\"\"Pair.\"\"\"\n", 1, 1),
2785            ("subscript target", b"x = {}\nx[\"k\"] = 1\n\"\"\"No.\"\"\"\n", 1, 0),
2786            ("attribute target", b"import os\nos.sep = \"/\"\n\"\"\"No.\"\"\"\n", 1, 0),
2787            ("class-level attribute target", b"class C:\n    Foo.x = 1\n    \"\"\"No.\"\"\"\n", 1, 0),
2788            // Only a `str` literal is stored as `__doc__`.
2789            ("f-string docstring", b"def f():\n    f\"\"\"F.\"\"\"\n    return 1\n", 1, 0),
2790            ("bytes docstring", b"def f():\n    b\"\"\"B.\"\"\"\n    return 1\n", 1, 0),
2791            ("raw docstring", b"def f():\n    r\"\"\"R.\"\"\"\n    return 1\n", 1, 1),
2792            ("f-string fragment", b"def f():\n    \"\"\"a\"\"\" f\"b\"\n    return 1\n", 1, 0),
2793            ("string after an f-string", b"def f():\n    f\"\"\"x\"\"\"\n    \"\"\"No.\"\"\"\n    return 1\n", 2, 0),
2794        ];
2795        for (name, source, cloc, dloc) in cases {
2796            let loc = metrics_verbatim(crate::LANG::Python, source, MetricsOptions::default()).loc;
2797            assert_eq!((loc.cloc(), loc.dloc()), (*cloc, *dloc), "Python: {name}");
2798        }
2799    }
2800
2801    #[cfg(feature = "python")]
2802    #[test]
2803    fn python_docstring_with_an_escape_sequence_is_not_a_code_row() {
2804        // `escape_sequence` is a leaf under `string_content`; left to the
2805        // catch-all it credited the docstring's row to PLOC, so the row
2806        // read as both code and docstring (#1597). expected: 3 rows —
2807        // `def`, the docstring, `return`; the docstring row is a comment
2808        // row only.
2809        check_metrics::<PythonParser>(
2810            "def f():\n    \"\"\"Has a \\n escape.\"\"\"\n    return 1",
2811            "foo.py",
2812            |metric| {
2813                assert_eq!(metric.loc.sloc(), 3);
2814                assert_eq!(metric.loc.ploc(), 2);
2815                assert_eq!(metric.loc.cloc(), 1);
2816                assert_eq!(metric.loc.dloc(), 1);
2817                assert_eq!(metric.loc.dloc_without_code(), 1);
2818            },
2819        );
2820    }
2821
2822    #[cfg(feature = "python")]
2823    #[test]
2824    fn python_concatenated_docstring_counts_its_rows() {
2825        // `"""Doc.""" "more"` is one docstring to CPython (adjacent
2826        // literals join at compile time) and to Halstead (#695); the LOC
2827        // walk shares that predicate, so the whole `concatenated_string`
2828        // is billed as docstring rows and its fragments add no code row.
2829        // expected: 4 rows — `def`, two docstring rows, `return`.
2830        check_metrics::<PythonParser>(
2831            "def f():\n    \"\"\"Doc one.\n    Two.\"\"\" \"and more.\"\n    return 1",
2832            "foo.py",
2833            |metric| {
2834                assert_eq!(metric.loc.sloc(), 4);
2835                assert_eq!(metric.loc.ploc(), 2);
2836                assert_eq!(metric.loc.cloc(), 2);
2837                assert_eq!(metric.loc.dloc(), 2);
2838            },
2839        );
2840    }
2841
2842    #[cfg(feature = "python")]
2843    #[test]
2844    fn python_concatenated_string_value_stays_code() {
2845        // The other branch of the `ConcatenatedString` arm: a concatenated
2846        // value keeps the catch-all's PLOC credit for its opening row.
2847        // expected: 3 rows, all code.
2848        check_metrics::<PythonParser>(
2849            "def f():\n    x = \"a\" \"b\"\n    return x",
2850            "foo.py",
2851            |metric| {
2852                assert_eq!(metric.loc.sloc(), 3);
2853                assert_eq!(metric.loc.ploc(), 3);
2854                assert_eq!(metric.loc.cloc(), 0);
2855                assert_eq!(metric.loc.dloc(), 0);
2856            },
2857        );
2858    }
2859
2860    #[cfg(feature = "python")]
2861    #[test]
2862    fn python_bare_tuple_of_strings_is_not_a_docstring() {
2863        // A bare `"a", "b"` statement has two expression children, so it
2864        // is a discarded tuple, not a docstring — the `child_count == 1`
2865        // gate Halstead has applied since #695 and the LOC walk now
2866        // shares. expected: 3 rows, all code.
2867        check_metrics::<PythonParser>(
2868            "def f():\n    \"a\", \"b\"\n    return 1",
2869            "foo.py",
2870            |metric| {
2871                assert_eq!(metric.loc.sloc(), 3);
2872                assert_eq!(metric.loc.ploc(), 3);
2873                assert_eq!(metric.loc.cloc(), 0);
2874                assert_eq!(metric.loc.dloc(), 0);
2875            },
2876        );
2877        // A multi-row leading element: its opening row has no other token,
2878        // so the literal must credit it — no row of the statement is blank.
2879        // expected: 4 rows, all code.
2880        check_metrics::<PythonParser>(
2881            "def f():\n    \"\"\"first\n    second\"\"\", None\n    return 1",
2882            "foo.py",
2883            |metric| {
2884                assert_eq!(metric.loc.sloc(), 4);
2885                assert_eq!(metric.loc.ploc(), 4);
2886                assert_eq!(metric.loc.cloc(), 0);
2887                assert_eq!(metric.loc.blank(), 0);
2888            },
2889        );
2890    }
2891
2892    /// CPython drops the parentheses around an expression, so a
2893    /// parenthesized string statement is the same docstring, or the same
2894    /// discarded string, as the bare one (#1610): the parentheses' rows
2895    /// are its rows, and a comment inside them stays a comment row only.
2896    #[cfg(feature = "python")]
2897    #[test]
2898    fn python_parenthesized_docstring_counts_as_the_bare_one() {
2899        let cases: &[(&str, &[u8], [u64; 5])] = &[
2900            // (name, source, [sloc, ploc, cloc, dloc, blank])
2901            (
2902                "function docstring",
2903                b"def f():\n    (\"\"\"F doc.\"\"\")\n    return 1\n",
2904                [3, 2, 1, 1, 0],
2905            ),
2906            (
2907                "module docstring",
2908                b"(\"\"\"Module doc.\"\"\")\nx = 1\n",
2909                [2, 1, 1, 1, 0],
2910            ),
2911            (
2912                "nested parentheses",
2913                b"def f():\n    ((\"doc\"))\n    return 1\n",
2914                [3, 2, 1, 1, 0],
2915            ),
2916            (
2917                "concatenation",
2918                b"def f():\n    (\"a\" \"b\")\n    return 1\n",
2919                [3, 2, 1, 1, 0],
2920            ),
2921            // A tuple, not a parenthesized string: code.
2922            (
2923                "tuple of strings",
2924                b"def f():\n    (\"a\", \"b\")\n    return 1\n",
2925                [3, 3, 0, 0, 0],
2926            ),
2927            // A tuple whose first element is a parenthesized string: code.
2928            (
2929                "tuple led by a parenthesized string",
2930                b"def f():\n    (\n        \"a\"\n    ), \"b\"\n    return 1\n",
2931                [5, 5, 0, 0, 0],
2932            ),
2933            // expected: the same rows as the plain two-row docstring,
2934            // `def f():\n    """doc\n    more"""\n    return 1`.
2935            (
2936                "two-row docstring",
2937                b"def f():\n    (\"\"\"doc\n    more\"\"\")\n    return 1\n",
2938                [4, 2, 2, 2, 0],
2939            ),
2940            // expected: `(` and `)` rows are docstring rows beside the
2941            // string's; nothing in the statement is code.
2942            (
2943                "parentheses on their own rows",
2944                b"def f():\n    (\n        \"\"\"doc\n        more\"\"\"\n    )\n    return 1\n",
2945                [6, 2, 4, 4, 0],
2946            ),
2947            // expected: the comment row is a comment, not documentation.
2948            (
2949                "comment inside the parentheses",
2950                b"def f():\n    (\n        # c\n        \"\"\"doc\"\"\"\n    )\n    return 1\n",
2951                [6, 2, 4, 3, 0],
2952            ),
2953            // An f-string is no docstring, parenthesized or not; bare, it
2954            // is a comment row (#415), as `f"x"` alone is.
2955            (
2956                "parenthesized f-string",
2957                b"def f():\n    (f\"x\")\n    return 1\n",
2958                [3, 2, 1, 0, 0],
2959            ),
2960            (
2961                "parenthesized additional docstring",
2962                b"def f():\n    \"\"\"F.\"\"\"\n    (\"\"\"More.\"\"\")\n    return 1\n",
2963                [4, 2, 2, 2, 0],
2964            ),
2965            (
2966                "attribute docstring",
2967                b"x = 1\n(\"\"\"X.\"\"\")\n",
2968                [2, 1, 1, 1, 0],
2969            ),
2970            // A parenthesized name is code, parentheses and all.
2971            (
2972                "parenthesized name",
2973                b"def f(x):\n    (\n        x\n    )\n    return 1\n",
2974                [5, 5, 0, 0, 0],
2975            ),
2976            // A parenthesized value is still a value.
2977            (
2978                "parenthesized assigned string",
2979                b"x = (\"\"\"a\nb\"\"\")\n",
2980                [2, 2, 0, 0, 0],
2981            ),
2982        ];
2983        let row = |source: &[u8]| {
2984            let loc = metrics_verbatim(crate::LANG::Python, source, MetricsOptions::default()).loc;
2985            [loc.sloc(), loc.ploc(), loc.cloc(), loc.dloc(), loc.blank()]
2986        };
2987        for (name, source, expected) in cases {
2988            assert_eq!(row(source), *expected, "Python: {name}");
2989        }
2990        // CPython rejects a 201st nested parenthesis, so the string at 200
2991        // is a docstring and the one at 201 is code, every parenthesis of it.
2992        for (depth, expected) in [(200, [3, 2, 1, 1, 0]), (201, [3, 3, 0, 0, 0])] {
2993            let source = format!(
2994                "def f():\n    {}\"doc\"{}\n    return 1\n",
2995                "(".repeat(depth),
2996                ")".repeat(depth)
2997            );
2998            assert_eq!(
2999                row(source.as_bytes()),
3000                expected,
3001                "Python: {depth} parentheses"
3002            );
3003        }
3004    }
3005
3006    #[cfg(feature = "python")]
3007    #[test]
3008    fn python_docstring_sharing_a_code_row_is_not_without_code() {
3009        // `dloc_without_code` is what MI subtracts from the comment
3010        // ratio's denominator: a def-row docstring is a docstring row and
3011        // a code row, so nothing leaves it; a two-row docstring whose
3012        // closing quote is followed by a statement gives up only its
3013        // first row.
3014        check_metrics::<PythonParser>("def f(): \"\"\"x\"\"\"", "foo.py", |metric| {
3015            assert_eq!((metric.loc.dloc(), metric.loc.dloc_without_code()), (1, 0));
3016        });
3017        check_metrics::<PythonParser>(
3018            "def f():\n    \"\"\"Doc.\n    More.\"\"\"; y = 1\n    return y",
3019            "foo.py",
3020            |metric| {
3021                assert_eq!(metric.loc.ploc(), 3);
3022                assert_eq!((metric.loc.dloc(), metric.loc.dloc_without_code()), (2, 1));
3023            },
3024        );
3025    }
3026
3027    /// Docstring sibling of `python_nested_min_max_propagates` (#437): the
3028    /// per-space minimum, maximum and average fold through unit -> class
3029    /// -> method exactly as the other five loc fields do.
3030    #[cfg(feature = "python")]
3031    #[test]
3032    fn python_docstring_min_max_propagate_through_nested_spaces() {
3033        // expected: Spaces are unit, `C`, `small`, `big`. Own docstring
3034        // rows: unit 2 (module docstring), `C` 1, `small` 0, `big` 3.
3035        // Merging unions upward, so the unit reports 6 and `C` reports 4;
3036        // `small` holds the minimum 0 and the unit's merged total is the
3037        // maximum, as `sloc_max == sloc()` in the sibling test. Average is
3038        // 6 / 4 spaces.
3039        check_metrics::<PythonParser>(
3040            "\"\"\"Module doc.\nMore.\"\"\"\nclass C:\n    \"\"\"Class doc.\"\"\"\n    def small(self):\n        x = 1\n    def big(self):\n        \"\"\"Big doc.\n        Second.\n        Third.\"\"\"\n        return 1\n",
3041            "c.py",
3042            |metric| {
3043                let loc = &metric.loc;
3044                assert_eq!(loc.dloc(), 6);
3045                assert_eq!(loc.cloc(), 6);
3046                assert_eq!(loc.dloc_min(), 0, "smallest leaf method has no docstring");
3047                assert_eq!(loc.dloc_max(), 6, "the unit's merged total is the maximum");
3048                assert_eq!(loc.dloc_average(), 1.5);
3049            },
3050        );
3051    }
3052
3053    #[cfg(feature = "rust")]
3054    #[test]
3055    fn rust_no_field_expression_lloc() {
3056        check_metrics::<RustParser>(
3057            "struct Foo {
3058                field: usize,
3059             }
3060             let foo = Foo { 42 };
3061             foo.field;",
3062            "foo.rs",
3063            |metric| {
3064                // Spaces: 1
3065                insta::assert_json_snapshot!(
3066                    metric.loc,
3067                    @r#"
3068                {
3069                  "sloc": 5,
3070                  "ploc": 5,
3071                  "lloc": 2,
3072                  "cloc": 0,
3073                  "blank": 0,
3074                  "dloc": 0,
3075                  "sloc_average": 5.0,
3076                  "ploc_average": 5.0,
3077                  "lloc_average": 2.0,
3078                  "cloc_average": 0.0,
3079                  "blank_average": 0.0,
3080                  "dloc_average": 0.0,
3081                  "sloc_min": 5,
3082                  "sloc_max": 5,
3083                  "cloc_min": 0,
3084                  "cloc_max": 0,
3085                  "ploc_min": 5,
3086                  "ploc_max": 5,
3087                  "lloc_min": 2,
3088                  "lloc_max": 2,
3089                  "blank_min": 0,
3090                  "blank_max": 0,
3091                  "dloc_min": 0,
3092                  "dloc_max": 0
3093                }
3094                "#
3095                );
3096            },
3097        );
3098    }
3099
3100    #[cfg(feature = "rust")]
3101    #[test]
3102    fn rust_no_parenthesized_expression_lloc() {
3103        check_metrics::<RustParser>("let a = (42 + 0);", "foo.rs", |metric| {
3104            // Spaces: 1
3105            insta::assert_json_snapshot!(
3106                metric.loc,
3107                @r#"
3108            {
3109              "sloc": 1,
3110              "ploc": 1,
3111              "lloc": 1,
3112              "cloc": 0,
3113              "blank": 0,
3114              "dloc": 0,
3115              "sloc_average": 1.0,
3116              "ploc_average": 1.0,
3117              "lloc_average": 1.0,
3118              "cloc_average": 0.0,
3119              "blank_average": 0.0,
3120              "dloc_average": 0.0,
3121              "sloc_min": 1,
3122              "sloc_max": 1,
3123              "cloc_min": 0,
3124              "cloc_max": 0,
3125              "ploc_min": 1,
3126              "ploc_max": 1,
3127              "lloc_min": 1,
3128              "lloc_max": 1,
3129              "blank_min": 0,
3130              "blank_max": 0,
3131              "dloc_min": 0,
3132              "dloc_max": 0
3133            }
3134            "#
3135            );
3136        });
3137    }
3138
3139    #[cfg(feature = "rust")]
3140    #[test]
3141    fn rust_no_array_expression_lloc() {
3142        check_metrics::<RustParser>("let a = [0; 42];", "foo.rs", |metric| {
3143            // Spaces: 1
3144            insta::assert_json_snapshot!(
3145                metric.loc,
3146                @r#"
3147            {
3148              "sloc": 1,
3149              "ploc": 1,
3150              "lloc": 1,
3151              "cloc": 0,
3152              "blank": 0,
3153              "dloc": 0,
3154              "sloc_average": 1.0,
3155              "ploc_average": 1.0,
3156              "lloc_average": 1.0,
3157              "cloc_average": 0.0,
3158              "blank_average": 0.0,
3159              "dloc_average": 0.0,
3160              "sloc_min": 1,
3161              "sloc_max": 1,
3162              "cloc_min": 0,
3163              "cloc_max": 0,
3164              "ploc_min": 1,
3165              "ploc_max": 1,
3166              "lloc_min": 1,
3167              "lloc_max": 1,
3168              "blank_min": 0,
3169              "blank_max": 0,
3170              "dloc_min": 0,
3171              "dloc_max": 0
3172            }
3173            "#
3174            );
3175        });
3176    }
3177
3178    #[cfg(feature = "rust")]
3179    #[test]
3180    fn rust_no_tuple_expression_lloc() {
3181        check_metrics::<RustParser>("let a = (0, 42);", "foo.rs", |metric| {
3182            // Spaces: 1
3183            insta::assert_json_snapshot!(
3184                metric.loc,
3185                @r#"
3186            {
3187              "sloc": 1,
3188              "ploc": 1,
3189              "lloc": 1,
3190              "cloc": 0,
3191              "blank": 0,
3192              "dloc": 0,
3193              "sloc_average": 1.0,
3194              "ploc_average": 1.0,
3195              "lloc_average": 1.0,
3196              "cloc_average": 0.0,
3197              "blank_average": 0.0,
3198              "dloc_average": 0.0,
3199              "sloc_min": 1,
3200              "sloc_max": 1,
3201              "cloc_min": 0,
3202              "cloc_max": 0,
3203              "ploc_min": 1,
3204              "ploc_max": 1,
3205              "lloc_min": 1,
3206              "lloc_max": 1,
3207              "blank_min": 0,
3208              "blank_max": 0,
3209              "dloc_min": 0,
3210              "dloc_max": 0
3211            }
3212            "#
3213            );
3214        });
3215    }
3216
3217    #[cfg(feature = "rust")]
3218    #[test]
3219    fn rust_no_unit_expression_lloc() {
3220        check_metrics::<RustParser>("let a = ();", "foo.rs", |metric| {
3221            // Spaces: 1
3222            insta::assert_json_snapshot!(
3223                metric.loc,
3224                @r#"
3225            {
3226              "sloc": 1,
3227              "ploc": 1,
3228              "lloc": 1,
3229              "cloc": 0,
3230              "blank": 0,
3231              "dloc": 0,
3232              "sloc_average": 1.0,
3233              "ploc_average": 1.0,
3234              "lloc_average": 1.0,
3235              "cloc_average": 0.0,
3236              "blank_average": 0.0,
3237              "dloc_average": 0.0,
3238              "sloc_min": 1,
3239              "sloc_max": 1,
3240              "cloc_min": 0,
3241              "cloc_max": 0,
3242              "ploc_min": 1,
3243              "ploc_max": 1,
3244              "lloc_min": 1,
3245              "lloc_max": 1,
3246              "blank_min": 0,
3247              "blank_max": 0,
3248              "dloc_min": 0,
3249              "dloc_max": 0
3250            }
3251            "#
3252            );
3253        });
3254    }
3255
3256    #[cfg(feature = "rust")]
3257    #[test]
3258    fn rust_call_function_lloc() {
3259        check_metrics::<RustParser>(
3260            "let a = foo(); // +1
3261             foo(); // +1
3262             k!(foo()); // +1",
3263            "foo.rs",
3264            |metric| {
3265                // Spaces: 1
3266                insta::assert_json_snapshot!(
3267                    metric.loc,
3268                    @r#"
3269                {
3270                  "sloc": 3,
3271                  "ploc": 3,
3272                  "lloc": 3,
3273                  "cloc": 3,
3274                  "blank": 0,
3275                  "dloc": 0,
3276                  "sloc_average": 3.0,
3277                  "ploc_average": 3.0,
3278                  "lloc_average": 3.0,
3279                  "cloc_average": 3.0,
3280                  "blank_average": 0.0,
3281                  "dloc_average": 0.0,
3282                  "sloc_min": 3,
3283                  "sloc_max": 3,
3284                  "cloc_min": 3,
3285                  "cloc_max": 3,
3286                  "ploc_min": 3,
3287                  "ploc_max": 3,
3288                  "lloc_min": 3,
3289                  "lloc_max": 3,
3290                  "blank_min": 0,
3291                  "blank_max": 0,
3292                  "dloc_min": 0,
3293                  "dloc_max": 0
3294                }
3295                "#
3296                );
3297            },
3298        );
3299    }
3300
3301    #[cfg(feature = "rust")]
3302    #[test]
3303    fn rust_macro_invocation_lloc() {
3304        check_metrics::<RustParser>(
3305            "let a = foo!(); // +1
3306             foo!(); // +1
3307             k(foo!()); // +1",
3308            "foo.rs",
3309            |metric| {
3310                // Spaces: 1
3311                insta::assert_json_snapshot!(
3312                    metric.loc,
3313                    @r#"
3314                {
3315                  "sloc": 3,
3316                  "ploc": 3,
3317                  "lloc": 3,
3318                  "cloc": 3,
3319                  "blank": 0,
3320                  "dloc": 0,
3321                  "sloc_average": 3.0,
3322                  "ploc_average": 3.0,
3323                  "lloc_average": 3.0,
3324                  "cloc_average": 3.0,
3325                  "blank_average": 0.0,
3326                  "dloc_average": 0.0,
3327                  "sloc_min": 3,
3328                  "sloc_max": 3,
3329                  "cloc_min": 3,
3330                  "cloc_max": 3,
3331                  "ploc_min": 3,
3332                  "ploc_max": 3,
3333                  "lloc_min": 3,
3334                  "lloc_max": 3,
3335                  "blank_min": 0,
3336                  "blank_max": 0,
3337                  "dloc_min": 0,
3338                  "dloc_max": 0
3339                }
3340                "#
3341                );
3342            },
3343        );
3344    }
3345
3346    #[cfg(feature = "rust")]
3347    #[test]
3348    fn rust_function_in_loop_lloc() {
3349        check_metrics::<RustParser>(
3350            "for (a, b) in c.iter().enumerate() {} // +1
3351             while (a, b) in c.iter().enumerate() {} // +1
3352             while let Some(a) = c.strip_prefix(\"hi\") {} // +1",
3353            "foo.rs",
3354            |metric| {
3355                // Spaces: 1
3356                insta::assert_json_snapshot!(
3357                    metric.loc,
3358                    @r#"
3359                {
3360                  "sloc": 3,
3361                  "ploc": 3,
3362                  "lloc": 3,
3363                  "cloc": 3,
3364                  "blank": 0,
3365                  "dloc": 0,
3366                  "sloc_average": 3.0,
3367                  "ploc_average": 3.0,
3368                  "lloc_average": 3.0,
3369                  "cloc_average": 3.0,
3370                  "blank_average": 0.0,
3371                  "dloc_average": 0.0,
3372                  "sloc_min": 3,
3373                  "sloc_max": 3,
3374                  "cloc_min": 3,
3375                  "cloc_max": 3,
3376                  "ploc_min": 3,
3377                  "ploc_max": 3,
3378                  "lloc_min": 3,
3379                  "lloc_max": 3,
3380                  "blank_min": 0,
3381                  "blank_max": 0,
3382                  "dloc_min": 0,
3383                  "dloc_max": 0
3384                }
3385                "#
3386                );
3387            },
3388        );
3389    }
3390
3391    #[cfg(feature = "rust")]
3392    #[test]
3393    fn rust_function_in_if_lloc() {
3394        check_metrics::<RustParser>(
3395            "if foo() {} // +1
3396             if let Some(a) = foo() {} // +1",
3397            "foo.rs",
3398            |metric| {
3399                // Spaces: 1
3400                insta::assert_json_snapshot!(
3401                    metric.loc,
3402                    @r#"
3403                {
3404                  "sloc": 2,
3405                  "ploc": 2,
3406                  "lloc": 2,
3407                  "cloc": 2,
3408                  "blank": 0,
3409                  "dloc": 0,
3410                  "sloc_average": 2.0,
3411                  "ploc_average": 2.0,
3412                  "lloc_average": 2.0,
3413                  "cloc_average": 2.0,
3414                  "blank_average": 0.0,
3415                  "dloc_average": 0.0,
3416                  "sloc_min": 2,
3417                  "sloc_max": 2,
3418                  "cloc_min": 2,
3419                  "cloc_max": 2,
3420                  "ploc_min": 2,
3421                  "ploc_max": 2,
3422                  "lloc_min": 2,
3423                  "lloc_max": 2,
3424                  "blank_min": 0,
3425                  "blank_max": 0,
3426                  "dloc_min": 0,
3427                  "dloc_max": 0
3428                }
3429                "#
3430                );
3431            },
3432        );
3433    }
3434
3435    #[cfg(feature = "rust")]
3436    #[test]
3437    fn rust_function_in_return_lloc() {
3438        check_metrics::<RustParser>(
3439            "return foo();
3440             await foo();",
3441            "foo.rs",
3442            |metric| {
3443                // Spaces: 1
3444                insta::assert_json_snapshot!(
3445                    metric.loc,
3446                    @r#"
3447                {
3448                  "sloc": 2,
3449                  "ploc": 2,
3450                  "lloc": 2,
3451                  "cloc": 0,
3452                  "blank": 0,
3453                  "dloc": 0,
3454                  "sloc_average": 2.0,
3455                  "ploc_average": 2.0,
3456                  "lloc_average": 2.0,
3457                  "cloc_average": 0.0,
3458                  "blank_average": 0.0,
3459                  "dloc_average": 0.0,
3460                  "sloc_min": 2,
3461                  "sloc_max": 2,
3462                  "cloc_min": 0,
3463                  "cloc_max": 0,
3464                  "ploc_min": 2,
3465                  "ploc_max": 2,
3466                  "lloc_min": 2,
3467                  "lloc_max": 2,
3468                  "blank_min": 0,
3469                  "blank_max": 0,
3470                  "dloc_min": 0,
3471                  "dloc_max": 0
3472                }
3473                "#
3474                );
3475            },
3476        );
3477    }
3478
3479    #[cfg(feature = "rust")]
3480    #[test]
3481    fn rust_closure_expression_lloc() {
3482        check_metrics::<RustParser>(
3483            "let a = |i: i32| -> i32 { i + 1 }; // +1
3484             a(42); // +1
3485             k(b.iter().map(|n| n.parse.ok().unwrap_or(42))); // +1",
3486            "foo.rs",
3487            |metric| {
3488                // Spaces: 3
3489                insta::assert_json_snapshot!(
3490                    metric.loc,
3491                    @r#"
3492                {
3493                  "sloc": 3,
3494                  "ploc": 3,
3495                  "lloc": 3,
3496                  "cloc": 3,
3497                  "blank": 0,
3498                  "dloc": 0,
3499                  "sloc_average": 1.0,
3500                  "ploc_average": 1.0,
3501                  "lloc_average": 1.0,
3502                  "cloc_average": 1.0,
3503                  "blank_average": 0.0,
3504                  "dloc_average": 0.0,
3505                  "sloc_min": 1,
3506                  "sloc_max": 3,
3507                  "cloc_min": 0,
3508                  "cloc_max": 3,
3509                  "ploc_min": 1,
3510                  "ploc_max": 3,
3511                  "lloc_min": 0,
3512                  "lloc_max": 3,
3513                  "blank_min": 0,
3514                  "blank_max": 0,
3515                  "dloc_min": 0,
3516                  "dloc_max": 0
3517                }
3518                "#
3519                );
3520            },
3521        );
3522    }
3523
3524    #[cfg(feature = "python")]
3525    #[test]
3526    fn python_general_loc() {
3527        check_metrics::<PythonParser>(
3528            "def func(a,
3529                      b,
3530                      c):
3531                 print(a)
3532                 print(b)
3533                 print(c)",
3534            "foo.py",
3535            |metric| {
3536                // Spaces: 2
3537                insta::assert_json_snapshot!(
3538                    metric.loc,
3539                    @r#"
3540                {
3541                  "sloc": 6,
3542                  "ploc": 6,
3543                  "lloc": 3,
3544                  "cloc": 0,
3545                  "blank": 0,
3546                  "dloc": 0,
3547                  "sloc_average": 3.0,
3548                  "ploc_average": 3.0,
3549                  "lloc_average": 1.5,
3550                  "cloc_average": 0.0,
3551                  "blank_average": 0.0,
3552                  "dloc_average": 0.0,
3553                  "sloc_min": 6,
3554                  "sloc_max": 6,
3555                  "cloc_min": 0,
3556                  "cloc_max": 0,
3557                  "ploc_min": 6,
3558                  "ploc_max": 6,
3559                  "lloc_min": 3,
3560                  "lloc_max": 3,
3561                  "blank_min": 0,
3562                  "blank_max": 0,
3563                  "dloc_min": 0,
3564                  "dloc_max": 0
3565                }
3566                "#
3567                );
3568            },
3569        );
3570    }
3571
3572    #[cfg(feature = "python")]
3573    #[test]
3574    fn python_real_loc() {
3575        check_metrics::<PythonParser>(
3576            "def web_socket_transfer_data(request):
3577                while True:
3578                    line = request.ws_stream.receive_message()
3579                    if line is None:
3580                        return
3581                    code, reason = line.split(' ', 1)
3582                    if code is None or reason is None:
3583                        return
3584                    request.ws_stream.close_connection(int(code), reason)
3585                    # close_connection() initiates closing handshake. It validates code
3586                    # and reason. If you want to send a broken close frame for a test,
3587                    # following code will be useful.
3588                    # > data = struct.pack('!H', int(code)) + reason.encode('UTF-8')
3589                    # > request.connection.write(stream.create_close_frame(data))
3590                    # > # Suppress to re-respond client responding close frame.
3591                    # > raise Exception(\"customized server initiated closing handshake\")",
3592            "foo.py",
3593            |metric| {
3594                // Spaces: 2
3595                insta::assert_json_snapshot!(
3596                    metric.loc,
3597                    @r#"
3598                {
3599                  "sloc": 16,
3600                  "ploc": 9,
3601                  "lloc": 8,
3602                  "cloc": 7,
3603                  "blank": 0,
3604                  "dloc": 0,
3605                  "sloc_average": 8.0,
3606                  "ploc_average": 4.5,
3607                  "lloc_average": 4.0,
3608                  "cloc_average": 3.5,
3609                  "blank_average": 0.0,
3610                  "dloc_average": 0.0,
3611                  "sloc_min": 16,
3612                  "sloc_max": 16,
3613                  "cloc_min": 7,
3614                  "cloc_max": 7,
3615                  "ploc_min": 9,
3616                  "ploc_max": 9,
3617                  "lloc_min": 8,
3618                  "lloc_max": 8,
3619                  "blank_min": 0,
3620                  "blank_max": 0,
3621                  "dloc_min": 0,
3622                  "dloc_max": 0
3623                }
3624                "#
3625                );
3626            },
3627        );
3628    }
3629
3630    #[cfg(feature = "javascript")]
3631    #[test]
3632    fn javascript_real_loc() {
3633        check_metrics::<JavascriptParser>(
3634            "assert.throws(Test262Error, function() {
3635               for (let { poisoned: x = ++initEvalCount } = poisonedProperty; ; ) {
3636                 return;
3637               }
3638             });",
3639            "foo.js",
3640            |metric| {
3641                // Spaces: 2
3642                insta::assert_json_snapshot!(
3643                    metric.loc,
3644                    @r#"
3645                {
3646                  "sloc": 5,
3647                  "ploc": 5,
3648                  "lloc": 4,
3649                  "cloc": 0,
3650                  "blank": 0,
3651                  "dloc": 0,
3652                  "sloc_average": 2.5,
3653                  "ploc_average": 2.5,
3654                  "lloc_average": 2.0,
3655                  "cloc_average": 0.0,
3656                  "blank_average": 0.0,
3657                  "dloc_average": 0.0,
3658                  "sloc_min": 5,
3659                  "sloc_max": 5,
3660                  "cloc_min": 0,
3661                  "cloc_max": 0,
3662                  "ploc_min": 5,
3663                  "ploc_max": 5,
3664                  "lloc_min": 3,
3665                  "lloc_max": 4,
3666                  "blank_min": 0,
3667                  "blank_max": 0,
3668                  "dloc_min": 0,
3669                  "dloc_max": 0
3670                }
3671                "#
3672                );
3673            },
3674        );
3675    }
3676
3677    #[cfg(feature = "mozjs")]
3678    #[test]
3679    fn mozjs_real_loc() {
3680        check_metrics::<MozjsParser>(
3681            "assert.throws(Test262Error, function() {
3682               for (let { poisoned: x = ++initEvalCount } = poisonedProperty; ; ) {
3683                 return;
3684               }
3685             });",
3686            "foo.js",
3687            |metric| {
3688                // Spaces: 2
3689                insta::assert_json_snapshot!(
3690                    metric.loc,
3691                    @r#"
3692                {
3693                  "sloc": 5,
3694                  "ploc": 5,
3695                  "lloc": 4,
3696                  "cloc": 0,
3697                  "blank": 0,
3698                  "dloc": 0,
3699                  "sloc_average": 2.5,
3700                  "ploc_average": 2.5,
3701                  "lloc_average": 2.0,
3702                  "cloc_average": 0.0,
3703                  "blank_average": 0.0,
3704                  "dloc_average": 0.0,
3705                  "sloc_min": 5,
3706                  "sloc_max": 5,
3707                  "cloc_min": 0,
3708                  "cloc_max": 0,
3709                  "ploc_min": 5,
3710                  "ploc_max": 5,
3711                  "lloc_min": 3,
3712                  "lloc_max": 4,
3713                  "blank_min": 0,
3714                  "blank_max": 0,
3715                  "dloc_min": 0,
3716                  "dloc_max": 0
3717                }
3718                "#
3719                );
3720            },
3721        );
3722    }
3723
3724    #[cfg(feature = "mozjs")]
3725    #[test]
3726    fn mozjs_blank_and_comment_loc() {
3727        check_metrics::<MozjsParser>(
3728            "// a comment
3729             function f() {
3730
3731                 var x = 1;
3732
3733             }",
3734            "foo.js",
3735            |metric| {
3736                insta::assert_json_snapshot!(
3737                    metric.loc,
3738                    @r#"
3739                {
3740                  "sloc": 6,
3741                  "ploc": 3,
3742                  "lloc": 1,
3743                  "cloc": 1,
3744                  "blank": 2,
3745                  "dloc": 0,
3746                  "sloc_average": 3.0,
3747                  "ploc_average": 1.5,
3748                  "lloc_average": 0.5,
3749                  "cloc_average": 0.5,
3750                  "blank_average": 1.0,
3751                  "dloc_average": 0.0,
3752                  "sloc_min": 5,
3753                  "sloc_max": 6,
3754                  "cloc_min": 0,
3755                  "cloc_max": 1,
3756                  "ploc_min": 3,
3757                  "ploc_max": 3,
3758                  "lloc_min": 1,
3759                  "lloc_max": 1,
3760                  "blank_min": 2,
3761                  "blank_max": 2,
3762                  "dloc_min": 0,
3763                  "dloc_max": 0
3764                }
3765                "#
3766                );
3767            },
3768        );
3769    }
3770
3771    #[cfg(feature = "cpp")]
3772    #[test]
3773    fn cpp_namespace_loc() {
3774        check_metrics::<CppParser>(
3775            "namespace mozilla::dom::quota {} // namespace mozilla::dom::quota",
3776            "foo.cpp",
3777            |metric| {
3778                // Spaces: 2
3779                insta::assert_json_snapshot!(
3780                    metric.loc,
3781                    @r#"
3782                {
3783                  "sloc": 1,
3784                  "ploc": 1,
3785                  "lloc": 0,
3786                  "cloc": 1,
3787                  "blank": 0,
3788                  "dloc": 0,
3789                  "sloc_average": 0.5,
3790                  "ploc_average": 0.5,
3791                  "lloc_average": 0.0,
3792                  "cloc_average": 0.5,
3793                  "blank_average": 0.0,
3794                  "dloc_average": 0.0,
3795                  "sloc_min": 1,
3796                  "sloc_max": 1,
3797                  "cloc_min": 0,
3798                  "cloc_max": 1,
3799                  "ploc_min": 1,
3800                  "ploc_max": 1,
3801                  "lloc_min": 0,
3802                  "lloc_max": 0,
3803                  "blank_min": 0,
3804                  "blank_max": 0,
3805                  "dloc_min": 0,
3806                  "dloc_max": 0
3807                }
3808                "#
3809                );
3810            },
3811        );
3812    }
3813
3814    #[cfg(feature = "java")]
3815    #[test]
3816    fn java_comments() {
3817        check_metrics::<JavaParser>(
3818            "for (int i = 0; i < 100; i++) { \
3819               // Print hello
3820               System.out.println(\"hello\"); \
3821               // Print world
3822               System.out.println(\"hello\"); \
3823             }",
3824            "foo.java",
3825            |metric| {
3826                // Spaces: 1
3827                insta::assert_json_snapshot!(
3828                    metric.loc,
3829                    @r#"
3830                {
3831                  "sloc": 3,
3832                  "ploc": 3,
3833                  "lloc": 3,
3834                  "cloc": 2,
3835                  "blank": 0,
3836                  "dloc": 0,
3837                  "sloc_average": 3.0,
3838                  "ploc_average": 3.0,
3839                  "lloc_average": 3.0,
3840                  "cloc_average": 2.0,
3841                  "blank_average": 0.0,
3842                  "dloc_average": 0.0,
3843                  "sloc_min": 3,
3844                  "sloc_max": 3,
3845                  "cloc_min": 2,
3846                  "cloc_max": 2,
3847                  "ploc_min": 3,
3848                  "ploc_max": 3,
3849                  "lloc_min": 3,
3850                  "lloc_max": 3,
3851                  "blank_min": 0,
3852                  "blank_max": 0,
3853                  "dloc_min": 0,
3854                  "dloc_max": 0
3855                }
3856                "#
3857                );
3858            },
3859        );
3860    }
3861
3862    #[cfg(feature = "java")]
3863    #[test]
3864    fn java_blank() {
3865        check_metrics::<JavaParser>(
3866            "int x = 1;
3867
3868
3869            int y = 2;",
3870            "foo.java",
3871            |metric| {
3872                // Spaces: 1
3873                insta::assert_json_snapshot!(
3874                    metric.loc,
3875                    @r#"
3876                {
3877                  "sloc": 4,
3878                  "ploc": 2,
3879                  "lloc": 2,
3880                  "cloc": 0,
3881                  "blank": 2,
3882                  "dloc": 0,
3883                  "sloc_average": 4.0,
3884                  "ploc_average": 2.0,
3885                  "lloc_average": 2.0,
3886                  "cloc_average": 0.0,
3887                  "blank_average": 2.0,
3888                  "dloc_average": 0.0,
3889                  "sloc_min": 4,
3890                  "sloc_max": 4,
3891                  "cloc_min": 0,
3892                  "cloc_max": 0,
3893                  "ploc_min": 2,
3894                  "ploc_max": 2,
3895                  "lloc_min": 2,
3896                  "lloc_max": 2,
3897                  "blank_min": 2,
3898                  "blank_max": 2,
3899                  "dloc_min": 0,
3900                  "dloc_max": 0
3901                }
3902                "#
3903                );
3904            },
3905        );
3906    }
3907
3908    #[cfg(feature = "java")]
3909    #[test]
3910    fn java_sloc() {
3911        check_metrics::<JavaParser>(
3912            "for (int i = 0; i < 100; i++) {
3913               System.out.println(i);
3914             }",
3915            "foo.java",
3916            |metric| {
3917                // Spaces: 1
3918                insta::assert_json_snapshot!(
3919                    metric.loc,
3920                    @r#"
3921                {
3922                  "sloc": 3,
3923                  "ploc": 3,
3924                  "lloc": 2,
3925                  "cloc": 0,
3926                  "blank": 0,
3927                  "dloc": 0,
3928                  "sloc_average": 3.0,
3929                  "ploc_average": 3.0,
3930                  "lloc_average": 2.0,
3931                  "cloc_average": 0.0,
3932                  "blank_average": 0.0,
3933                  "dloc_average": 0.0,
3934                  "sloc_min": 3,
3935                  "sloc_max": 3,
3936                  "cloc_min": 0,
3937                  "cloc_max": 0,
3938                  "ploc_min": 3,
3939                  "ploc_max": 3,
3940                  "lloc_min": 2,
3941                  "lloc_max": 2,
3942                  "blank_min": 0,
3943                  "blank_max": 0,
3944                  "dloc_min": 0,
3945                  "dloc_max": 0
3946                }
3947                "#
3948                );
3949            },
3950        );
3951    }
3952
3953    #[cfg(feature = "java")]
3954    #[test]
3955    fn java_module_sloc() {
3956        check_metrics::<JavaParser>(
3957            "module helloworld{
3958              exports com.test;
3959            }",
3960            "foo.java",
3961            |metric| {
3962                // Spaces: 1
3963                insta::assert_json_snapshot!(
3964                    metric.loc,
3965                    @r#"
3966                {
3967                  "sloc": 3,
3968                  "ploc": 3,
3969                  "lloc": 0,
3970                  "cloc": 0,
3971                  "blank": 0,
3972                  "dloc": 0,
3973                  "sloc_average": 3.0,
3974                  "ploc_average": 3.0,
3975                  "lloc_average": 0.0,
3976                  "cloc_average": 0.0,
3977                  "blank_average": 0.0,
3978                  "dloc_average": 0.0,
3979                  "sloc_min": 3,
3980                  "sloc_max": 3,
3981                  "cloc_min": 0,
3982                  "cloc_max": 0,
3983                  "ploc_min": 3,
3984                  "ploc_max": 3,
3985                  "lloc_min": 0,
3986                  "lloc_max": 0,
3987                  "blank_min": 0,
3988                  "blank_max": 0,
3989                  "dloc_min": 0,
3990                  "dloc_max": 0
3991                }
3992                "#
3993                );
3994            },
3995        );
3996    }
3997
3998    #[cfg(feature = "java")]
3999    #[test]
4000    fn java_single_ploc() {
4001        check_metrics::<JavaParser>("int x = 1;", "foo.java", |metric| {
4002            // Spaces: 1
4003            insta::assert_json_snapshot!(
4004                metric.loc,
4005                @r#"
4006            {
4007              "sloc": 1,
4008              "ploc": 1,
4009              "lloc": 1,
4010              "cloc": 0,
4011              "blank": 0,
4012              "dloc": 0,
4013              "sloc_average": 1.0,
4014              "ploc_average": 1.0,
4015              "lloc_average": 1.0,
4016              "cloc_average": 0.0,
4017              "blank_average": 0.0,
4018              "dloc_average": 0.0,
4019              "sloc_min": 1,
4020              "sloc_max": 1,
4021              "cloc_min": 0,
4022              "cloc_max": 0,
4023              "ploc_min": 1,
4024              "ploc_max": 1,
4025              "lloc_min": 1,
4026              "lloc_max": 1,
4027              "blank_min": 0,
4028              "blank_max": 0,
4029              "dloc_min": 0,
4030              "dloc_max": 0
4031            }
4032            "#
4033            );
4034        });
4035    }
4036
4037    #[cfg(feature = "java")]
4038    #[test]
4039    fn java_simple_ploc() {
4040        check_metrics::<JavaParser>(
4041            "for (int i = 0; i < 100; i = i++) {
4042               System.out.println(i);
4043             }",
4044            "foo.java",
4045            |metric| {
4046                // Spaces: 1
4047                insta::assert_json_snapshot!(
4048                    metric.loc,
4049                    @r#"
4050                {
4051                  "sloc": 3,
4052                  "ploc": 3,
4053                  "lloc": 2,
4054                  "cloc": 0,
4055                  "blank": 0,
4056                  "dloc": 0,
4057                  "sloc_average": 3.0,
4058                  "ploc_average": 3.0,
4059                  "lloc_average": 2.0,
4060                  "cloc_average": 0.0,
4061                  "blank_average": 0.0,
4062                  "dloc_average": 0.0,
4063                  "sloc_min": 3,
4064                  "sloc_max": 3,
4065                  "cloc_min": 0,
4066                  "cloc_max": 0,
4067                  "ploc_min": 3,
4068                  "ploc_max": 3,
4069                  "lloc_min": 2,
4070                  "lloc_max": 2,
4071                  "blank_min": 0,
4072                  "blank_max": 0,
4073                  "dloc_min": 0,
4074                  "dloc_max": 0
4075                }
4076                "#
4077                );
4078            },
4079        );
4080    }
4081
4082    #[cfg(feature = "java")]
4083    #[test]
4084    fn java_multi_ploc() {
4085        check_metrics::<JavaParser>(
4086            "int x = 1;
4087            for (int i = 0; i < 100; i++) {
4088               System.out.println(i);
4089             }",
4090            "foo.java",
4091            |metric| {
4092                // Spaces: 1
4093                insta::assert_json_snapshot!(
4094                    metric.loc,
4095                    @r#"
4096                {
4097                  "sloc": 4,
4098                  "ploc": 4,
4099                  "lloc": 3,
4100                  "cloc": 0,
4101                  "blank": 0,
4102                  "dloc": 0,
4103                  "sloc_average": 4.0,
4104                  "ploc_average": 4.0,
4105                  "lloc_average": 3.0,
4106                  "cloc_average": 0.0,
4107                  "blank_average": 0.0,
4108                  "dloc_average": 0.0,
4109                  "sloc_min": 4,
4110                  "sloc_max": 4,
4111                  "cloc_min": 0,
4112                  "cloc_max": 0,
4113                  "ploc_min": 4,
4114                  "ploc_max": 4,
4115                  "lloc_min": 3,
4116                  "lloc_max": 3,
4117                  "blank_min": 0,
4118                  "blank_max": 0,
4119                  "dloc_min": 0,
4120                  "dloc_max": 0
4121                }
4122                "#
4123                );
4124            },
4125        );
4126    }
4127
4128    #[cfg(feature = "java")]
4129    #[test]
4130    fn java_single_statement_lloc() {
4131        check_metrics::<JavaParser>("int max = 10;", "foo.java", |metric| {
4132            // Spaces: 1
4133            insta::assert_json_snapshot!(
4134                metric.loc,
4135                @r#"
4136            {
4137              "sloc": 1,
4138              "ploc": 1,
4139              "lloc": 1,
4140              "cloc": 0,
4141              "blank": 0,
4142              "dloc": 0,
4143              "sloc_average": 1.0,
4144              "ploc_average": 1.0,
4145              "lloc_average": 1.0,
4146              "cloc_average": 0.0,
4147              "blank_average": 0.0,
4148              "dloc_average": 0.0,
4149              "sloc_min": 1,
4150              "sloc_max": 1,
4151              "cloc_min": 0,
4152              "cloc_max": 0,
4153              "ploc_min": 1,
4154              "ploc_max": 1,
4155              "lloc_min": 1,
4156              "lloc_max": 1,
4157              "blank_min": 0,
4158              "blank_max": 0,
4159              "dloc_min": 0,
4160              "dloc_max": 0
4161            }
4162            "#
4163            );
4164        });
4165    }
4166
4167    #[cfg(feature = "java")]
4168    #[test]
4169    fn java_for_lloc() {
4170        check_metrics::<JavaParser>(
4171            "for (int i = 0; i < 100; i++) { // + 1
4172               System.out.println(i); // + 1
4173             }",
4174            "foo.java",
4175            |metric| {
4176                // Spaces: 1
4177                insta::assert_json_snapshot!(
4178                    metric.loc,
4179                    @r#"
4180                {
4181                  "sloc": 3,
4182                  "ploc": 3,
4183                  "lloc": 2,
4184                  "cloc": 2,
4185                  "blank": 0,
4186                  "dloc": 0,
4187                  "sloc_average": 3.0,
4188                  "ploc_average": 3.0,
4189                  "lloc_average": 2.0,
4190                  "cloc_average": 2.0,
4191                  "blank_average": 0.0,
4192                  "dloc_average": 0.0,
4193                  "sloc_min": 3,
4194                  "sloc_max": 3,
4195                  "cloc_min": 2,
4196                  "cloc_max": 2,
4197                  "ploc_min": 3,
4198                  "ploc_max": 3,
4199                  "lloc_min": 2,
4200                  "lloc_max": 2,
4201                  "blank_min": 0,
4202                  "blank_max": 0,
4203                  "dloc_min": 0,
4204                  "dloc_max": 0
4205                }
4206                "#
4207                );
4208            },
4209        );
4210    }
4211
4212    #[cfg(feature = "java")]
4213    #[test]
4214    fn java_foreach_lloc() {
4215        check_metrics::<JavaParser>(
4216            "
4217            int arr[]={12,13,14,44}; // +1
4218            for (int i:arr) { // +1
4219               System.out.println(i); // +1
4220             }",
4221            "foo.java",
4222            |metric| {
4223                // Spaces: 1
4224                insta::assert_json_snapshot!(
4225                    metric.loc,
4226                    @r#"
4227                {
4228                  "sloc": 4,
4229                  "ploc": 4,
4230                  "lloc": 3,
4231                  "cloc": 3,
4232                  "blank": 0,
4233                  "dloc": 0,
4234                  "sloc_average": 4.0,
4235                  "ploc_average": 4.0,
4236                  "lloc_average": 3.0,
4237                  "cloc_average": 3.0,
4238                  "blank_average": 0.0,
4239                  "dloc_average": 0.0,
4240                  "sloc_min": 4,
4241                  "sloc_max": 4,
4242                  "cloc_min": 3,
4243                  "cloc_max": 3,
4244                  "ploc_min": 4,
4245                  "ploc_max": 4,
4246                  "lloc_min": 3,
4247                  "lloc_max": 3,
4248                  "blank_min": 0,
4249                  "blank_max": 0,
4250                  "dloc_min": 0,
4251                  "dloc_max": 0
4252                }
4253                "#
4254                );
4255            },
4256        );
4257    }
4258
4259    #[cfg(feature = "java")]
4260    #[test]
4261    fn java_while_lloc() {
4262        check_metrics::<JavaParser>(
4263            "
4264            int i=0; // +1
4265            while(i < 10) { // +1
4266                i++; // +1
4267                System.out.println(i); // +1
4268             }",
4269            "foo.java",
4270            |metric| {
4271                // Spaces: 1
4272                insta::assert_json_snapshot!(
4273                    metric.loc,
4274                    @r#"
4275                {
4276                  "sloc": 5,
4277                  "ploc": 5,
4278                  "lloc": 4,
4279                  "cloc": 4,
4280                  "blank": 0,
4281                  "dloc": 0,
4282                  "sloc_average": 5.0,
4283                  "ploc_average": 5.0,
4284                  "lloc_average": 4.0,
4285                  "cloc_average": 4.0,
4286                  "blank_average": 0.0,
4287                  "dloc_average": 0.0,
4288                  "sloc_min": 5,
4289                  "sloc_max": 5,
4290                  "cloc_min": 4,
4291                  "cloc_max": 4,
4292                  "ploc_min": 5,
4293                  "ploc_max": 5,
4294                  "lloc_min": 4,
4295                  "lloc_max": 4,
4296                  "blank_min": 0,
4297                  "blank_max": 0,
4298                  "dloc_min": 0,
4299                  "dloc_max": 0
4300                }
4301                "#
4302                );
4303            },
4304        );
4305    }
4306
4307    #[cfg(feature = "java")]
4308    #[test]
4309    fn java_do_while_lloc() {
4310        check_metrics::<JavaParser>(
4311            "
4312            int i=0; // +1
4313            do { // +1
4314                i++; // +1
4315                System.out.println(i); // +1
4316             } while(i < 10)",
4317            "foo.java",
4318            |metric| {
4319                // Spaces: 1
4320                insta::assert_json_snapshot!(
4321                    metric.loc,
4322                    @r#"
4323                {
4324                  "sloc": 5,
4325                  "ploc": 5,
4326                  "lloc": 4,
4327                  "cloc": 4,
4328                  "blank": 0,
4329                  "dloc": 0,
4330                  "sloc_average": 5.0,
4331                  "ploc_average": 5.0,
4332                  "lloc_average": 4.0,
4333                  "cloc_average": 4.0,
4334                  "blank_average": 0.0,
4335                  "dloc_average": 0.0,
4336                  "sloc_min": 5,
4337                  "sloc_max": 5,
4338                  "cloc_min": 4,
4339                  "cloc_max": 4,
4340                  "ploc_min": 5,
4341                  "ploc_max": 5,
4342                  "lloc_min": 4,
4343                  "lloc_max": 4,
4344                  "blank_min": 0,
4345                  "blank_max": 0,
4346                  "dloc_min": 0,
4347                  "dloc_max": 0
4348                }
4349                "#
4350                );
4351            },
4352        );
4353    }
4354
4355    #[cfg(feature = "java")]
4356    #[test]
4357    fn java_switch_lloc() {
4358        check_metrics::<JavaParser>(
4359            "switch(grade) { // +1
4360                case 'A' :
4361                   System.out.println(\"Pass with distinction\"); // +1
4362                   break; // +1
4363                case 'B' :
4364                case 'C' :
4365                   System.out.println(\"Pass\"); // +1
4366                   break; // +1
4367                case 'D' :
4368                   System.out.println(\"At risk\"); // +1
4369                case 'F' :
4370                   System.out.println(\"Fail\"); // +1
4371                   break; // +1
4372                default :
4373                   System.out.println(\"Invalid grade\"); // +1
4374             }",
4375            "foo.java",
4376            |metric| {
4377                // Spaces: 1
4378                insta::assert_json_snapshot!(
4379                    metric.loc,
4380                    @r#"
4381                {
4382                  "sloc": 16,
4383                  "ploc": 16,
4384                  "lloc": 9,
4385                  "cloc": 9,
4386                  "blank": 0,
4387                  "dloc": 0,
4388                  "sloc_average": 16.0,
4389                  "ploc_average": 16.0,
4390                  "lloc_average": 9.0,
4391                  "cloc_average": 9.0,
4392                  "blank_average": 0.0,
4393                  "dloc_average": 0.0,
4394                  "sloc_min": 16,
4395                  "sloc_max": 16,
4396                  "cloc_min": 9,
4397                  "cloc_max": 9,
4398                  "ploc_min": 16,
4399                  "ploc_max": 16,
4400                  "lloc_min": 9,
4401                  "lloc_max": 9,
4402                  "blank_min": 0,
4403                  "blank_max": 0,
4404                  "dloc_min": 0,
4405                  "dloc_max": 0
4406                }
4407                "#
4408                );
4409            },
4410        );
4411    }
4412
4413    #[cfg(feature = "java")]
4414    #[test]
4415    fn java_continue_lloc() {
4416        check_metrics::<JavaParser>(
4417            "int max = 10; // +1
4418
4419            for (int i = 0; i < max; i++) { // +1
4420                if(i % 2 == 0) { continue;} + 2
4421                System.out.println(i); // +1
4422             }",
4423            "foo.java",
4424            |metric| {
4425                // Spaces: 1
4426                insta::assert_json_snapshot!(
4427                    metric.loc,
4428                    @r#"
4429                {
4430                  "sloc": 6,
4431                  "ploc": 5,
4432                  "lloc": 5,
4433                  "cloc": 3,
4434                  "blank": 1,
4435                  "dloc": 0,
4436                  "sloc_average": 6.0,
4437                  "ploc_average": 5.0,
4438                  "lloc_average": 5.0,
4439                  "cloc_average": 3.0,
4440                  "blank_average": 1.0,
4441                  "dloc_average": 0.0,
4442                  "sloc_min": 6,
4443                  "sloc_max": 6,
4444                  "cloc_min": 3,
4445                  "cloc_max": 3,
4446                  "ploc_min": 5,
4447                  "ploc_max": 5,
4448                  "lloc_min": 5,
4449                  "lloc_max": 5,
4450                  "blank_min": 1,
4451                  "blank_max": 1,
4452                  "dloc_min": 0,
4453                  "dloc_max": 0
4454                }
4455                "#
4456                );
4457            },
4458        );
4459    }
4460
4461    #[cfg(feature = "java")]
4462    #[test]
4463    fn java_try_lloc() {
4464        check_metrics::<JavaParser>(
4465            "try { // +1
4466                int[] myNumbers = {1, 2, 3}; // +1
4467                System.out.println(myNumbers[10]); // +1
4468              } catch (Exception e) {
4469                System.out.println(e.getMessage()); // +1
4470                throw e; // +1
4471              }",
4472            "foo.java",
4473            |metric| {
4474                // Spaces: 1
4475                insta::assert_json_snapshot!(
4476                    metric.loc,
4477                    @r#"
4478                {
4479                  "sloc": 7,
4480                  "ploc": 7,
4481                  "lloc": 5,
4482                  "cloc": 5,
4483                  "blank": 0,
4484                  "dloc": 0,
4485                  "sloc_average": 7.0,
4486                  "ploc_average": 7.0,
4487                  "lloc_average": 5.0,
4488                  "cloc_average": 5.0,
4489                  "blank_average": 0.0,
4490                  "dloc_average": 0.0,
4491                  "sloc_min": 7,
4492                  "sloc_max": 7,
4493                  "cloc_min": 5,
4494                  "cloc_max": 5,
4495                  "ploc_min": 7,
4496                  "ploc_max": 7,
4497                  "lloc_min": 5,
4498                  "lloc_max": 5,
4499                  "blank_min": 0,
4500                  "blank_max": 0,
4501                  "dloc_min": 0,
4502                  "dloc_max": 0
4503                }
4504                "#
4505                );
4506            },
4507        );
4508    }
4509
4510    #[cfg(feature = "java")]
4511    #[test]
4512    fn java_class_loc() {
4513        check_metrics::<JavaParser>(
4514            "
4515            public class Person {
4516              private String name;
4517              public Person(String name){
4518                this.name = name; // +1
4519              }
4520              public String getName() {
4521                return name; // +1
4522              }
4523            }",
4524            "foo.java",
4525            |metric| {
4526                // Spaces: 4
4527                insta::assert_json_snapshot!(
4528                    metric.loc,
4529                    @r#"
4530                {
4531                  "sloc": 9,
4532                  "ploc": 9,
4533                  "lloc": 2,
4534                  "cloc": 2,
4535                  "blank": 0,
4536                  "dloc": 0,
4537                  "sloc_average": 2.25,
4538                  "ploc_average": 2.25,
4539                  "lloc_average": 0.5,
4540                  "cloc_average": 0.5,
4541                  "blank_average": 0.0,
4542                  "dloc_average": 0.0,
4543                  "sloc_min": 3,
4544                  "sloc_max": 9,
4545                  "cloc_min": 1,
4546                  "cloc_max": 2,
4547                  "ploc_min": 3,
4548                  "ploc_max": 9,
4549                  "lloc_min": 1,
4550                  "lloc_max": 2,
4551                  "blank_min": 0,
4552                  "blank_max": 0,
4553                  "dloc_min": 0,
4554                  "dloc_max": 0
4555                }
4556                "#
4557                );
4558            },
4559        );
4560    }
4561
4562    #[cfg(feature = "java")]
4563    #[test]
4564    fn java_expressions_lloc() {
4565        check_metrics::<JavaParser>(
4566            "int x = 10;                                                            // +1 local var declaration
4567            x=+89;                                                                  // +1 expression statement
4568            int y = x * 2;                                                          // +1 local var declaration
4569            IntFunction double = (n) -> n*2;                                        // +1 local var declaration
4570            int y2 = double(x);                                                     // +1 local var declaration
4571            System.out.println(\"double \" + x + \" = \" + y2);                     // +1 expression statement
4572            String message = (x % 2) == 0 ? \"Evenly done.\" : \"Oddly done.\";     // +1 local var declaration
4573            Object done = (Runnable) () -> { System.out.println(\"Done!\"); };      // +2 local var declaration + expression statement
4574            String s = \"string\";                                                  // +1 local var declaration
4575            boolean isS = (s instanceof String);                                    // +1 local var declaration
4576            done.run();                                                             // +1 expression statement
4577            ",
4578            "foo.java",
4579            |metric| {
4580                // Spaces: 1
4581                insta::assert_json_snapshot!(
4582                    metric.loc,
4583                    @r#"
4584                {
4585                  "sloc": 11,
4586                  "ploc": 11,
4587                  "lloc": 12,
4588                  "cloc": 11,
4589                  "blank": 0,
4590                  "dloc": 0,
4591                  "sloc_average": 11.0,
4592                  "ploc_average": 11.0,
4593                  "lloc_average": 12.0,
4594                  "cloc_average": 11.0,
4595                  "blank_average": 0.0,
4596                  "dloc_average": 0.0,
4597                  "sloc_min": 11,
4598                  "sloc_max": 11,
4599                  "cloc_min": 11,
4600                  "cloc_max": 11,
4601                  "ploc_min": 11,
4602                  "ploc_max": 11,
4603                  "lloc_min": 12,
4604                  "lloc_max": 12,
4605                  "blank_min": 0,
4606                  "blank_max": 0,
4607                  "dloc_min": 0,
4608                  "dloc_max": 0
4609                }
4610                "#
4611                );
4612            },
4613        );
4614    }
4615
4616    #[cfg(feature = "java")]
4617    #[test]
4618    fn java_statement_inline_loc() {
4619        check_metrics::<JavaParser>(
4620            "for (int i = 0; i < 100; i++) { System.out.println(\"hello\"); }",
4621            "foo.java",
4622            |metric| {
4623                // Spaces: 1
4624                insta::assert_json_snapshot!(
4625                    metric.loc,
4626                    @r#"
4627                {
4628                  "sloc": 1,
4629                  "ploc": 1,
4630                  "lloc": 2,
4631                  "cloc": 0,
4632                  "blank": 0,
4633                  "dloc": 0,
4634                  "sloc_average": 1.0,
4635                  "ploc_average": 1.0,
4636                  "lloc_average": 2.0,
4637                  "cloc_average": 0.0,
4638                  "blank_average": 0.0,
4639                  "dloc_average": 0.0,
4640                  "sloc_min": 1,
4641                  "sloc_max": 1,
4642                  "cloc_min": 0,
4643                  "cloc_max": 0,
4644                  "ploc_min": 1,
4645                  "ploc_max": 1,
4646                  "lloc_min": 2,
4647                  "lloc_max": 2,
4648                  "blank_min": 0,
4649                  "blank_max": 0,
4650                  "dloc_min": 0,
4651                  "dloc_max": 0
4652                }
4653                "#
4654                );
4655            },
4656        );
4657    }
4658
4659    #[cfg(feature = "java")]
4660    #[test]
4661    fn java_general_loc() {
4662        check_metrics::<JavaParser>(
4663            "int max = 100;
4664
4665            /*
4666              Loop through and print
4667                from: 0
4668                to: max
4669            */
4670            for (int i = 0; i < max; i++) {
4671               // Print the value
4672               System.out.println(i);
4673             }",
4674            "foo.java",
4675            |metric| {
4676                // Spaces: 1
4677                insta::assert_json_snapshot!(
4678                    metric.loc,
4679                    @r#"
4680                {
4681                  "sloc": 11,
4682                  "ploc": 4,
4683                  "lloc": 3,
4684                  "cloc": 6,
4685                  "blank": 1,
4686                  "dloc": 0,
4687                  "sloc_average": 11.0,
4688                  "ploc_average": 4.0,
4689                  "lloc_average": 3.0,
4690                  "cloc_average": 6.0,
4691                  "blank_average": 1.0,
4692                  "dloc_average": 0.0,
4693                  "sloc_min": 11,
4694                  "sloc_max": 11,
4695                  "cloc_min": 6,
4696                  "cloc_max": 6,
4697                  "ploc_min": 4,
4698                  "ploc_max": 4,
4699                  "lloc_min": 3,
4700                  "lloc_max": 3,
4701                  "blank_min": 1,
4702                  "blank_max": 1,
4703                  "dloc_min": 0,
4704                  "dloc_max": 0
4705                }
4706                "#
4707                );
4708            },
4709        );
4710    }
4711
4712    #[cfg(feature = "java")]
4713    #[test]
4714    fn java_main_class_loc() {
4715        check_metrics::<JavaParser>(
4716            "package com.company;
4717             /**
4718             * The HelloWorldApp class implements an application that
4719             * simply prints \"Hello World!\" to standard output.
4720             */
4721
4722            class HelloWorldApp {
4723              public void main(String[] args) {
4724                String message = args.length == 0 ? \"Hello empty world\" : \"Hello world\"; // +1 lloc : 1 var assignment
4725                System.out.println(message); // Display the string. +1 lloc
4726              }
4727            }",
4728            "foo.java",
4729            |metric| {
4730                // Spaces: 3
4731                insta::assert_json_snapshot!(
4732                    metric.loc,
4733                    @r#"
4734                {
4735                  "sloc": 12,
4736                  "ploc": 7,
4737                  "lloc": 2,
4738                  "cloc": 6,
4739                  "blank": 1,
4740                  "dloc": 4,
4741                  "sloc_average": 4.0,
4742                  "ploc_average": 2.3333333333333335,
4743                  "lloc_average": 0.6666666666666666,
4744                  "cloc_average": 2.0,
4745                  "blank_average": 0.3333333333333333,
4746                  "dloc_average": 1.3333333333333333,
4747                  "sloc_min": 4,
4748                  "sloc_max": 12,
4749                  "cloc_min": 2,
4750                  "cloc_max": 6,
4751                  "ploc_min": 4,
4752                  "ploc_max": 7,
4753                  "lloc_min": 2,
4754                  "lloc_max": 2,
4755                  "blank_min": 0,
4756                  "blank_max": 1,
4757                  "dloc_min": 0,
4758                  "dloc_max": 4
4759                }
4760                "#
4761                );
4762            },
4763        );
4764    }
4765
4766    #[cfg(feature = "go")]
4767    #[test]
4768    fn go_general_loc() {
4769        check_metrics::<GoParser>(
4770            "package main
4771
4772            // entrypoint
4773            func main() {
4774                /* loop body */
4775                for i := 0; i < 10; i++ {
4776                    fmt.Println(i)
4777                }
4778            }",
4779            "foo.go",
4780            |metric| {
4781                // Spaces: 2 (unit + main).
4782                // lloc: for_statement (+1), fmt.Println expression (+1).
4783                //       `i := 0` and `i++` inside the for-clause are gated.
4784                // cloc: 2 comments (line + block).
4785                insta::assert_json_snapshot!(
4786                    metric.loc,
4787                    @r#"
4788                {
4789                  "sloc": 9,
4790                  "ploc": 6,
4791                  "lloc": 2,
4792                  "cloc": 2,
4793                  "blank": 1,
4794                  "dloc": 1,
4795                  "sloc_average": 4.5,
4796                  "ploc_average": 3.0,
4797                  "lloc_average": 1.0,
4798                  "cloc_average": 1.0,
4799                  "blank_average": 0.5,
4800                  "dloc_average": 0.5,
4801                  "sloc_min": 6,
4802                  "sloc_max": 9,
4803                  "cloc_min": 1,
4804                  "cloc_max": 2,
4805                  "ploc_min": 5,
4806                  "ploc_max": 6,
4807                  "lloc_min": 2,
4808                  "lloc_max": 2,
4809                  "blank_min": 0,
4810                  "blank_max": 1,
4811                  "dloc_min": 0,
4812                  "dloc_max": 1
4813                }
4814                "#
4815                );
4816            },
4817        );
4818    }
4819
4820    #[cfg(feature = "go")]
4821    #[test]
4822    fn go_for_clause_does_not_double_count_lloc() {
4823        // Bare `for` body has only a return; the `for_statement` itself is the
4824        // single logical line. Confirms ShortVarDeclaration in a for-clause
4825        // does not add an extra lloc.
4826        check_metrics::<GoParser>(
4827            "package main
4828            func f(n int) int {
4829                for i := 0; i < n; i++ {
4830                    return i
4831                }
4832                return 0
4833            }",
4834            "foo.go",
4835            |metric| {
4836                // Expected lloc: for (+1), return (+1), return (+1) = 3.
4837                // Without the gate, ShortVarDeclaration would add an extra (+1).
4838                assert_eq!(metric.loc.lloc(), 3);
4839            },
4840        );
4841    }
4842
4843    #[cfg(feature = "go")]
4844    #[test]
4845    fn go_blank() {
4846        check_metrics::<GoParser>(
4847            "package main
4848
4849            func foo() {
4850                x := 1
4851
4852                y := 2
4853            }",
4854            "foo.go",
4855            |metric| {
4856                // Spaces: 2 (unit + foo).
4857                // blank: 2 (lines 2 and 5 are empty).
4858                insta::assert_json_snapshot!(
4859                    metric.loc,
4860                    @r#"
4861                {
4862                  "sloc": 7,
4863                  "ploc": 5,
4864                  "lloc": 2,
4865                  "cloc": 0,
4866                  "blank": 2,
4867                  "dloc": 0,
4868                  "sloc_average": 3.5,
4869                  "ploc_average": 2.5,
4870                  "lloc_average": 1.0,
4871                  "cloc_average": 0.0,
4872                  "blank_average": 1.0,
4873                  "dloc_average": 0.0,
4874                  "sloc_min": 5,
4875                  "sloc_max": 7,
4876                  "cloc_min": 0,
4877                  "cloc_max": 0,
4878                  "ploc_min": 4,
4879                  "ploc_max": 5,
4880                  "lloc_min": 2,
4881                  "lloc_max": 2,
4882                  "blank_min": 1,
4883                  "blank_max": 2,
4884                  "dloc_min": 0,
4885                  "dloc_max": 0
4886                }
4887                "#
4888                );
4889            },
4890        );
4891    }
4892
4893    #[cfg(feature = "go")]
4894    #[test]
4895    fn go_cloc_line_comments() {
4896        check_metrics::<GoParser>(
4897            "package main
4898
4899            // helper adds two numbers.
4900            // It returns their sum.
4901            func add(a, b int) int {
4902                // compute the result
4903                return a + b
4904            }",
4905            "foo.go",
4906            |metric| {
4907                // Spaces: 2 (unit + add).
4908                // cloc: 3 lines with `//` comments.
4909                insta::assert_json_snapshot!(
4910                    metric.loc,
4911                    @r#"
4912                {
4913                  "sloc": 8,
4914                  "ploc": 4,
4915                  "lloc": 1,
4916                  "cloc": 3,
4917                  "blank": 1,
4918                  "dloc": 2,
4919                  "sloc_average": 4.0,
4920                  "ploc_average": 2.0,
4921                  "lloc_average": 0.5,
4922                  "cloc_average": 1.5,
4923                  "blank_average": 0.5,
4924                  "dloc_average": 1.0,
4925                  "sloc_min": 4,
4926                  "sloc_max": 8,
4927                  "cloc_min": 1,
4928                  "cloc_max": 3,
4929                  "ploc_min": 3,
4930                  "ploc_max": 4,
4931                  "lloc_min": 1,
4932                  "lloc_max": 1,
4933                  "blank_min": 0,
4934                  "blank_max": 1,
4935                  "dloc_min": 0,
4936                  "dloc_max": 2
4937                }
4938                "#
4939                );
4940            },
4941        );
4942    }
4943
4944    #[cfg(feature = "go")]
4945    #[test]
4946    fn go_cloc_block_comments() {
4947        check_metrics::<GoParser>(
4948            "package main
4949
4950            /* block comment
4951               spanning two lines */
4952            func foo() {
4953                x := 1 /* inline block */
4954            }",
4955            "foo.go",
4956            |metric| {
4957                // Spaces: 2 (unit + foo).
4958                // cloc: 2-line block comment + inline block = 3 comment lines.
4959                insta::assert_json_snapshot!(
4960                    metric.loc,
4961                    @r#"
4962                {
4963                  "sloc": 7,
4964                  "ploc": 4,
4965                  "lloc": 1,
4966                  "cloc": 3,
4967                  "blank": 1,
4968                  "dloc": 2,
4969                  "sloc_average": 3.5,
4970                  "ploc_average": 2.0,
4971                  "lloc_average": 0.5,
4972                  "cloc_average": 1.5,
4973                  "blank_average": 0.5,
4974                  "dloc_average": 1.0,
4975                  "sloc_min": 3,
4976                  "sloc_max": 7,
4977                  "cloc_min": 1,
4978                  "cloc_max": 3,
4979                  "ploc_min": 3,
4980                  "ploc_max": 4,
4981                  "lloc_min": 1,
4982                  "lloc_max": 1,
4983                  "blank_min": 0,
4984                  "blank_max": 1,
4985                  "dloc_min": 0,
4986                  "dloc_max": 2
4987                }
4988                "#
4989                );
4990            },
4991        );
4992    }
4993
4994    #[cfg(feature = "go")]
4995    #[test]
4996    fn go_lloc_if_for_switch() {
4997        check_metrics::<GoParser>(
4998            "package main
4999
5000            func foo(n int) int {
5001                if n > 0 {
5002                    for i := 0; i < n; i++ {
5003                        switch i {
5004                        }
5005                    }
5006                }
5007                return n
5008            }",
5009            "foo.go",
5010            |metric| {
5011                // Spaces: 2 (unit + foo).
5012                // lloc: if (+1), for (+1), switch (+1), return (+1) = 4.
5013                insta::assert_json_snapshot!(
5014                    metric.loc,
5015                    @r#"
5016                {
5017                  "sloc": 11,
5018                  "ploc": 10,
5019                  "lloc": 4,
5020                  "cloc": 0,
5021                  "blank": 1,
5022                  "dloc": 0,
5023                  "sloc_average": 5.5,
5024                  "ploc_average": 5.0,
5025                  "lloc_average": 2.0,
5026                  "cloc_average": 0.0,
5027                  "blank_average": 0.5,
5028                  "dloc_average": 0.0,
5029                  "sloc_min": 9,
5030                  "sloc_max": 11,
5031                  "cloc_min": 0,
5032                  "cloc_max": 0,
5033                  "ploc_min": 9,
5034                  "ploc_max": 10,
5035                  "lloc_min": 4,
5036                  "lloc_max": 4,
5037                  "blank_min": 0,
5038                  "blank_max": 1,
5039                  "dloc_min": 0,
5040                  "dloc_max": 0
5041                }
5042                "#
5043                );
5044            },
5045        );
5046    }
5047
5048    #[cfg(feature = "go")]
5049    #[test]
5050    fn go_lloc_go_defer() {
5051        check_metrics::<GoParser>(
5052            "package main
5053
5054            func foo() {
5055                go run()
5056                defer cleanup()
5057            }",
5058            "foo.go",
5059            |metric| {
5060                // Spaces: 2 (unit + foo).
5061                // lloc: go (+1), defer (+1) = 2.
5062                insta::assert_json_snapshot!(
5063                    metric.loc,
5064                    @r#"
5065                {
5066                  "sloc": 6,
5067                  "ploc": 5,
5068                  "lloc": 2,
5069                  "cloc": 0,
5070                  "blank": 1,
5071                  "dloc": 0,
5072                  "sloc_average": 3.0,
5073                  "ploc_average": 2.5,
5074                  "lloc_average": 1.0,
5075                  "cloc_average": 0.0,
5076                  "blank_average": 0.5,
5077                  "dloc_average": 0.0,
5078                  "sloc_min": 4,
5079                  "sloc_max": 6,
5080                  "cloc_min": 0,
5081                  "cloc_max": 0,
5082                  "ploc_min": 4,
5083                  "ploc_max": 5,
5084                  "lloc_min": 2,
5085                  "lloc_max": 2,
5086                  "blank_min": 0,
5087                  "blank_max": 1,
5088                  "dloc_min": 0,
5089                  "dloc_max": 0
5090                }
5091                "#
5092                );
5093            },
5094        );
5095    }
5096
5097    #[cfg(feature = "go")]
5098    #[test]
5099    fn go_lloc_var_const_declarations() {
5100        check_metrics::<GoParser>(
5101            "package main
5102
5103            func foo() {
5104                var x int
5105                var y = 10
5106                const z = 42
5107                a := 3
5108                a = 4
5109            }",
5110            "foo.go",
5111            |metric| {
5112                // Spaces: 2 (unit + foo).
5113                // lloc: var (+1), var (+1), const (+1),
5114                //       short_var_decl (+1), assignment (+1) = 5.
5115                insta::assert_json_snapshot!(
5116                    metric.loc,
5117                    @r#"
5118                {
5119                  "sloc": 9,
5120                  "ploc": 8,
5121                  "lloc": 5,
5122                  "cloc": 0,
5123                  "blank": 1,
5124                  "dloc": 0,
5125                  "sloc_average": 4.5,
5126                  "ploc_average": 4.0,
5127                  "lloc_average": 2.5,
5128                  "cloc_average": 0.0,
5129                  "blank_average": 0.5,
5130                  "dloc_average": 0.0,
5131                  "sloc_min": 7,
5132                  "sloc_max": 9,
5133                  "cloc_min": 0,
5134                  "cloc_max": 0,
5135                  "ploc_min": 7,
5136                  "ploc_max": 8,
5137                  "lloc_min": 5,
5138                  "lloc_max": 5,
5139                  "blank_min": 0,
5140                  "blank_max": 1,
5141                  "dloc_min": 0,
5142                  "dloc_max": 0
5143                }
5144                "#
5145                );
5146            },
5147        );
5148    }
5149
5150    #[cfg(feature = "go")]
5151    #[test]
5152    fn go_lloc_select() {
5153        check_metrics::<GoParser>(
5154            "package main
5155
5156            func foo(ch chan int) {
5157                select {
5158                case v := <-ch:
5159                    _ = v
5160                }
5161            }",
5162            "foo.go",
5163            |metric| {
5164                // Spaces: 2 (unit + foo).
5165                // lloc: select (+1), assignment `_ = v` (+1) = 2.
5166                // `case v := <-ch:` is a receive_statement inside a
5167                // communication_case, not a ShortVarDeclaration.
5168                insta::assert_json_snapshot!(
5169                    metric.loc,
5170                    @r#"
5171                {
5172                  "sloc": 8,
5173                  "ploc": 7,
5174                  "lloc": 2,
5175                  "cloc": 0,
5176                  "blank": 1,
5177                  "dloc": 0,
5178                  "sloc_average": 4.0,
5179                  "ploc_average": 3.5,
5180                  "lloc_average": 1.0,
5181                  "cloc_average": 0.0,
5182                  "blank_average": 0.5,
5183                  "dloc_average": 0.0,
5184                  "sloc_min": 6,
5185                  "sloc_max": 8,
5186                  "cloc_min": 0,
5187                  "cloc_max": 0,
5188                  "ploc_min": 6,
5189                  "ploc_max": 7,
5190                  "lloc_min": 2,
5191                  "lloc_max": 2,
5192                  "blank_min": 0,
5193                  "blank_max": 1,
5194                  "dloc_min": 0,
5195                  "dloc_max": 0
5196                }
5197                "#
5198                );
5199            },
5200        );
5201    }
5202
5203    #[cfg(feature = "go")]
5204    #[test]
5205    fn go_sloc_multiline_function() {
5206        check_metrics::<GoParser>(
5207            "package main
5208
5209            func add(
5210                a int,
5211                b int,
5212            ) int {
5213                return a + b
5214            }",
5215            "foo.go",
5216            |metric| {
5217                // Spaces: 2 (unit + add).
5218                // The multi-line signature should count each line as sloc.
5219                insta::assert_json_snapshot!(
5220                    metric.loc,
5221                    @r#"
5222                {
5223                  "sloc": 8,
5224                  "ploc": 7,
5225                  "lloc": 1,
5226                  "cloc": 0,
5227                  "blank": 1,
5228                  "dloc": 0,
5229                  "sloc_average": 4.0,
5230                  "ploc_average": 3.5,
5231                  "lloc_average": 0.5,
5232                  "cloc_average": 0.0,
5233                  "blank_average": 0.5,
5234                  "dloc_average": 0.0,
5235                  "sloc_min": 6,
5236                  "sloc_max": 8,
5237                  "cloc_min": 0,
5238                  "cloc_max": 0,
5239                  "ploc_min": 6,
5240                  "ploc_max": 7,
5241                  "lloc_min": 1,
5242                  "lloc_max": 1,
5243                  "blank_min": 0,
5244                  "blank_max": 1,
5245                  "dloc_min": 0,
5246                  "dloc_max": 0
5247                }
5248                "#
5249                );
5250            },
5251        );
5252    }
5253
5254    #[cfg(feature = "go")]
5255    #[test]
5256    fn go_code_comment_same_line() {
5257        check_metrics::<GoParser>(
5258            "package main
5259
5260            func foo() {
5261                x := 1 // initialize x
5262                y := 2 // initialize y
5263            }",
5264            "foo.go",
5265            |metric| {
5266                // Spaces: 2 (unit + foo).
5267                // cloc: 2 (inline comments on code lines).
5268                // blank: 1 (line between package and func).
5269                // The code+comment lines should count for both ploc and cloc.
5270                insta::assert_json_snapshot!(
5271                    metric.loc,
5272                    @r#"
5273                {
5274                  "sloc": 6,
5275                  "ploc": 5,
5276                  "lloc": 2,
5277                  "cloc": 2,
5278                  "blank": 1,
5279                  "dloc": 0,
5280                  "sloc_average": 3.0,
5281                  "ploc_average": 2.5,
5282                  "lloc_average": 1.0,
5283                  "cloc_average": 1.0,
5284                  "blank_average": 0.5,
5285                  "dloc_average": 0.0,
5286                  "sloc_min": 4,
5287                  "sloc_max": 6,
5288                  "cloc_min": 2,
5289                  "cloc_max": 2,
5290                  "ploc_min": 4,
5291                  "ploc_max": 5,
5292                  "lloc_min": 2,
5293                  "lloc_max": 2,
5294                  "blank_min": 0,
5295                  "blank_max": 1,
5296                  "dloc_min": 0,
5297                  "dloc_max": 0
5298                }
5299                "#
5300                );
5301            },
5302        );
5303    }
5304
5305    #[cfg(feature = "perl")]
5306    #[test]
5307    fn perl_grammar_smoke() {
5308        // Pin the contract that tree-sitter-perl 1.1.2 cleanly parses every
5309        // Perl construct exercised by the rest of the `perl_*` test suite.
5310        // If a future grammar bump turns one of these into an error tree,
5311        // the metric assertions might still pass numerically by coincidence;
5312        // this test fails loudly instead.
5313        assert_perl_parses_cleanly(
5314            "use strict;
5315use warnings;
5316
5317# line comment
5318
5319=pod
5320multi-line POD
5321=cut
5322
5323sub factorial {
5324    my ($n) = @_;
5325    return 1 if $n <= 1;
5326    return $n * factorial($n - 1);
5327}
5328
5329my @arr = (1, 2, 3);
5330my %hash = (a => 1, b => 2);
5331my $closure = sub { return $_[0] + 1; };
5332
5333for my $i (1..3) {
5334    if ($i % 2 == 0) {
5335        print \"even\\n\";
5336    } elsif ($i == 1) {
5337        print \"one\\n\";
5338    } else {
5339        print \"odd\\n\";
5340    }
5341}
5342
5343while ($x > 0) {
5344    last if $x == 0;
5345    $x--;
5346}
5347
5348unless ($done) {
5349    next;
5350}
5351
5352my $heredoc = <<END;
5353hello
5354END
5355",
5356        );
5357    }
5358
5359    #[cfg(feature = "perl")]
5360    #[test]
5361    fn perl_blank() {
5362        check_metrics::<PerlParser>(
5363            "
5364
5365my $a = 42;
5366
5367my $b = 43;
5368
5369",
5370            "foo.pl",
5371            |metric| {
5372                insta::assert_json_snapshot!(metric.loc, @r#"
5373                {
5374                  "sloc": 3,
5375                  "ploc": 2,
5376                  "lloc": 2,
5377                  "cloc": 0,
5378                  "blank": 1,
5379                  "dloc": 0,
5380                  "sloc_average": 3.0,
5381                  "ploc_average": 2.0,
5382                  "lloc_average": 2.0,
5383                  "cloc_average": 0.0,
5384                  "blank_average": 1.0,
5385                  "dloc_average": 0.0,
5386                  "sloc_min": 3,
5387                  "sloc_max": 3,
5388                  "cloc_min": 0,
5389                  "cloc_max": 0,
5390                  "ploc_min": 2,
5391                  "ploc_max": 2,
5392                  "lloc_min": 2,
5393                  "lloc_max": 2,
5394                  "blank_min": 1,
5395                  "blank_max": 1,
5396                  "dloc_min": 0,
5397                  "dloc_max": 0
5398                }
5399                "#);
5400            },
5401        );
5402    }
5403
5404    #[cfg(feature = "perl")]
5405    #[test]
5406    fn perl_no_zero_blank() {
5407        // Blank line interleaved with code that carries trailing comments —
5408        // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
5409        check_metrics::<PerlParser>(
5410            "my $a = 1;
5411my $b = 2;
5412
5413my $c = 3; # trailing
5414my $d = 4; # trailing
5415my $e = 5;",
5416            "foo.pl",
5417            |metric| {
5418                assert_eq!(metric.loc.sloc(), 6);
5419                assert_eq!(metric.loc.ploc(), 5);
5420                assert_eq!(metric.loc.cloc(), 2);
5421                assert_eq!(metric.loc.blank(), 1);
5422                insta::assert_json_snapshot!(metric.loc);
5423            },
5424        );
5425    }
5426
5427    #[cfg(feature = "perl")]
5428    #[test]
5429    fn perl_blank_zero_sanity() {
5430        // Sanity check: blank must report 0, never go negative, when the
5431        // input has no blank lines.
5432        check_metrics::<PerlParser>(
5433            "my $a = 1;
5434my $b = 2;",
5435            "foo.pl",
5436            |metric| {
5437                assert_eq!(metric.loc.sloc(), 2);
5438                assert_eq!(metric.loc.ploc(), 2);
5439                assert_eq!(metric.loc.lloc(), 2);
5440                assert_eq!(metric.loc.cloc(), 0);
5441                assert_eq!(metric.loc.blank(), 0);
5442            },
5443        );
5444    }
5445
5446    /// expected: row 0 is comment-only, row 1 is code carrying a trailing
5447    /// comment, row 2 is code — so `ploc 2` and `cloc 2`, with row 1 in
5448    /// both tallies. This pinned `ploc 3` until #1137: the `#` token
5449    /// inside the `comments` node reached the PLOC catch-all, which also
5450    /// reclassified row 0 from comment-only to code-and-comment.
5451    #[cfg(feature = "perl")]
5452    #[test]
5453    fn perl_cloc_line_comments() {
5454        check_metrics::<PerlParser>(
5455            "# top comment
5456my $a = 1; # trailing
5457my $b = 2;",
5458            "foo.pl",
5459            |metric| {
5460                insta::assert_json_snapshot!(metric.loc, @r#"
5461                {
5462                  "sloc": 3,
5463                  "ploc": 2,
5464                  "lloc": 2,
5465                  "cloc": 2,
5466                  "blank": 0,
5467                  "dloc": 0,
5468                  "sloc_average": 3.0,
5469                  "ploc_average": 2.0,
5470                  "lloc_average": 2.0,
5471                  "cloc_average": 2.0,
5472                  "blank_average": 0.0,
5473                  "dloc_average": 0.0,
5474                  "sloc_min": 3,
5475                  "sloc_max": 3,
5476                  "cloc_min": 2,
5477                  "cloc_max": 2,
5478                  "ploc_min": 2,
5479                  "ploc_max": 2,
5480                  "lloc_min": 2,
5481                  "lloc_max": 2,
5482                  "blank_min": 0,
5483                  "blank_max": 0,
5484                  "dloc_min": 0,
5485                  "dloc_max": 0
5486                }
5487                "#);
5488            },
5489        );
5490    }
5491
5492    #[cfg(feature = "perl")]
5493    #[test]
5494    fn perl_cloc_pod_block() {
5495        check_metrics::<PerlParser>(
5496            "my $x = 1;
5497=pod
5498multi-line
5499pod block
5500=cut
5501my $y = 2;",
5502            "foo.pl",
5503            |metric| {
5504                insta::assert_json_snapshot!(metric.loc, @r#"
5505                {
5506                  "sloc": 6,
5507                  "ploc": 2,
5508                  "lloc": 2,
5509                  "cloc": 4,
5510                  "blank": 0,
5511                  "dloc": 4,
5512                  "sloc_average": 6.0,
5513                  "ploc_average": 2.0,
5514                  "lloc_average": 2.0,
5515                  "cloc_average": 4.0,
5516                  "blank_average": 0.0,
5517                  "dloc_average": 4.0,
5518                  "sloc_min": 6,
5519                  "sloc_max": 6,
5520                  "cloc_min": 4,
5521                  "cloc_max": 4,
5522                  "ploc_min": 2,
5523                  "ploc_max": 2,
5524                  "lloc_min": 2,
5525                  "lloc_max": 2,
5526                  "blank_min": 0,
5527                  "blank_max": 0,
5528                  "dloc_min": 4,
5529                  "dloc_max": 4
5530                }
5531                "#);
5532            },
5533        );
5534    }
5535
5536    #[cfg(feature = "perl")]
5537    #[test]
5538    fn perl_lloc_simple_statements() {
5539        check_metrics::<PerlParser>(
5540            "my $a = 1;
5541my $b = 2;
5542my $c = 3;",
5543            "foo.pl",
5544            |metric| {
5545                insta::assert_json_snapshot!(metric.loc, @r#"
5546                {
5547                  "sloc": 3,
5548                  "ploc": 3,
5549                  "lloc": 3,
5550                  "cloc": 0,
5551                  "blank": 0,
5552                  "dloc": 0,
5553                  "sloc_average": 3.0,
5554                  "ploc_average": 3.0,
5555                  "lloc_average": 3.0,
5556                  "cloc_average": 0.0,
5557                  "blank_average": 0.0,
5558                  "dloc_average": 0.0,
5559                  "sloc_min": 3,
5560                  "sloc_max": 3,
5561                  "cloc_min": 0,
5562                  "cloc_max": 0,
5563                  "ploc_min": 3,
5564                  "ploc_max": 3,
5565                  "lloc_min": 3,
5566                  "lloc_max": 3,
5567                  "blank_min": 0,
5568                  "blank_max": 0,
5569                  "dloc_min": 0,
5570                  "dloc_max": 0
5571                }
5572                "#);
5573            },
5574        );
5575    }
5576
5577    #[cfg(feature = "perl")]
5578    #[test]
5579    fn perl_lloc_compound_statements() {
5580        check_metrics::<PerlParser>(
5581            "if ($x) {
5582    print 'a';
5583}
5584while ($n > 0) {
5585    $n--;
5586}",
5587            "foo.pl",
5588            |metric| {
5589                insta::assert_json_snapshot!(metric.loc, @r#"
5590                {
5591                  "sloc": 6,
5592                  "ploc": 6,
5593                  "lloc": 4,
5594                  "cloc": 0,
5595                  "blank": 0,
5596                  "dloc": 0,
5597                  "sloc_average": 6.0,
5598                  "ploc_average": 6.0,
5599                  "lloc_average": 4.0,
5600                  "cloc_average": 0.0,
5601                  "blank_average": 0.0,
5602                  "dloc_average": 0.0,
5603                  "sloc_min": 6,
5604                  "sloc_max": 6,
5605                  "cloc_min": 0,
5606                  "cloc_max": 0,
5607                  "ploc_min": 6,
5608                  "ploc_max": 6,
5609                  "lloc_min": 4,
5610                  "lloc_max": 4,
5611                  "blank_min": 0,
5612                  "blank_max": 0,
5613                  "dloc_min": 0,
5614                  "dloc_max": 0
5615                }
5616                "#);
5617            },
5618        );
5619    }
5620
5621    #[cfg(feature = "perl")]
5622    #[test]
5623    fn perl_lloc_postfix_form_counts_once() {
5624        // `do_thing() if cond;` is one logical line — wrapped in
5625        // single_line_statement; the inner if_simple_statement does not
5626        // add a second LLOC.
5627        check_metrics::<PerlParser>(
5628            "sub f {
5629    return 1 if $_[0];
5630}",
5631            "foo.pl",
5632            |metric| {
5633                assert_eq!(metric.loc.lloc(), 1);
5634            },
5635        );
5636    }
5637
5638    #[cfg(feature = "perl")]
5639    #[test]
5640    fn perl_lloc_use_statement() {
5641        check_metrics::<PerlParser>(
5642            "use strict;
5643use warnings;
5644my $x = 1;",
5645            "foo.pl",
5646            |metric| {
5647                insta::assert_json_snapshot!(metric.loc, @r#"
5648                {
5649                  "sloc": 3,
5650                  "ploc": 3,
5651                  "lloc": 3,
5652                  "cloc": 0,
5653                  "blank": 0,
5654                  "dloc": 0,
5655                  "sloc_average": 3.0,
5656                  "ploc_average": 3.0,
5657                  "lloc_average": 3.0,
5658                  "cloc_average": 0.0,
5659                  "blank_average": 0.0,
5660                  "dloc_average": 0.0,
5661                  "sloc_min": 3,
5662                  "sloc_max": 3,
5663                  "cloc_min": 0,
5664                  "cloc_max": 0,
5665                  "ploc_min": 3,
5666                  "ploc_max": 3,
5667                  "lloc_min": 3,
5668                  "lloc_max": 3,
5669                  "blank_min": 0,
5670                  "blank_max": 0,
5671                  "dloc_min": 0,
5672                  "dloc_max": 0
5673                }
5674                "#);
5675            },
5676        );
5677    }
5678
5679    #[cfg(feature = "perl")]
5680    #[test]
5681    fn perl_lloc_for_loop() {
5682        check_metrics::<PerlParser>(
5683            "for my $i (1..3) {
5684    print $i;
5685}",
5686            "foo.pl",
5687            |metric| {
5688                // `for_statement_2` (+1) and `print …;` SEMI in block (+1) → 2
5689                assert_eq!(metric.loc.lloc(), 2);
5690            },
5691        );
5692    }
5693
5694    #[cfg(feature = "perl")]
5695    #[test]
5696    fn perl_lloc_loop_control_statement() {
5697        check_metrics::<PerlParser>(
5698            "while (1) {
5699    last if $done;
5700}",
5701            "foo.pl",
5702            |metric| {
5703                // while_statement (+1) + loop_control_statement (+1) = 2
5704                assert_eq!(metric.loc.lloc(), 2);
5705            },
5706        );
5707    }
5708
5709    #[cfg(feature = "perl")]
5710    #[test]
5711    fn perl_lloc_no_double_count_inside_single_line_statement() {
5712        // SEMI inside a single_line_statement (postfix form) is a child of
5713        // if_simple_statement, not Block — so it must not add a second LLOC.
5714        check_metrics::<PerlParser>(
5715            "sub f {
5716    print 'a' unless $_[0];
5717}",
5718            "foo.pl",
5719            |metric| {
5720                assert_eq!(metric.loc.lloc(), 1);
5721            },
5722        );
5723    }
5724
5725    #[cfg(feature = "perl")]
5726    #[test]
5727    fn perl_lloc_function_definition_not_counted() {
5728        // `sub f { ... }` itself is a function space, not an LLOC; only its
5729        // body statements count.
5730        check_metrics::<PerlParser>(
5731            "sub f {
5732    my $x = 1;
5733}",
5734            "foo.pl",
5735            |metric| {
5736                assert_eq!(metric.loc.lloc(), 1);
5737            },
5738        );
5739    }
5740
5741    #[cfg(feature = "perl")]
5742    #[test]
5743    fn perl_lloc_anonymous_function() {
5744        // `my $f = sub { return 1; };` — the assignment is one LLOC at the
5745        // top level (the SEMI after `};`); the `return 1;` inside the
5746        // anonymous function block is a second LLOC inside the closure.
5747        check_metrics::<PerlParser>("my $f = sub { return 1; };", "foo.pl", |metric| {
5748            assert_eq!(metric.loc.lloc(), 2);
5749        });
5750    }
5751
5752    #[cfg(feature = "perl")]
5753    #[test]
5754    fn perl_multiline_string_assignment_ploc() {
5755        // Regression test for issue #778: interior rows of a multi-line string
5756        // literal are real code, not blank lines, and must be credited to PLOC
5757        // exactly as Python does (#415). Previously Perl no-op'd its string
5758        // kinds, so row 1 reached neither PLOC nor CLOC and `blank =
5759        // sloc - ploc - cloc` mislabelled it as blank (ploc was 2, blank 1).
5760        // Row 0 holds `my $s = "line1`, row 1 `line2`, row 2 `line3";`.
5761        check_metrics::<PerlParser>(
5762            "my $s = \"line1
5763line2
5764line3\";",
5765            "foo.pl",
5766            |metric| {
5767                // Three physical rows, all code, no blanks — matching Python.
5768                assert_eq!(metric.loc.sloc(), 3);
5769                assert_eq!(metric.loc.ploc(), 3);
5770                assert_eq!(metric.loc.cloc(), 0);
5771                assert_eq!(metric.loc.blank(), 0);
5772            },
5773        );
5774    }
5775
5776    // The union of the fixture languages below, so a feature subset that
5777    // enables none of them makes this test *absent* rather than failing
5778    // in a way that reads as a defect in whatever is being changed
5779    // (`.claude/rules/testing.md`). Every call site carries its own
5780    // narrower gate, so if the test exists at least one row runs and it
5781    // cannot go vacuous.
5782    #[cfg(any(
5783        feature = "bash",
5784        feature = "c",
5785        feature = "elixir",
5786        feature = "go",
5787        feature = "irules",
5788        feature = "kotlin",
5789        feature = "mozcpp",
5790        feature = "mozjs",
5791        feature = "perl",
5792        feature = "php",
5793        feature = "python",
5794        feature = "ruby",
5795        feature = "tcl",
5796    ))]
5797    #[test]
5798    fn multiline_string_ploc_consistent_across_languages() {
5799        // Cross-language parity for issue #778: the SAME 3-line string
5800        // assignment must report identical ploc / blank in every language
5801        // that has a multi-line string literal. The canonical value is
5802        // Python's #415 decision: all three rows are code, none are blank.
5803        // `check_metrics` takes a plain `fn(CodeMetrics)`, so the shared
5804        // assertion is a named function rather than a capturing closure and
5805        // must take its argument by value to match that pointer type.
5806        //
5807        // The middle row is **empty**, and that is the point (#1412).
5808        // Until then every fixture here read `line1\nline2\nline3`, which
5809        // a language passes whether or not it credits an interior row
5810        // with no text — so the table could not see either #1396 (PHP) or
5811        // #1412 (Bash), the two defects it is the cross-language home
5812        // for. C is the one language that cannot express the shape: its
5813        // multi-row literal is a backslash-newline continuation, and an
5814        // unescaped empty row ends the string, so its row keeps `line2`.
5815        //
5816        // Gated on its own callers rather than on the test's union: Go is
5817        // in the union and is the one row with different expectations, so
5818        // an ungated helper is dead code in a Go-only build.
5819        #[cfg(any(
5820            feature = "bash",
5821            feature = "c",
5822            feature = "elixir",
5823            feature = "irules",
5824            feature = "kotlin",
5825            feature = "mozcpp",
5826            feature = "mozjs",
5827            feature = "perl",
5828            feature = "php",
5829            feature = "python",
5830            feature = "ruby",
5831            feature = "tcl",
5832        ))]
5833        #[allow(clippy::needless_pass_by_value)]
5834        fn assert_three_code_rows(metric: crate::CodeMetrics) {
5835            assert_eq!(metric.loc.sloc(), 3);
5836            assert_eq!(metric.loc.ploc(), 3);
5837            assert_eq!(metric.loc.cloc(), 0);
5838            assert_eq!(metric.loc.blank(), 0);
5839        }
5840        // The heredoc forms carry their opener and terminator on rows of
5841        // their own, so they are four rows rather than three.
5842        #[cfg(any(feature = "bash", feature = "php"))]
5843        #[allow(clippy::needless_pass_by_value)]
5844        fn assert_four_code_rows(metric: crate::CodeMetrics) {
5845            assert_eq!(metric.loc.sloc(), 4);
5846            assert_eq!(metric.loc.ploc(), 4);
5847            assert_eq!(metric.loc.cloc(), 0);
5848            assert_eq!(metric.loc.blank(), 0);
5849        }
5850        #[cfg(feature = "python")]
5851        check_metrics::<PythonParser>(
5852            "s = \"\"\"line1\n\nline3\"\"\"",
5853            "foo.py",
5854            assert_three_code_rows,
5855        );
5856        #[cfg(feature = "perl")]
5857        check_metrics::<PerlParser>(
5858            "my $s = \"line1\n\nline3\";",
5859            "foo.pl",
5860            assert_three_code_rows,
5861        );
5862        #[cfg(feature = "ruby")]
5863        check_metrics::<RubyParser>("s = \"line1\n\nline3\"", "foo.rb", assert_three_code_rows);
5864        // Go, Kotlin, and Mozilla-C++ reach the same shared
5865        // `add_multiline_string_ploc` helper through their own
5866        // raw-string kinds (`raw_string_literal`,
5867        // `multiline_string_literal`, `raw_string_literal`), and were
5868        // the three call sites of it that no test exercised. Each needs
5869        // its own syntax, so they cannot reuse the quoted form above.
5870        #[cfg(feature = "go")]
5871        check_metrics::<GoParser>(
5872            "package p\n\nvar s = `line1\n\nline3`",
5873            "foo.go",
5874            |metric| {
5875                // Two extra code rows for `package p` and the blank
5876                // between it and the declaration, which Go requires.
5877                assert_eq!(metric.loc.sloc(), 5);
5878                assert_eq!(metric.loc.ploc(), 4);
5879                assert_eq!(metric.loc.cloc(), 0);
5880                assert_eq!(metric.loc.blank(), 1);
5881            },
5882        );
5883        #[cfg(feature = "kotlin")]
5884        check_metrics::<KotlinParser>(
5885            "val s = \"\"\"line1\n\nline3\"\"\"",
5886            "foo.kt",
5887            assert_three_code_rows,
5888        );
5889        #[cfg(feature = "mozcpp")]
5890        check_metrics::<MozcppParser>(
5891            "const char* s = R\"(line1\n\nline3)\";",
5892            "foo.cpp",
5893            assert_three_code_rows,
5894        );
5895        // C has no raw string; tree-sitter-c folds a backslash-newline
5896        // continuation into one `string_literal` spanning every row, and
5897        // that continuation is why the C row below is the one that keeps
5898        // a non-empty middle line.
5899        // Mozjs carries the same `template_string` arm as its upstream
5900        // JS siblings, but only `.jsm` routes to it, so the JavaScript
5901        // and TypeScript template tests never reach this copy.
5902        #[cfg(feature = "c")]
5903        check_metrics::<CParser>(
5904            "const char* s = \"line1\\\nline2\\\nline3\";",
5905            "foo.c",
5906            assert_three_code_rows,
5907        );
5908        #[cfg(feature = "mozjs")]
5909        check_metrics::<MozjsParser>(
5910            "const s = `line1\n\nline3`;",
5911            "foo.jsm",
5912            assert_three_code_rows,
5913        );
5914        // The four languages #778 missed, added by #1260. Bash's shape is
5915        // the single-quoted `raw_string`; Tcl and its iRules dialect spell
5916        // the literal `quoted_word`; Elixir routes the `quoted_content`
5917        // every one of its string forms wraps.
5918        #[cfg(feature = "bash")]
5919        check_metrics::<BashParser>("s='line1\n\nline3'", "foo.sh", assert_three_code_rows);
5920        #[cfg(feature = "tcl")]
5921        check_metrics::<TclParser>(
5922            "set s \"line1\n\nline3\"",
5923            "foo.tcl",
5924            assert_three_code_rows,
5925        );
5926        #[cfg(feature = "irules")]
5927        check_metrics::<IrulesParser>(
5928            "set s \"line1\n\nline3\"",
5929            "foo.irule",
5930            assert_three_code_rows,
5931        );
5932        #[cfg(feature = "elixir")]
5933        check_metrics::<ElixirParser>("s = \"line1\n\nline3\"", "foo.ex", assert_three_code_rows);
5934        // PHP was absent from this table entirely — including for the
5935        // quoted forms #778 routed — which is half of why #1396 survived
5936        // #778.
5937        #[cfg(feature = "php")]
5938        check_metrics::<PhpParser>(
5939            "<?php $s = \"line1\n\nline3\";",
5940            "foo.php",
5941            assert_three_code_rows,
5942        );
5943        // The two heredocs. Neither language's quoted form above can
5944        // reach the defect its heredoc had: PHP's `heredoc` wrapper
5945        // (#1396) and Bash's `heredoc_redirect` (#1412) are separate
5946        // grammar nodes from the quoted literal, and each dropped a body
5947        // row that had no text.
5948        #[cfg(feature = "php")]
5949        check_metrics::<PhpParser>(
5950            "<?php $s = <<<EOT\n\nline3\nEOT;",
5951            "foo.php",
5952            assert_four_code_rows,
5953        );
5954        #[cfg(feature = "bash")]
5955        check_metrics::<BashParser>("cat <<EOT\n\nline3\nEOT", "foo.sh", assert_four_code_rows);
5956    }
5957
5958    #[cfg(any(
5959        feature = "bash",
5960        feature = "elixir",
5961        feature = "irules",
5962        feature = "php",
5963        feature = "tcl",
5964    ))]
5965    #[test]
5966    fn multiline_literal_ending_at_eof_credits_every_row() {
5967        // The #1260 fix lives in exactly the class both metric harnesses
5968        // normalise away: `check_metrics` and the integration
5969        // `read_file_with_eol` path each force a trailing newline, so a
5970        // node ending at EOF is unreachable from either and a test written
5971        // the ordinary way passes against unfixed code (#1051, #1067 —
5972        // `.claude/rules/testing.md`). These fixtures reach the parser
5973        // byte-for-byte through `metrics_verbatim`, with the closing
5974        // delimiter as the last byte of the file.
5975        //
5976        // The interior row is empty in every fixture, for the reason the
5977        // sibling table above records: `line1\nline2\nline3` passes
5978        // whether or not a textless interior row is credited, so the
5979        // table could not see #1396 or #1412.
5980        //
5981        // `(language, source, rows the file has)`. Every row is fully
5982        // code and none blank.
5983        const FIXTURES: &[(crate::LANG, &[u8], u64)] = &[
5984            #[cfg(feature = "bash")]
5985            (crate::LANG::Bash, b"s='line1\n\nline3'", 3),
5986            #[cfg(feature = "tcl")]
5987            (crate::LANG::Tcl, b"set s \"line1\n\nline3\"", 3),
5988            #[cfg(feature = "irules")]
5989            (crate::LANG::Irules, b"set s \"line1\n\nline3\"", 3),
5990            #[cfg(feature = "elixir")]
5991            (crate::LANG::Elixir, b"s = \"line1\n\nline3\"", 3),
5992            #[cfg(feature = "php")]
5993            (crate::LANG::Php, b"<?php $s = \"line1\n\nline3\";", 3),
5994            // The heredocs, whose terminator is the file's last row —
5995            // the one shape a heredoc can end at EOF in.
5996            #[cfg(feature = "php")]
5997            (crate::LANG::Php, b"<?php $s = <<<EOT\n\nline3\nEOT;", 4),
5998            #[cfg(feature = "bash")]
5999            (crate::LANG::Bash, b"cat <<EOT\n\nline3\nEOT", 4),
6000            // Empty throughout, which is the shape whose body node
6001            // collapses to zero width (#1412).
6002            #[cfg(feature = "bash")]
6003            (crate::LANG::Bash, b"cat <<EOT\n\nEOT", 3),
6004        ];
6005
6006        crate::test_support::assert_fixtures_present(FIXTURES);
6007        for &(lang, source, rows) in FIXTURES {
6008            let text = String::from_utf8_lossy(source);
6009            let loc = metrics_verbatim(lang, source, MetricsOptions::default()).loc;
6010            assert_eq!(loc.sloc(), rows, "{lang:?} sloc for {text:?}");
6011            assert_eq!(loc.ploc(), rows, "{lang:?} ploc for {text:?}");
6012            assert_eq!(loc.cloc(), 0, "{lang:?} cloc for {text:?}");
6013            assert_eq!(loc.blank(), 0, "{lang:?} blank for {text:?}");
6014        }
6015    }
6016
6017    #[cfg(feature = "perl")]
6018    #[test]
6019    fn perl_lloc_unless_until() {
6020        check_metrics::<PerlParser>(
6021            "unless ($x) {
6022    print 'a';
6023}
6024until ($n == 0) {
6025    $n--;
6026}",
6027            "foo.pl",
6028            |metric| {
6029                // unless_statement (+1) + print SEMI (+1) + until_statement (+1)
6030                // + $n-- SEMI (+1) = 4
6031                assert_eq!(metric.loc.lloc(), 4);
6032            },
6033        );
6034    }
6035
6036    #[cfg(feature = "perl")]
6037    #[test]
6038    fn perl_lloc_heredoc_body_not_counted() {
6039        // Heredoc body content is data, not code: the body lines should not
6040        // contribute LLOC or PLOC.
6041        check_metrics::<PerlParser>(
6042            "my $s = <<END;
6043line1
6044line2
6045END
6046my $x = 1;",
6047            "foo.pl",
6048            |metric| {
6049                // Two top-level statements: the heredoc-using `my $s = …;`
6050                // and `my $x = 1;`.
6051                assert_eq!(metric.loc.lloc(), 2);
6052            },
6053        );
6054        // Independent confirmation that the snippet is a valid heredoc and
6055        // not silently parsed as an error tree (which could otherwise yield
6056        // the same `lloc == 2.0` and mask a grammar regression).
6057        assert_perl_parses_cleanly(
6058            "my $s = <<END;
6059line1
6060line2
6061END
6062my $x = 1;",
6063        );
6064    }
6065
6066    #[cfg(feature = "perl")]
6067    #[test]
6068    fn perl_lloc_package_and_require() {
6069        check_metrics::<PerlParser>(
6070            "package Foo;
6071require 5.010;
6072my $x = 1;",
6073            "foo.pl",
6074            |metric| {
6075                insta::assert_json_snapshot!(metric.loc, @r#"
6076                {
6077                  "sloc": 3,
6078                  "ploc": 3,
6079                  "lloc": 3,
6080                  "cloc": 0,
6081                  "blank": 0,
6082                  "dloc": 0,
6083                  "sloc_average": 3.0,
6084                  "ploc_average": 3.0,
6085                  "lloc_average": 3.0,
6086                  "cloc_average": 0.0,
6087                  "blank_average": 0.0,
6088                  "dloc_average": 0.0,
6089                  "sloc_min": 3,
6090                  "sloc_max": 3,
6091                  "cloc_min": 0,
6092                  "cloc_max": 0,
6093                  "ploc_min": 3,
6094                  "ploc_max": 3,
6095                  "lloc_min": 3,
6096                  "lloc_max": 3,
6097                  "blank_min": 0,
6098                  "blank_max": 0,
6099                  "dloc_min": 0,
6100                  "dloc_max": 0
6101                }
6102                "#);
6103            },
6104        );
6105    }
6106
6107    #[cfg(feature = "lua")]
6108    #[test]
6109    fn lua_blank() {
6110        check_metrics::<LuaParser>(
6111            "local x = 1
6112
6113local y = 2",
6114            "foo.lua",
6115            |metric| {
6116                assert_eq!(metric.loc.sloc(), 3);
6117                assert_eq!(metric.loc.ploc(), 2);
6118                assert_eq!(metric.loc.lloc(), 2);
6119                assert_eq!(metric.loc.cloc(), 0);
6120                assert_eq!(metric.loc.blank(), 1);
6121                insta::assert_json_snapshot!(metric.loc);
6122            },
6123        );
6124    }
6125
6126    #[cfg(feature = "lua")]
6127    #[test]
6128    fn lua_no_zero_blank() {
6129        // Blank line interleaved with code that carries trailing comments —
6130        // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
6131        check_metrics::<LuaParser>(
6132            "local a = 1
6133local b = 2
6134
6135local c = 3 -- trailing
6136local d = 4 -- trailing
6137local e = 5",
6138            "foo.lua",
6139            |metric| {
6140                assert_eq!(metric.loc.sloc(), 6);
6141                assert_eq!(metric.loc.ploc(), 5);
6142                assert_eq!(metric.loc.cloc(), 2);
6143                assert_eq!(metric.loc.blank(), 1);
6144                insta::assert_json_snapshot!(metric.loc);
6145            },
6146        );
6147    }
6148
6149    #[cfg(feature = "lua")]
6150    #[test]
6151    fn lua_blank_zero_sanity() {
6152        // Sanity check: blank must report 0, never go negative, when the
6153        // input has no blank lines.
6154        check_metrics::<LuaParser>(
6155            "local x = 1
6156local y = 2",
6157            "foo.lua",
6158            |metric| {
6159                assert_eq!(metric.loc.sloc(), 2);
6160                assert_eq!(metric.loc.ploc(), 2);
6161                assert_eq!(metric.loc.lloc(), 2);
6162                assert_eq!(metric.loc.cloc(), 0);
6163                assert_eq!(metric.loc.blank(), 0);
6164            },
6165        );
6166    }
6167
6168    #[cfg(feature = "lua")]
6169    #[test]
6170    fn lua_cloc() {
6171        check_metrics::<LuaParser>(
6172            "-- single line comment
6173local x = 1
6174--[[
6175  block comment
6176  second line
6177]]",
6178            "foo.lua",
6179            |metric| {
6180                assert_eq!(metric.loc.sloc(), 6);
6181                assert_eq!(metric.loc.ploc(), 1);
6182                assert_eq!(metric.loc.lloc(), 1);
6183                assert_eq!(metric.loc.cloc(), 5);
6184                assert_eq!(metric.loc.blank(), 0);
6185                insta::assert_json_snapshot!(metric.loc);
6186            },
6187        );
6188    }
6189
6190    #[cfg(feature = "lua")]
6191    #[test]
6192    fn lua_lloc() {
6193        check_metrics::<LuaParser>(
6194            "local function f(x)
6195  if x > 0 then
6196    local y = x + 1
6197    return y
6198  end
6199  return 0
6200end",
6201            "foo.lua",
6202            |metric| {
6203                assert_eq!(metric.loc.sloc(), 7);
6204                assert_eq!(metric.loc.ploc(), 7);
6205                assert_eq!(metric.loc.lloc(), 5);
6206                assert_eq!(metric.loc.cloc(), 0);
6207                assert_eq!(metric.loc.blank(), 0);
6208                insta::assert_json_snapshot!(metric.loc);
6209            },
6210        );
6211    }
6212
6213    #[cfg(feature = "lua")]
6214    #[test]
6215    fn lua_no_string_lloc() {
6216        // Long strings spanning multiple lines must not inflate lloc.
6217        check_metrics::<LuaParser>(
6218            "local s = [[
6219  line one
6220  line two
6221]]",
6222            "foo.lua",
6223            |metric| {
6224                // #778: a multi-line long-bracket string credits every spanned
6225                // row to PLOC (matching Python's #415 decision), so all four
6226                // rows are code and none are blank. It still contributes a
6227                // single lloc — the assignment statement.
6228                assert_eq!(metric.loc.sloc(), 4);
6229                assert_eq!(metric.loc.ploc(), 4);
6230                assert_eq!(metric.loc.lloc(), 1);
6231                assert_eq!(metric.loc.cloc(), 0);
6232                assert_eq!(metric.loc.blank(), 0);
6233                insta::assert_json_snapshot!(metric.loc);
6234            },
6235        );
6236    }
6237
6238    #[cfg(feature = "lua")]
6239    #[test]
6240    fn lua_no_functiondefinition_lloc() {
6241        // Anonymous function definition is an expression, not a statement.
6242        // The containing variable_declaration counts as lloc; FunctionDefinition must not.
6243        check_metrics::<LuaParser>(
6244            "local f = function(x)
6245  return x + 1
6246end",
6247            "foo.lua",
6248            |metric| {
6249                assert_eq!(metric.loc.sloc(), 3);
6250                assert_eq!(metric.loc.ploc(), 3);
6251                assert_eq!(metric.loc.lloc(), 2);
6252                assert_eq!(metric.loc.cloc(), 0);
6253                assert_eq!(metric.loc.blank(), 0);
6254                insta::assert_json_snapshot!(metric.loc);
6255            },
6256        );
6257    }
6258
6259    #[cfg(feature = "lua")]
6260    #[test]
6261    fn lua_no_elseif_lloc() {
6262        // elseif_statement must not add lloc; only if_statement does.
6263        check_metrics::<LuaParser>(
6264            "local function f(x)
6265  if x > 0 then
6266    return 1
6267  elseif x < 0 then
6268    return -1
6269  else
6270    return 0
6271  end
6272end",
6273            "foo.lua",
6274            |metric| {
6275                assert_eq!(metric.loc.sloc(), 9);
6276                assert_eq!(metric.loc.ploc(), 9);
6277                assert_eq!(metric.loc.lloc(), 5);
6278                assert_eq!(metric.loc.cloc(), 0);
6279                assert_eq!(metric.loc.blank(), 0);
6280                insta::assert_json_snapshot!(metric.loc);
6281            },
6282        );
6283    }
6284
6285    #[cfg(feature = "lua")]
6286    #[test]
6287    fn lua_no_else_lloc() {
6288        // else_statement must not add lloc.
6289        check_metrics::<LuaParser>(
6290            "local function f(x)
6291  if x > 0 then
6292    return 1
6293  else
6294    return 0
6295  end
6296end",
6297            "foo.lua",
6298            |metric| {
6299                assert_eq!(metric.loc.sloc(), 7);
6300                assert_eq!(metric.loc.ploc(), 7);
6301                assert_eq!(metric.loc.lloc(), 4);
6302                assert_eq!(metric.loc.cloc(), 0);
6303                assert_eq!(metric.loc.blank(), 0);
6304                insta::assert_json_snapshot!(metric.loc);
6305            },
6306        );
6307    }
6308
6309    #[cfg(feature = "lua")]
6310    #[test]
6311    fn lua_functiondeclaration_lloc() {
6312        // Named function declaration counts as one lloc.
6313        check_metrics::<LuaParser>(
6314            "function f()
6315  return 1
6316end",
6317            "foo.lua",
6318            |metric| {
6319                assert_eq!(metric.loc.sloc(), 3);
6320                assert_eq!(metric.loc.ploc(), 3);
6321                assert_eq!(metric.loc.lloc(), 2);
6322                assert_eq!(metric.loc.cloc(), 0);
6323                assert_eq!(metric.loc.blank(), 0);
6324                insta::assert_json_snapshot!(metric.loc);
6325            },
6326        );
6327    }
6328
6329    #[cfg(feature = "lua")]
6330    #[test]
6331    fn lua_local_function_lloc() {
6332        // local function declaration is also a function_declaration node → one lloc.
6333        check_metrics::<LuaParser>(
6334            "local function g()
6335  return 2
6336end",
6337            "foo.lua",
6338            |metric| {
6339                assert_eq!(metric.loc.sloc(), 3);
6340                assert_eq!(metric.loc.ploc(), 3);
6341                assert_eq!(metric.loc.lloc(), 2);
6342                assert_eq!(metric.loc.cloc(), 0);
6343                assert_eq!(metric.loc.blank(), 0);
6344                insta::assert_json_snapshot!(metric.loc);
6345            },
6346        );
6347    }
6348
6349    #[cfg(feature = "lua")]
6350    #[test]
6351    fn lua_for_numeric_lloc() {
6352        check_metrics::<LuaParser>(
6353            "for i = 1, 10 do
6354  print(i)
6355end",
6356            "foo.lua",
6357            |metric| {
6358                assert_eq!(metric.loc.sloc(), 3);
6359                assert_eq!(metric.loc.ploc(), 3);
6360                assert_eq!(metric.loc.lloc(), 1);
6361                assert_eq!(metric.loc.cloc(), 0);
6362                assert_eq!(metric.loc.blank(), 0);
6363                insta::assert_json_snapshot!(metric.loc);
6364            },
6365        );
6366    }
6367
6368    #[cfg(feature = "lua")]
6369    #[test]
6370    fn lua_for_generic_lloc() {
6371        check_metrics::<LuaParser>(
6372            "for k, v in pairs(t) do
6373  print(k, v)
6374end",
6375            "foo.lua",
6376            |metric| {
6377                assert_eq!(metric.loc.sloc(), 3);
6378                assert_eq!(metric.loc.ploc(), 3);
6379                assert_eq!(metric.loc.lloc(), 1);
6380                assert_eq!(metric.loc.cloc(), 0);
6381                assert_eq!(metric.loc.blank(), 0);
6382                insta::assert_json_snapshot!(metric.loc);
6383            },
6384        );
6385    }
6386
6387    #[cfg(feature = "lua")]
6388    #[test]
6389    fn lua_repeat_lloc() {
6390        check_metrics::<LuaParser>(
6391            "local i = 0
6392repeat
6393  i = i + 1
6394until i >= 10",
6395            "foo.lua",
6396            |metric| {
6397                assert_eq!(metric.loc.sloc(), 4);
6398                assert_eq!(metric.loc.ploc(), 4);
6399                assert_eq!(metric.loc.lloc(), 3);
6400                assert_eq!(metric.loc.cloc(), 0);
6401                assert_eq!(metric.loc.blank(), 0);
6402                insta::assert_json_snapshot!(metric.loc);
6403            },
6404        );
6405    }
6406
6407    #[cfg(feature = "lua")]
6408    #[test]
6409    fn lua_local_decl_lloc() {
6410        check_metrics::<LuaParser>(
6411            "local x = 1
6412local y, z = 2, 3",
6413            "foo.lua",
6414            |metric| {
6415                assert_eq!(metric.loc.sloc(), 2);
6416                assert_eq!(metric.loc.ploc(), 2);
6417                assert_eq!(metric.loc.lloc(), 2);
6418                assert_eq!(metric.loc.cloc(), 0);
6419                assert_eq!(metric.loc.blank(), 0);
6420                insta::assert_json_snapshot!(metric.loc);
6421            },
6422        );
6423    }
6424
6425    #[cfg(feature = "lua")]
6426    #[test]
6427    fn lua_function_call_lloc() {
6428        // Standalone function calls have no expression_statement wrapper in Lua.
6429        // They fall to the `_` branch → counted as ploc, not lloc.
6430        check_metrics::<LuaParser>(
6431            "print(\"hello\")
6432local x = 1",
6433            "foo.lua",
6434            |metric| {
6435                assert_eq!(metric.loc.sloc(), 2);
6436                assert_eq!(metric.loc.ploc(), 2);
6437                assert_eq!(metric.loc.lloc(), 1);
6438                assert_eq!(metric.loc.cloc(), 0);
6439                assert_eq!(metric.loc.blank(), 0);
6440                insta::assert_json_snapshot!(metric.loc);
6441            },
6442        );
6443    }
6444
6445    #[cfg(feature = "lua")]
6446    #[test]
6447    fn lua_toplevel_assignment_lloc() {
6448        // Bare `x = 1` at chunk level: parent is Chunk, not VariableDeclaration,
6449        // so the parent-guard correctly counts it as 1 lloc.
6450        check_metrics::<LuaParser>(
6451            "x = 1
6452y, z = 2, 3",
6453            "foo.lua",
6454            |metric| {
6455                assert_eq!(metric.loc.sloc(), 2);
6456                assert_eq!(metric.loc.ploc(), 2);
6457                assert_eq!(metric.loc.lloc(), 2);
6458                assert_eq!(metric.loc.cloc(), 0);
6459                assert_eq!(metric.loc.blank(), 0);
6460                insta::assert_json_snapshot!(metric.loc);
6461            },
6462        );
6463    }
6464
6465    #[cfg(feature = "typescript")]
6466    #[test]
6467    fn tsx_basic_loc() {
6468        check_metrics::<TsxParser>(
6469            "// A simple utility function
6470            function add(a: number, b: number): number {
6471                /* multi-line
6472                   comment */
6473                return a + b;
6474            }
6475
6476            const greet = (name: string) => {
6477                return `Hello, ${name}`;
6478            };",
6479            "foo.tsx",
6480            |metric| {
6481                insta::assert_json_snapshot!(
6482                    metric.loc,
6483                    @r#"
6484                {
6485                  "sloc": 10,
6486                  "ploc": 6,
6487                  "lloc": 3,
6488                  "cloc": 3,
6489                  "blank": 1,
6490                  "dloc": 0,
6491                  "sloc_average": 3.3333333333333335,
6492                  "ploc_average": 2.0,
6493                  "lloc_average": 1.0,
6494                  "cloc_average": 1.0,
6495                  "blank_average": 0.3333333333333333,
6496                  "dloc_average": 0.0,
6497                  "sloc_min": 3,
6498                  "sloc_max": 10,
6499                  "cloc_min": 0,
6500                  "cloc_max": 3,
6501                  "ploc_min": 3,
6502                  "ploc_max": 6,
6503                  "lloc_min": 1,
6504                  "lloc_max": 3,
6505                  "blank_min": 0,
6506                  "blank_max": 1,
6507                  "dloc_min": 0,
6508                  "dloc_max": 0
6509                }
6510                "#
6511                );
6512            },
6513        );
6514    }
6515
6516    #[cfg(feature = "typescript")]
6517    #[test]
6518    fn typescript_basic_loc() {
6519        check_metrics::<TypescriptParser>(
6520            "// Line comment
6521            /* Block
6522               comment */
6523            function greet(name: string): string {
6524                return `Hello, ${name}`;
6525            }
6526
6527            const add = (a: number, b: number): number => a + b;",
6528            "foo.ts",
6529            |metric| {
6530                insta::assert_json_snapshot!(
6531                    metric.loc,
6532                    @r#"
6533                {
6534                  "sloc": 8,
6535                  "ploc": 4,
6536                  "lloc": 2,
6537                  "cloc": 3,
6538                  "blank": 1,
6539                  "dloc": 0,
6540                  "sloc_average": 2.6666666666666665,
6541                  "ploc_average": 1.3333333333333333,
6542                  "lloc_average": 0.6666666666666666,
6543                  "cloc_average": 1.0,
6544                  "blank_average": 0.3333333333333333,
6545                  "dloc_average": 0.0,
6546                  "sloc_min": 1,
6547                  "sloc_max": 8,
6548                  "cloc_min": 0,
6549                  "cloc_max": 3,
6550                  "ploc_min": 1,
6551                  "ploc_max": 4,
6552                  "lloc_min": 0,
6553                  "lloc_max": 2,
6554                  "blank_min": 0,
6555                  "blank_max": 1,
6556                  "dloc_min": 0,
6557                  "dloc_max": 0
6558                }
6559                "#
6560                );
6561            },
6562        );
6563    }
6564
6565    #[cfg(feature = "csharp")]
6566    #[test]
6567    fn csharp_comments() {
6568        check_metrics::<CsharpParser>(
6569            "for (int i = 0; i < 100; i++) {
6570               // Print hello
6571               System.Console.WriteLine(\"hello\");
6572               /// XML doc comment
6573               System.Console.WriteLine(\"hello\");
6574             }",
6575            "foo.cs",
6576            |metric| {
6577                assert_eq!(metric.loc.sloc(), 6);
6578                assert_eq!(metric.loc.ploc(), 4);
6579                assert_eq!(metric.loc.lloc(), 3);
6580                assert_eq!(metric.loc.cloc(), 2);
6581                assert_eq!(metric.loc.blank(), 0);
6582                // Roslyn attaches no `///` to a statement (CS1587, #1613).
6583                assert_eq!(metric.loc.dloc(), 0);
6584                insta::assert_json_snapshot!(metric.loc);
6585            },
6586        );
6587    }
6588
6589    #[cfg(feature = "csharp")]
6590    #[test]
6591    fn csharp_blank() {
6592        check_metrics::<CsharpParser>(
6593            "int x = 1;
6594
6595
6596            int y = 2;",
6597            "foo.cs",
6598            |metric| {
6599                assert_eq!(metric.loc.sloc(), 4);
6600                assert_eq!(metric.loc.ploc(), 2);
6601                assert_eq!(metric.loc.lloc(), 2);
6602                assert_eq!(metric.loc.cloc(), 0);
6603                assert_eq!(metric.loc.blank(), 2);
6604                insta::assert_json_snapshot!(metric.loc);
6605            },
6606        );
6607    }
6608
6609    #[cfg(feature = "csharp")]
6610    #[test]
6611    fn csharp_sloc() {
6612        check_metrics::<CsharpParser>(
6613            "for (int i = 0; i < 100; i++) {
6614               System.Console.WriteLine(i);
6615             }",
6616            "foo.cs",
6617            |metric| {
6618                assert_eq!(metric.loc.sloc(), 3);
6619                assert_eq!(metric.loc.ploc(), 3);
6620                assert_eq!(metric.loc.lloc(), 2);
6621                assert_eq!(metric.loc.cloc(), 0);
6622                assert_eq!(metric.loc.blank(), 0);
6623                insta::assert_json_snapshot!(metric.loc);
6624            },
6625        );
6626    }
6627
6628    #[cfg(feature = "csharp")]
6629    #[test]
6630    fn csharp_module_sloc() {
6631        check_metrics::<CsharpParser>(
6632            "namespace HelloWorld {
6633              class Program { }
6634            }",
6635            "foo.cs",
6636            |metric| {
6637                assert_eq!(metric.loc.sloc(), 3);
6638                assert_eq!(metric.loc.ploc(), 3);
6639                assert_eq!(metric.loc.lloc(), 0);
6640                assert_eq!(metric.loc.cloc(), 0);
6641                assert_eq!(metric.loc.blank(), 0);
6642                insta::assert_json_snapshot!(metric.loc);
6643            },
6644        );
6645    }
6646
6647    #[cfg(feature = "csharp")]
6648    #[test]
6649    fn csharp_single_ploc() {
6650        check_metrics::<CsharpParser>("int x = 1;", "foo.cs", |metric| {
6651            assert_eq!(metric.loc.sloc(), 1);
6652            assert_eq!(metric.loc.ploc(), 1);
6653            assert_eq!(metric.loc.lloc(), 1);
6654            assert_eq!(metric.loc.cloc(), 0);
6655            assert_eq!(metric.loc.blank(), 0);
6656            insta::assert_json_snapshot!(metric.loc);
6657        });
6658    }
6659
6660    #[cfg(feature = "csharp")]
6661    #[test]
6662    fn csharp_simple_ploc() {
6663        check_metrics::<CsharpParser>(
6664            "for (int i = 0; i < 100; i++) {
6665               System.Console.WriteLine(i);
6666             }",
6667            "foo.cs",
6668            |metric| {
6669                assert_eq!(metric.loc.sloc(), 3);
6670                assert_eq!(metric.loc.ploc(), 3);
6671                assert_eq!(metric.loc.lloc(), 2);
6672                assert_eq!(metric.loc.cloc(), 0);
6673                assert_eq!(metric.loc.blank(), 0);
6674                insta::assert_json_snapshot!(metric.loc);
6675            },
6676        );
6677    }
6678
6679    #[cfg(feature = "csharp")]
6680    #[test]
6681    fn csharp_multi_ploc() {
6682        check_metrics::<CsharpParser>(
6683            "int x = 1;
6684            for (int i = 0; i < 100; i++) {
6685               System.Console.WriteLine(i);
6686             }",
6687            "foo.cs",
6688            |metric| {
6689                assert_eq!(metric.loc.sloc(), 4);
6690                assert_eq!(metric.loc.ploc(), 4);
6691                assert_eq!(metric.loc.lloc(), 3);
6692                assert_eq!(metric.loc.cloc(), 0);
6693                assert_eq!(metric.loc.blank(), 0);
6694                insta::assert_json_snapshot!(metric.loc);
6695            },
6696        );
6697    }
6698
6699    #[cfg(feature = "csharp")]
6700    #[test]
6701    fn csharp_single_statement_lloc() {
6702        check_metrics::<CsharpParser>("int max = 10;", "foo.cs", |metric| {
6703            assert_eq!(metric.loc.sloc(), 1);
6704            assert_eq!(metric.loc.ploc(), 1);
6705            assert_eq!(metric.loc.lloc(), 1);
6706            assert_eq!(metric.loc.cloc(), 0);
6707            assert_eq!(metric.loc.blank(), 0);
6708            insta::assert_json_snapshot!(metric.loc);
6709        });
6710    }
6711
6712    #[cfg(feature = "csharp")]
6713    #[test]
6714    fn csharp_for_lloc() {
6715        check_metrics::<CsharpParser>(
6716            "for (int i = 0; i < 10; i++) {
6717                System.Console.WriteLine(i);
6718            }",
6719            "foo.cs",
6720            |metric| {
6721                assert_eq!(metric.loc.sloc(), 3);
6722                assert_eq!(metric.loc.ploc(), 3);
6723                assert_eq!(metric.loc.lloc(), 2);
6724                assert_eq!(metric.loc.cloc(), 0);
6725                assert_eq!(metric.loc.blank(), 0);
6726                insta::assert_json_snapshot!(metric.loc);
6727            },
6728        );
6729    }
6730
6731    #[cfg(feature = "csharp")]
6732    #[test]
6733    fn csharp_foreach_lloc() {
6734        check_metrics::<CsharpParser>(
6735            "foreach (var item in items) {
6736                System.Console.WriteLine(item);
6737            }",
6738            "foo.cs",
6739            |metric| {
6740                assert_eq!(metric.loc.sloc(), 3);
6741                assert_eq!(metric.loc.ploc(), 3);
6742                assert_eq!(metric.loc.lloc(), 2);
6743                assert_eq!(metric.loc.cloc(), 0);
6744                assert_eq!(metric.loc.blank(), 0);
6745                insta::assert_json_snapshot!(metric.loc);
6746            },
6747        );
6748    }
6749
6750    #[cfg(feature = "csharp")]
6751    #[test]
6752    fn csharp_while_lloc() {
6753        check_metrics::<CsharpParser>(
6754            "int i = 0;
6755            while (i < 10) {
6756                i++;
6757            }",
6758            "foo.cs",
6759            |metric| {
6760                assert_eq!(metric.loc.sloc(), 4);
6761                assert_eq!(metric.loc.ploc(), 4);
6762                assert_eq!(metric.loc.lloc(), 3);
6763                assert_eq!(metric.loc.cloc(), 0);
6764                assert_eq!(metric.loc.blank(), 0);
6765                insta::assert_json_snapshot!(metric.loc);
6766            },
6767        );
6768    }
6769
6770    #[cfg(feature = "csharp")]
6771    #[test]
6772    fn csharp_do_while_lloc() {
6773        check_metrics::<CsharpParser>(
6774            "int i = 0;
6775            do {
6776                i++;
6777            } while (i < 10);",
6778            "foo.cs",
6779            |metric| {
6780                assert_eq!(metric.loc.sloc(), 4);
6781                assert_eq!(metric.loc.ploc(), 4);
6782                assert_eq!(metric.loc.lloc(), 3);
6783                assert_eq!(metric.loc.cloc(), 0);
6784                assert_eq!(metric.loc.blank(), 0);
6785                insta::assert_json_snapshot!(metric.loc);
6786            },
6787        );
6788    }
6789
6790    #[cfg(feature = "csharp")]
6791    #[test]
6792    fn csharp_switch_lloc() {
6793        check_metrics::<CsharpParser>(
6794            "switch (x) {
6795                case 1: System.Console.WriteLine(1); break;
6796                case 2: System.Console.WriteLine(2); break;
6797                default: System.Console.WriteLine(0); break;
6798            }
6799            string s = x switch { 1 => \"one\", _ => \"other\" };",
6800            "foo.cs",
6801            |metric| {
6802                assert_eq!(metric.loc.sloc(), 6);
6803                assert_eq!(metric.loc.ploc(), 6);
6804                assert_eq!(metric.loc.lloc(), 8);
6805                assert_eq!(metric.loc.cloc(), 0);
6806                assert_eq!(metric.loc.blank(), 0);
6807                insta::assert_json_snapshot!(metric.loc);
6808            },
6809        );
6810    }
6811
6812    #[cfg(feature = "csharp")]
6813    #[test]
6814    fn csharp_continue_lloc() {
6815        check_metrics::<CsharpParser>(
6816            "for (int i = 0; i < 10; i++) {
6817                if (i == 5) continue;
6818                System.Console.WriteLine(i);
6819            }",
6820            "foo.cs",
6821            |metric| {
6822                assert_eq!(metric.loc.sloc(), 4);
6823                assert_eq!(metric.loc.ploc(), 4);
6824                assert_eq!(metric.loc.lloc(), 4);
6825                assert_eq!(metric.loc.cloc(), 0);
6826                assert_eq!(metric.loc.blank(), 0);
6827                insta::assert_json_snapshot!(metric.loc);
6828            },
6829        );
6830    }
6831
6832    #[cfg(feature = "csharp")]
6833    #[test]
6834    fn csharp_try_lloc() {
6835        check_metrics::<CsharpParser>(
6836            "try {
6837                System.Console.WriteLine(\"try\");
6838            } catch (System.Exception e) {
6839                throw new System.Exception(\"caught\");
6840            } finally {
6841                System.Console.WriteLine(\"done\");
6842            }",
6843            "foo.cs",
6844            |metric| {
6845                assert_eq!(metric.loc.sloc(), 7);
6846                assert_eq!(metric.loc.ploc(), 7);
6847                assert_eq!(metric.loc.lloc(), 4);
6848                assert_eq!(metric.loc.cloc(), 0);
6849                assert_eq!(metric.loc.blank(), 0);
6850                insta::assert_json_snapshot!(metric.loc);
6851            },
6852        );
6853    }
6854
6855    #[cfg(feature = "csharp")]
6856    #[test]
6857    fn csharp_class_loc() {
6858        check_metrics::<CsharpParser>(
6859            "class A {
6860                int x;
6861                public void M() {
6862                    System.Console.WriteLine(x);
6863                }
6864            }",
6865            "foo.cs",
6866            |metric| {
6867                assert_eq!(metric.loc.sloc(), 6);
6868                assert_eq!(metric.loc.ploc(), 6);
6869                assert_eq!(metric.loc.lloc(), 1);
6870                assert_eq!(metric.loc.cloc(), 0);
6871                assert_eq!(metric.loc.blank(), 0);
6872                insta::assert_json_snapshot!(metric.loc);
6873            },
6874        );
6875    }
6876
6877    #[cfg(feature = "csharp")]
6878    #[test]
6879    fn csharp_expressions_lloc() {
6880        check_metrics::<CsharpParser>(
6881            "int a = 1;
6882            int b = 2;
6883            int c = a + b;
6884            System.Console.WriteLine(c);",
6885            "foo.cs",
6886            |metric| {
6887                assert_eq!(metric.loc.sloc(), 4);
6888                assert_eq!(metric.loc.ploc(), 4);
6889                assert_eq!(metric.loc.lloc(), 4);
6890                assert_eq!(metric.loc.cloc(), 0);
6891                assert_eq!(metric.loc.blank(), 0);
6892                insta::assert_json_snapshot!(metric.loc);
6893            },
6894        );
6895    }
6896
6897    #[cfg(feature = "csharp")]
6898    #[test]
6899    fn csharp_statement_inline_loc() {
6900        check_metrics::<CsharpParser>(
6901            "if (x > 0) System.Console.WriteLine(x);",
6902            "foo.cs",
6903            |metric| {
6904                assert_eq!(metric.loc.sloc(), 1);
6905                assert_eq!(metric.loc.ploc(), 1);
6906                assert_eq!(metric.loc.lloc(), 2);
6907                assert_eq!(metric.loc.cloc(), 0);
6908                assert_eq!(metric.loc.blank(), 0);
6909                insta::assert_json_snapshot!(metric.loc);
6910            },
6911        );
6912    }
6913
6914    #[cfg(feature = "csharp")]
6915    #[test]
6916    fn csharp_general_loc() {
6917        check_metrics::<CsharpParser>(
6918            "using System;
6919            namespace Demo {
6920                class A {
6921                    public void M() {
6922                        Console.WriteLine(\"hi\");
6923                    }
6924                }
6925                class B {
6926                    public int N() { return 0; }
6927                }
6928            }",
6929            "foo.cs",
6930            |metric| {
6931                assert_eq!(metric.loc.sloc(), 11);
6932                assert_eq!(metric.loc.ploc(), 11);
6933                assert_eq!(metric.loc.lloc(), 2);
6934                assert_eq!(metric.loc.cloc(), 0);
6935                assert_eq!(metric.loc.blank(), 0);
6936                insta::assert_json_snapshot!(metric.loc);
6937            },
6938        );
6939    }
6940
6941    #[cfg(feature = "csharp")]
6942    #[test]
6943    fn csharp_using_lloc() {
6944        // EC11 — `using_directive` does not bump LLOC; `using_statement`
6945        // (block form) and the C# 8 simple-using local-declaration
6946        // (`using var x = ...;`) both do, the latter via the standard
6947        // `LocalDeclarationStatement` path.
6948        check_metrics::<CsharpParser>(
6949            "using System;
6950            using System.IO;
6951            class A {
6952                public void M() {
6953                    using (var s = File.OpenRead(\"x\")) {
6954                        Console.WriteLine(s);
6955                    }
6956                    using var t = File.OpenRead(\"y\");
6957                    Console.WriteLine(t);
6958                }
6959            }",
6960            "foo.cs",
6961            |metric| {
6962                assert_eq!(metric.loc.sloc(), 11);
6963                assert_eq!(metric.loc.ploc(), 11);
6964                assert_eq!(metric.loc.lloc(), 4);
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 = "kotlin")]
6973    #[test]
6974    fn kotlin_loc_basic() {
6975        check_metrics::<KotlinParser>(
6976            "// A simple function
6977            fun greet(name: String): String {
6978                val greeting = \"Hello, \" + name
6979                if (name.isEmpty()) {
6980                    return \"Hello, World!\"
6981                }
6982                return greeting
6983            }",
6984            "foo.kt",
6985            |metric| {
6986                insta::assert_json_snapshot!(
6987                    metric.loc,
6988                    @r#"
6989                {
6990                  "sloc": 8,
6991                  "ploc": 7,
6992                  "lloc": 4,
6993                  "cloc": 1,
6994                  "blank": 0,
6995                  "dloc": 0,
6996                  "sloc_average": 4.0,
6997                  "ploc_average": 3.5,
6998                  "lloc_average": 2.0,
6999                  "cloc_average": 0.5,
7000                  "blank_average": 0.0,
7001                  "dloc_average": 0.0,
7002                  "sloc_min": 7,
7003                  "sloc_max": 8,
7004                  "cloc_min": 0,
7005                  "cloc_max": 1,
7006                  "ploc_min": 7,
7007                  "ploc_max": 7,
7008                  "lloc_min": 4,
7009                  "lloc_max": 4,
7010                  "blank_min": 0,
7011                  "blank_max": 0,
7012                  "dloc_min": 0,
7013                  "dloc_max": 0
7014                }
7015                "#
7016                );
7017            },
7018        );
7019    }
7020
7021    #[cfg(feature = "kotlin")]
7022    #[test]
7023    fn kotlin_loc_bare_expression() {
7024        check_metrics::<KotlinParser>(
7025            "fun main() {
7026                val x = 42
7027                println(x)
7028                listOf(1, 2, 3).forEach { println(it) }
7029            }",
7030            "foo.kt",
7031            |metric| {
7032                // lloc should count: val x = 42 (PropertyDeclaration, +1)
7033                // + println(x) (CallExpression, parent=Block, +1)
7034                // + listOf(1, 2, 3).forEach { ... } (CallExpression, parent=Block, +1) = 3
7035                insta::assert_json_snapshot!(
7036                    metric.loc,
7037                    @r#"
7038                {
7039                  "sloc": 5,
7040                  "ploc": 5,
7041                  "lloc": 3,
7042                  "cloc": 0,
7043                  "blank": 0,
7044                  "dloc": 0,
7045                  "sloc_average": 2.5,
7046                  "ploc_average": 2.5,
7047                  "lloc_average": 1.5,
7048                  "cloc_average": 0.0,
7049                  "blank_average": 0.0,
7050                  "dloc_average": 0.0,
7051                  "sloc_min": 5,
7052                  "sloc_max": 5,
7053                  "cloc_min": 0,
7054                  "cloc_max": 0,
7055                  "ploc_min": 5,
7056                  "ploc_max": 5,
7057                  "lloc_min": 3,
7058                  "lloc_max": 3,
7059                  "blank_min": 0,
7060                  "blank_max": 0,
7061                  "dloc_min": 0,
7062                  "dloc_max": 0
7063                }
7064                "#
7065                );
7066            },
7067        );
7068    }
7069
7070    #[cfg(feature = "bash")]
7071    #[test]
7072    fn bash_loc() {
7073        check_metrics::<BashParser>(
7074            "#!/bin/bash
7075# This is a comment
7076f() {
7077    echo 'hello'
7078}
7079
7080# Another comment
7081f",
7082            "foo.sh",
7083            |metric| {
7084                assert_eq!(metric.loc.sloc(), 8);
7085                assert_eq!(metric.loc.ploc(), 4);
7086                assert_eq!(metric.loc.lloc(), 3);
7087                assert_eq!(metric.loc.cloc(), 3);
7088                assert_eq!(metric.loc.blank(), 1);
7089                insta::assert_json_snapshot!(metric.loc);
7090            },
7091        );
7092    }
7093
7094    // CRLF regression tests: metrics must be identical regardless of line ending style.
7095    // These also serve as canaries for tree-sitter row-counting behaviour with \r bytes.
7096
7097    #[cfg(feature = "python")]
7098    #[test]
7099    fn python_cloc_crlf_matches_lf() {
7100        check_metrics::<PythonParser>("# comment\nx = 1", "foo.py", |m| {
7101            assert_eq!(m.loc.cloc(), 1);
7102            assert_eq!(m.loc.ploc(), 1);
7103            assert_eq!(m.loc.sloc(), 2);
7104            assert_eq!(m.loc.blank(), 0);
7105        });
7106        check_metrics::<PythonParser>("# comment\r\nx = 1", "foo.py", |m| {
7107            assert_eq!(m.loc.cloc(), 1);
7108            assert_eq!(m.loc.ploc(), 1);
7109            assert_eq!(m.loc.sloc(), 2);
7110            assert_eq!(m.loc.blank(), 0);
7111        });
7112        // Lone-CR (old Mac line endings) is the true canary: without CR normalisation,
7113        // tree-sitter 0.26.8 only advances its row counter on \n, collapsing all content
7114        // onto row 0 and producing wrong sloc/cloc metrics.
7115        check_metrics::<PythonParser>("# comment\rx = 1", "foo.py", |m| {
7116            assert_eq!(m.loc.cloc(), 1);
7117            assert_eq!(m.loc.ploc(), 1);
7118            assert_eq!(m.loc.sloc(), 2);
7119            assert_eq!(m.loc.blank(), 0);
7120        });
7121    }
7122
7123    #[cfg(feature = "python")]
7124    #[test]
7125    fn python_blank_crlf_matches_lf() {
7126        check_metrics::<PythonParser>("# comment\n\nx = 1", "foo.py", |m| {
7127            assert_eq!(m.loc.blank(), 1);
7128        });
7129        check_metrics::<PythonParser>("# comment\r\n\r\nx = 1", "foo.py", |m| {
7130            assert_eq!(m.loc.blank(), 1);
7131        });
7132        // Lone-CR: without normalisation the blank \r line stays on row 0 and is not counted.
7133        check_metrics::<PythonParser>("# comment\r\rx = 1", "foo.py", |m| {
7134            assert_eq!(m.loc.blank(), 1);
7135        });
7136    }
7137
7138    #[cfg(feature = "rust")]
7139    #[test]
7140    fn rust_cloc_crlf_matches_lf() {
7141        check_metrics::<RustParser>(
7142            "fn f() {\n    // comment\n    let x = 1;\n}",
7143            "foo.rs",
7144            |m| {
7145                assert_eq!(m.loc.cloc(), 1);
7146                assert_eq!(m.loc.sloc(), 4);
7147            },
7148        );
7149        check_metrics::<RustParser>(
7150            "fn f() {\r\n    // comment\r\n    let x = 1;\r\n}",
7151            "foo.rs",
7152            |m| {
7153                assert_eq!(m.loc.cloc(), 1);
7154                assert_eq!(m.loc.sloc(), 4);
7155            },
7156        );
7157        // Lone-CR: without normalisation, tree-sitter 0.26.8 only advances its row counter on
7158        // \n, so all content collapses onto row 0 and sloc becomes 1 instead of 4.
7159        check_metrics::<RustParser>(
7160            "fn f() {\r    // comment\r    let x = 1;\r}",
7161            "foo.rs",
7162            |m| {
7163                assert_eq!(m.loc.cloc(), 1);
7164                assert_eq!(m.loc.sloc(), 4);
7165            },
7166        );
7167    }
7168
7169    #[cfg(feature = "tcl")]
7170    #[test]
7171    fn tcl_blank() {
7172        check_metrics::<TclParser>("set x 1\n\nset y 2", "foo.tcl", |metric| {
7173            assert_eq!(metric.loc.sloc(), 3);
7174            assert_eq!(metric.loc.ploc(), 2);
7175            assert_eq!(metric.loc.lloc(), 2);
7176            assert_eq!(metric.loc.cloc(), 0);
7177            assert_eq!(metric.loc.blank(), 1);
7178            insta::assert_json_snapshot!(metric.loc);
7179        });
7180    }
7181
7182    #[cfg(feature = "tcl")]
7183    #[test]
7184    fn tcl_no_zero_blank() {
7185        // Blank line interleaved with code that carries trailing comments —
7186        // ensures the `blank = sloc - (ploc ∪ cloc lines)` union math holds
7187        // when code and comment lines coincide.
7188        check_metrics::<TclParser>(
7189            "set a 1\nset b 2\n\nset c 3 ;# trailing\nset d 4 ;# trailing\nset e 5",
7190            "foo.tcl",
7191            |metric| {
7192                assert_eq!(metric.loc.sloc(), 6);
7193                assert_eq!(metric.loc.ploc(), 5);
7194                assert_eq!(metric.loc.cloc(), 2);
7195                assert_eq!(metric.loc.blank(), 1);
7196            },
7197        );
7198    }
7199
7200    /// expected: row 0 is comment-only, row 1 is the sole code row — so
7201    /// `cloc 1 + ploc 1 == sloc 2`. This test pinned `ploc == 2` until
7202    /// #1135: the `LF` token terminating the comment row landed in the
7203    /// `_` catch-all and inserted that row into PLOC, which also drove
7204    /// `cloc + ploc` past `sloc`.
7205    #[cfg(feature = "tcl")]
7206    #[test]
7207    fn tcl_cloc() {
7208        check_metrics::<TclParser>("# This is a comment\nset x 1", "foo.tcl", |metric| {
7209            assert_eq!(metric.loc.sloc(), 2);
7210            assert_eq!(metric.loc.ploc(), 1);
7211            assert_eq!(metric.loc.lloc(), 1);
7212            assert_eq!(metric.loc.cloc(), 1);
7213            assert_eq!(metric.loc.blank(), 0);
7214            insta::assert_json_snapshot!(metric.loc);
7215        });
7216    }
7217
7218    #[cfg(feature = "tcl")]
7219    #[test]
7220    fn tcl_lloc() {
7221        check_metrics::<TclParser>(
7222            "proc f {x} {
7223    while {$x > 0} {
7224        if {$x > 10} {
7225            set x [expr {$x - 1}]
7226        }
7227    }
7228}",
7229            "foo.tcl",
7230            |metric| {
7231                assert_eq!(metric.loc.sloc(), 7);
7232                assert_eq!(metric.loc.ploc(), 7);
7233                assert_eq!(metric.loc.lloc(), 4);
7234                assert_eq!(metric.loc.cloc(), 0);
7235                assert_eq!(metric.loc.blank(), 0);
7236                insta::assert_json_snapshot!(metric.loc);
7237            },
7238        );
7239    }
7240
7241    #[cfg(feature = "tcl")]
7242    #[test]
7243    fn tcl_no_command_substitution_lloc() {
7244        // `string toupper` inside [...] is a sub-expression; only `puts` is top-level.
7245        check_metrics::<TclParser>("puts [string toupper x]", "foo.tcl", |metric| {
7246            assert_eq!(metric.loc.sloc(), 1);
7247            assert_eq!(metric.loc.ploc(), 1);
7248            assert_eq!(metric.loc.lloc(), 1);
7249            assert_eq!(metric.loc.cloc(), 0);
7250            assert_eq!(metric.loc.blank(), 0);
7251            insta::assert_json_snapshot!(metric.loc);
7252        });
7253    }
7254
7255    #[cfg(feature = "tcl")]
7256    #[test]
7257    fn tcl_procedure_lloc() {
7258        check_metrics::<TclParser>("proc foo {} {\n    puts hello\n}", "foo.tcl", |metric| {
7259            assert_eq!(metric.loc.sloc(), 3);
7260            assert_eq!(metric.loc.ploc(), 3);
7261            assert_eq!(metric.loc.lloc(), 2);
7262            assert_eq!(metric.loc.cloc(), 0);
7263            assert_eq!(metric.loc.blank(), 0);
7264            insta::assert_json_snapshot!(metric.loc);
7265        });
7266    }
7267
7268    #[cfg(feature = "tcl")]
7269    #[test]
7270    fn tcl_if_lloc() {
7271        check_metrics::<TclParser>("if {1} {\n    puts hello\n}", "foo.tcl", |metric| {
7272            assert_eq!(metric.loc.sloc(), 3);
7273            assert_eq!(metric.loc.ploc(), 3);
7274            assert_eq!(metric.loc.lloc(), 2);
7275            assert_eq!(metric.loc.cloc(), 0);
7276            assert_eq!(metric.loc.blank(), 0);
7277            insta::assert_json_snapshot!(metric.loc);
7278        });
7279    }
7280
7281    #[cfg(feature = "tcl")]
7282    #[test]
7283    fn tcl_elseif_lloc() {
7284        // if=1 lloc, elseif=1 lloc, else adds 0 lloc
7285        check_metrics::<TclParser>(
7286            "if {$x > 10} {
7287    puts big
7288} elseif {$x > 5} {
7289    puts medium
7290} else {
7291    puts small
7292}",
7293            "foo.tcl",
7294            |metric| {
7295                assert_eq!(metric.loc.sloc(), 7);
7296                assert_eq!(metric.loc.ploc(), 7);
7297                assert_eq!(metric.loc.lloc(), 5);
7298                assert_eq!(metric.loc.cloc(), 0);
7299                assert_eq!(metric.loc.blank(), 0);
7300                insta::assert_json_snapshot!(metric.loc);
7301            },
7302        );
7303    }
7304
7305    #[cfg(feature = "tcl")]
7306    #[test]
7307    fn tcl_while_lloc() {
7308        check_metrics::<TclParser>(
7309            "while {$x > 0} {\n    set x [expr {$x - 1}]\n}",
7310            "foo.tcl",
7311            |metric| {
7312                assert_eq!(metric.loc.sloc(), 3);
7313                assert_eq!(metric.loc.ploc(), 3);
7314                assert_eq!(metric.loc.lloc(), 2);
7315                assert_eq!(metric.loc.cloc(), 0);
7316                assert_eq!(metric.loc.blank(), 0);
7317                insta::assert_json_snapshot!(metric.loc);
7318            },
7319        );
7320    }
7321
7322    #[cfg(feature = "tcl")]
7323    #[test]
7324    fn tcl_foreach_lloc() {
7325        check_metrics::<TclParser>(
7326            "foreach item {a b c} {\n    puts $item\n}",
7327            "foo.tcl",
7328            |metric| {
7329                assert_eq!(metric.loc.sloc(), 3);
7330                assert_eq!(metric.loc.ploc(), 3);
7331                assert_eq!(metric.loc.lloc(), 2);
7332                assert_eq!(metric.loc.cloc(), 0);
7333                assert_eq!(metric.loc.blank(), 0);
7334                insta::assert_json_snapshot!(metric.loc);
7335            },
7336        );
7337    }
7338
7339    #[cfg(feature = "tcl")]
7340    #[test]
7341    fn tcl_set_lloc() {
7342        check_metrics::<TclParser>("set x 42", "foo.tcl", |metric| {
7343            assert_eq!(metric.loc.sloc(), 1);
7344            assert_eq!(metric.loc.ploc(), 1);
7345            assert_eq!(metric.loc.lloc(), 1);
7346            assert_eq!(metric.loc.cloc(), 0);
7347            assert_eq!(metric.loc.blank(), 0);
7348            insta::assert_json_snapshot!(metric.loc);
7349        });
7350    }
7351
7352    #[cfg(feature = "tcl")]
7353    #[test]
7354    fn tcl_global_lloc() {
7355        check_metrics::<TclParser>("global x", "foo.tcl", |metric| {
7356            assert_eq!(metric.loc.sloc(), 1);
7357            assert_eq!(metric.loc.ploc(), 1);
7358            assert_eq!(metric.loc.lloc(), 1);
7359            assert_eq!(metric.loc.cloc(), 0);
7360            assert_eq!(metric.loc.blank(), 0);
7361            insta::assert_json_snapshot!(metric.loc);
7362        });
7363    }
7364
7365    #[cfg(feature = "tcl")]
7366    #[test]
7367    fn tcl_try_catch_lloc() {
7368        // try=1 lloc; catch command=1 lloc; commands inside bodies count separately
7369        check_metrics::<TclParser>(
7370            "catch {
7371    set x 1
7372} result
7373try {
7374    set y 2
7375} on error {msg} {
7376    puts $msg
7377}",
7378            "foo.tcl",
7379            |metric| {
7380                assert_eq!(metric.loc.sloc(), 8);
7381                assert_eq!(metric.loc.ploc(), 8);
7382                assert_eq!(metric.loc.lloc(), 5);
7383                assert_eq!(metric.loc.cloc(), 0);
7384                assert_eq!(metric.loc.blank(), 0);
7385                insta::assert_json_snapshot!(metric.loc);
7386            },
7387        );
7388    }
7389
7390    #[cfg(feature = "tcl")]
7391    #[test]
7392    fn tcl_namespace_lloc() {
7393        check_metrics::<TclParser>(
7394            "namespace eval myns {\n    set x 1\n}",
7395            "foo.tcl",
7396            |metric| {
7397                assert_eq!(metric.loc.sloc(), 3);
7398                assert_eq!(metric.loc.ploc(), 3);
7399                assert_eq!(metric.loc.lloc(), 2);
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 = "tcl")]
7408    #[test]
7409    fn tcl_regexp_lloc() {
7410        check_metrics::<TclParser>("regexp {^[0-9]+$} $x", "foo.tcl", |metric| {
7411            assert_eq!(metric.loc.sloc(), 1);
7412            assert_eq!(metric.loc.ploc(), 1);
7413            assert_eq!(metric.loc.lloc(), 1);
7414            assert_eq!(metric.loc.cloc(), 0);
7415            assert_eq!(metric.loc.blank(), 0);
7416            insta::assert_json_snapshot!(metric.loc);
7417        });
7418    }
7419
7420    #[cfg(feature = "tcl")]
7421    #[test]
7422    fn tcl_expr_cmd_lloc() {
7423        check_metrics::<TclParser>("expr {1 + 2}", "foo.tcl", |metric| {
7424            assert_eq!(metric.loc.sloc(), 1);
7425            assert_eq!(metric.loc.ploc(), 1);
7426            assert_eq!(metric.loc.lloc(), 1);
7427            assert_eq!(metric.loc.cloc(), 0);
7428            assert_eq!(metric.loc.blank(), 0);
7429            insta::assert_json_snapshot!(metric.loc);
7430        });
7431    }
7432
7433    #[cfg(feature = "tcl")]
7434    #[test]
7435    fn tcl_no_expr_cmd_substitution_lloc() {
7436        // `expr` inside [...] is a sub-expression, not a statement; only `set` counts.
7437        check_metrics::<TclParser>("set x [expr {1 + 2}]", "foo.tcl", |metric| {
7438            assert_eq!(metric.loc.sloc(), 1);
7439            assert_eq!(metric.loc.ploc(), 1);
7440            assert_eq!(metric.loc.lloc(), 1);
7441            assert_eq!(metric.loc.cloc(), 0);
7442            assert_eq!(metric.loc.blank(), 0);
7443            insta::assert_json_snapshot!(metric.loc);
7444        });
7445    }
7446
7447    #[cfg(feature = "tcl")]
7448    #[test]
7449    fn tcl_nested_commands_lloc() {
7450        // Commands inside proc body are recursively parsed; verify each counts.
7451        check_metrics::<TclParser>(
7452            "proc f {x} {
7453    set y [expr {$x * 2}]
7454    puts $y
7455}",
7456            "foo.tcl",
7457            |metric| {
7458                assert_eq!(metric.loc.sloc(), 4);
7459                assert_eq!(metric.loc.ploc(), 4);
7460                assert_eq!(metric.loc.lloc(), 3);
7461                assert_eq!(metric.loc.cloc(), 0);
7462                assert_eq!(metric.loc.blank(), 0);
7463                insta::assert_json_snapshot!(metric.loc);
7464            },
7465        );
7466    }
7467
7468    #[cfg(feature = "tcl")]
7469    #[test]
7470    fn tcl_command_lloc() {
7471        check_metrics::<TclParser>("puts hello", "foo.tcl", |metric| {
7472            assert_eq!(metric.loc.sloc(), 1);
7473            assert_eq!(metric.loc.ploc(), 1);
7474            assert_eq!(metric.loc.lloc(), 1);
7475            assert_eq!(metric.loc.cloc(), 0);
7476            assert_eq!(metric.loc.blank(), 0);
7477            insta::assert_json_snapshot!(metric.loc);
7478        });
7479    }
7480
7481    #[cfg(feature = "tcl")]
7482    #[test]
7483    fn tcl_no_else_lloc() {
7484        // `else` block does not add a logical line.
7485        check_metrics::<TclParser>(
7486            "if {1} {\n    puts yes\n} else {\n    puts no\n}",
7487            "foo.tcl",
7488            |metric| {
7489                assert_eq!(metric.loc.sloc(), 5);
7490                assert_eq!(metric.loc.ploc(), 5);
7491                assert_eq!(metric.loc.lloc(), 3);
7492                assert_eq!(metric.loc.cloc(), 0);
7493                assert_eq!(metric.loc.blank(), 0);
7494                insta::assert_json_snapshot!(metric.loc);
7495            },
7496        );
7497    }
7498
7499    #[cfg(feature = "tcl")]
7500    #[test]
7501    fn tcl_no_finally_lloc() {
7502        // `finally` block, like `else`, does not add a logical line.
7503        // proc(1) + try(1) + puts_hi(1) + puts_done(1) + finally(0) = 4.
7504        check_metrics::<TclParser>(
7505            "proc f {} {\n    try {\n        puts hi\n    } finally {\n        puts done\n    }\n}",
7506            "foo.tcl",
7507            |metric| {
7508                assert_eq!(
7509                    metric.loc.lloc(),
7510                    4,
7511                    "finally adds 0 lloc; would be 5 if finally counted"
7512                );
7513            },
7514        );
7515    }
7516
7517    #[cfg(feature = "tcl")]
7518    #[test]
7519    fn tcl_multiline_block() {
7520        check_metrics::<TclParser>(
7521            "proc f {x} {
7522    set a 1
7523
7524    set b 2
7525    return [expr {$a + $b}]
7526}",
7527            "foo.tcl",
7528            |metric| {
7529                assert_eq!(metric.loc.sloc(), 6);
7530                assert_eq!(metric.loc.ploc(), 5);
7531                assert_eq!(metric.loc.lloc(), 4);
7532                assert_eq!(metric.loc.cloc(), 0);
7533                assert_eq!(metric.loc.blank(), 1);
7534                insta::assert_json_snapshot!(metric.loc);
7535            },
7536        );
7537    }
7538
7539    #[cfg(feature = "tcl")]
7540    #[test]
7541    fn tcl_no_string_lloc() {
7542        // Multi-line double-quoted strings must not inflate lloc — only the
7543        // surrounding command should count. Mirrors lua_no_string_lloc and
7544        // elixir_no_string_content_lloc; pins the heredoc-shaped invariant
7545        // for Tcl quoted_word bodies.
7546        //
7547        // expected: three physical rows (`set s "line one`, `line two`,
7548        // `line three"`), all code — the interior row of a multi-line
7549        // string is PLOC, not blank (#1260, following #778 / #415). This
7550        // test pinned `ploc 2 / blank 1` until #1260; the row `line two`
7551        // held no node at all under the old arm list, so it reached
7552        // neither PLOC nor CLOC and `blank = sloc - ploc - cloc` claimed
7553        // it. `lloc` is unchanged: only the `set` command counts.
7554        check_metrics::<TclParser>(
7555            "set s \"line one\nline two\nline three\"",
7556            "foo.tcl",
7557            |metric| {
7558                assert_eq!(metric.loc.sloc(), 3);
7559                assert_eq!(metric.loc.ploc(), 3);
7560                assert_eq!(metric.loc.lloc(), 1);
7561                assert_eq!(metric.loc.cloc(), 0);
7562                assert_eq!(metric.loc.blank(), 0);
7563                insta::assert_json_snapshot!(metric.loc);
7564            },
7565        );
7566    }
7567
7568    #[cfg(feature = "tcl")]
7569    #[test]
7570    fn tcl_multiline_quoted_word_credits_every_row_to_ploc() {
7571        // Regression test for #1260. A Tcl `quoted_word` carries no child
7572        // per row — only the two `"` tokens — so its interior rows reached
7573        // neither PLOC nor CLOC and `blank = sloc - ploc - cloc` claimed
7574        // them.
7575
7576        // expected: 4 rows — `set msg "line one`, `line two`,
7577        // `line three"`, `puts $msg` — all code, none blank. lloc 2 for the
7578        // `set` and the `puts` command.
7579        check_metrics::<TclParser>(
7580            "set msg \"line one\nline two\nline three\"\nputs $msg",
7581            "foo.tcl",
7582            |metric| {
7583                assert_eq!(metric.loc.sloc(), 4);
7584                assert_eq!(metric.loc.ploc(), 4);
7585                assert_eq!(metric.loc.lloc(), 2);
7586                assert_eq!(metric.loc.cloc(), 0);
7587                assert_eq!(metric.loc.blank(), 0);
7588            },
7589        );
7590
7591        // expected: 3 rows whose middle row is empty *inside* the literal —
7592        // `set msg "one`, ``, `three"`. Code, not blank, matching every
7593        // language that already routes a multi-line literal.
7594        check_metrics::<TclParser>("set msg \"one\n\nthree\"", "foo.tcl", |metric| {
7595            assert_eq!(metric.loc.sloc(), 3);
7596            assert_eq!(metric.loc.ploc(), 3);
7597            assert_eq!(metric.loc.cloc(), 0);
7598            assert_eq!(metric.loc.blank(), 0);
7599        });
7600    }
7601
7602    #[cfg(feature = "tcl")]
7603    #[test]
7604    fn tcl_braced_word_body_blank_rows_stay_blank() {
7605        // The #1260 arm deliberately stops at `quoted_word`. Tcl spells a
7606        // script body and a braced literal with one kind, `braced_word`,
7607        // and the grammar parses both as scripts — `puts {a\n\nb}` yields
7608        // `command` children exactly as a `proc` body does, which is why
7609        // #1318 had to tell the two roles apart out-of-band. Routing it
7610        // would turn every blank line inside every procedure into code.
7611        //
7612        // expected: 5 rows — `proc p {} {`, `    set x 1`, ``,
7613        // `    set y 2`, `}` — with row 3 genuinely blank, so sloc 5,
7614        // ploc 4, blank 1. lloc 3 = the procedure plus two `set` commands.
7615        check_metrics::<TclParser>(
7616            "proc p {} {\n    set x 1\n\n    set y 2\n}",
7617            "foo.tcl",
7618            |metric| {
7619                assert_eq!(metric.loc.sloc(), 5);
7620                assert_eq!(metric.loc.ploc(), 4);
7621                assert_eq!(metric.loc.lloc(), 3);
7622                assert_eq!(metric.loc.cloc(), 0);
7623                assert_eq!(metric.loc.blank(), 1);
7624            },
7625        );
7626    }
7627
7628    #[cfg(feature = "javascript")]
7629    #[test]
7630    fn javascript_blank() {
7631        check_metrics::<JavascriptParser>(
7632            "// header comment
7633        function f() {
7634
7635            var x = 1;
7636
7637            var y = 2;
7638        }",
7639            "foo.js",
7640            |metric| {
7641                assert_eq!(metric.loc.sloc(), 7);
7642                assert_eq!(metric.loc.ploc(), 4);
7643                assert_eq!(metric.loc.lloc(), 2);
7644                assert_eq!(metric.loc.cloc(), 1);
7645                assert_eq!(metric.loc.blank(), 2);
7646                insta::assert_json_snapshot!(metric.loc);
7647            },
7648        );
7649    }
7650
7651    #[cfg(feature = "javascript")]
7652    #[test]
7653    fn javascript_cloc() {
7654        check_metrics::<JavascriptParser>(
7655            "// line comment
7656        /* block
7657           comment */
7658        function f() {
7659            return 1; // inline
7660        }",
7661            "foo.js",
7662            |metric| {
7663                assert_eq!(metric.loc.sloc(), 6);
7664                assert_eq!(metric.loc.ploc(), 3);
7665                assert_eq!(metric.loc.lloc(), 1);
7666                assert_eq!(metric.loc.cloc(), 4);
7667                assert_eq!(metric.loc.blank(), 0);
7668                insta::assert_json_snapshot!(metric.loc);
7669            },
7670        );
7671    }
7672
7673    #[cfg(feature = "javascript")]
7674    #[test]
7675    fn javascript_cloc_html_comment() {
7676        // The Annex-B `<!-- -->` `html_comment` must count as CLOC, not
7677        // fall to the `_` arm and inflate PLOC (#697). Pre-fix this
7678        // fixture measured cloc 0 / ploc 4.
7679        check_metrics::<JavascriptParser>(
7680            "<!-- header comment -->
7681function f() {
7682  return 1;
7683}",
7684            "foo.js",
7685            |metric| {
7686                assert_eq!(metric.loc.sloc(), 4);
7687                assert_eq!(metric.loc.ploc(), 3);
7688                assert_eq!(metric.loc.lloc(), 1);
7689                assert_eq!(metric.loc.cloc(), 1);
7690                assert_eq!(metric.loc.blank(), 0);
7691            },
7692        );
7693    }
7694
7695    #[cfg(feature = "mozjs")]
7696    #[test]
7697    fn mozjs_blank() {
7698        check_metrics::<MozjsParser>(
7699            "function f() {
7700
7701            var x = 1;
7702
7703        }",
7704            "foo.js",
7705            |metric| {
7706                assert_eq!(metric.loc.sloc(), 5);
7707                assert_eq!(metric.loc.ploc(), 3);
7708                assert_eq!(metric.loc.lloc(), 1);
7709                assert_eq!(metric.loc.cloc(), 0);
7710                assert_eq!(metric.loc.blank(), 2);
7711                insta::assert_json_snapshot!(metric.loc);
7712            },
7713        );
7714    }
7715
7716    #[cfg(feature = "mozjs")]
7717    #[test]
7718    fn mozjs_cloc() {
7719        check_metrics::<MozjsParser>(
7720            "// header
7721        /* block comment */
7722        function f() {
7723            return 42;
7724        }",
7725            "foo.js",
7726            |metric| {
7727                assert_eq!(metric.loc.sloc(), 5);
7728                assert_eq!(metric.loc.ploc(), 3);
7729                assert_eq!(metric.loc.lloc(), 1);
7730                assert_eq!(metric.loc.cloc(), 2);
7731                assert_eq!(metric.loc.blank(), 0);
7732                insta::assert_json_snapshot!(metric.loc);
7733            },
7734        );
7735    }
7736
7737    #[cfg(feature = "mozjs")]
7738    #[test]
7739    fn mozjs_no_zero_blank() {
7740        // Blank line interleaved with code that carries trailing comments —
7741        // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
7742        check_metrics::<MozjsParser>(
7743            "function f() {
7744  var a = 1;
7745
7746  var b = 2; // trailing
7747  var c = 3; // trailing
7748}",
7749            "foo.js",
7750            |metric| {
7751                assert_eq!(metric.loc.sloc(), 6);
7752                assert_eq!(metric.loc.ploc(), 5);
7753                assert_eq!(metric.loc.cloc(), 2);
7754                assert_eq!(metric.loc.blank(), 1);
7755                insta::assert_json_snapshot!(metric.loc);
7756            },
7757        );
7758    }
7759
7760    #[cfg(feature = "mozjs")]
7761    #[test]
7762    fn mozjs_arrow_function_loc() {
7763        check_metrics::<MozjsParser>(
7764            "const add = (a, b) => a + b;
7765        const greet = name => {
7766            return 'Hello ' + name;
7767        };",
7768            "foo.js",
7769            |metric| {
7770                assert_eq!(metric.loc.sloc(), 4);
7771                assert_eq!(metric.loc.ploc(), 4);
7772                assert_eq!(metric.loc.lloc(), 3);
7773                assert_eq!(metric.loc.cloc(), 0);
7774                assert_eq!(metric.loc.blank(), 0);
7775                insta::assert_json_snapshot!(metric.loc);
7776            },
7777        );
7778    }
7779
7780    #[cfg(feature = "mozjs")]
7781    #[test]
7782    fn mozjs_multiple_functions_loc() {
7783        check_metrics::<MozjsParser>(
7784            "function f() {
7785            return 1;
7786        }
7787        function g() {
7788            return 2;
7789        }",
7790            "foo.js",
7791            |metric| {
7792                assert_eq!(metric.loc.sloc(), 6);
7793                assert_eq!(metric.loc.ploc(), 6);
7794                assert_eq!(metric.loc.lloc(), 2);
7795                assert_eq!(metric.loc.cloc(), 0);
7796                assert_eq!(metric.loc.blank(), 0);
7797                insta::assert_json_snapshot!(metric.loc);
7798            },
7799        );
7800    }
7801
7802    #[cfg(feature = "mozjs")]
7803    #[test]
7804    fn mozjs_nested_function_loc() {
7805        check_metrics::<MozjsParser>(
7806            "function outer() {
7807            function inner() {
7808                return 1;
7809            }
7810            return inner();
7811        }",
7812            "foo.js",
7813            |metric| {
7814                assert_eq!(metric.loc.sloc(), 6);
7815                assert_eq!(metric.loc.ploc(), 6);
7816                assert_eq!(metric.loc.lloc(), 2);
7817                assert_eq!(metric.loc.cloc(), 0);
7818                assert_eq!(metric.loc.blank(), 0);
7819                insta::assert_json_snapshot!(metric.loc);
7820            },
7821        );
7822    }
7823
7824    #[cfg(feature = "mozjs")]
7825    #[test]
7826    fn mozjs_if_lloc() {
7827        check_metrics::<MozjsParser>(
7828            "function f(x) {
7829            if (x > 0) {
7830                return 1;
7831            } else {
7832                return -1;
7833            }
7834        }",
7835            "foo.js",
7836            |metric| {
7837                assert_eq!(metric.loc.sloc(), 7);
7838                assert_eq!(metric.loc.ploc(), 7);
7839                // lloc = 3: the `if` statement plus the two `return`
7840                // statements. The three `{ … }` brace blocks (function
7841                // body, `if` consequent, `else` alternative) are syntactic
7842                // groupings, not logical statements, and contribute 0 —
7843                // matching C/Rust/Java for the equivalent code (#777).
7844                // Pre-#777 this asserted 6 (every StatementBlock counted).
7845                assert_eq!(metric.loc.lloc(), 3);
7846                assert_eq!(metric.loc.cloc(), 0);
7847                assert_eq!(metric.loc.blank(), 0);
7848                insta::assert_json_snapshot!(metric.loc);
7849            },
7850        );
7851    }
7852
7853    // Cross-language parity (#777): the same if/else function body yields
7854    // identical lloc across the JS family and the C-family / Rust baselines.
7855    // Removing `StatementBlock` from the JS-family lloc arms restored this
7856    // invariant — every brace block now contributes 0 lloc, as it always
7857    // had elsewhere. Pre-#777 the JS variants reported lloc 6 (three brace
7858    // blocks over-counted) against C's and Rust's 3.
7859    #[cfg(all(
7860        feature = "cpp",
7861        feature = "javascript",
7862        feature = "mozjs",
7863        feature = "rust",
7864        feature = "typescript",
7865    ))]
7866    #[test]
7867    fn js_family_if_lloc_matches_c_and_rust() {
7868        #[cfg(any(
7869            feature = "cpp",
7870            feature = "javascript",
7871            feature = "mozjs",
7872            feature = "rust",
7873            feature = "typescript",
7874        ))]
7875        const JS_SRC: &str = "function f(x) {
7876            if (x > 0) {
7877                return 1;
7878            } else {
7879                return -1;
7880            }
7881        }";
7882        #[cfg(any(
7883            feature = "cpp",
7884            feature = "javascript",
7885            feature = "mozjs",
7886            feature = "rust",
7887            feature = "typescript",
7888        ))]
7889        const C_SRC: &str = "int f(int x) {
7890            if (x > 0) {
7891                return 1;
7892            } else {
7893                return -1;
7894            }
7895        }";
7896        #[cfg(any(
7897            feature = "cpp",
7898            feature = "javascript",
7899            feature = "mozjs",
7900            feature = "rust",
7901            feature = "typescript",
7902        ))]
7903        const RUST_SRC: &str = "fn f(x: i32) -> i32 {
7904            if x > 0 {
7905                return 1;
7906            } else {
7907                return -1;
7908            }
7909        }";
7910
7911        // The logical-statement count is grammar-independent: one `if`
7912        // plus two `return`s, regardless of brace style or language.
7913        #[cfg(any(
7914            feature = "cpp",
7915            feature = "javascript",
7916            feature = "mozjs",
7917            feature = "rust",
7918            feature = "typescript",
7919        ))]
7920        const EXPECTED_LLOC: usize = 3;
7921
7922        check_metrics::<CppParser>(C_SRC, "f.c", |m| {
7923            assert_eq!(m.loc.lloc() as usize, EXPECTED_LLOC);
7924        });
7925        check_metrics::<RustParser>(RUST_SRC, "f.rs", |m| {
7926            assert_eq!(m.loc.lloc() as usize, EXPECTED_LLOC);
7927        });
7928        check_metrics::<MozjsParser>(JS_SRC, "f.js", |m| {
7929            assert_eq!(m.loc.lloc() as usize, EXPECTED_LLOC);
7930        });
7931        check_metrics::<JavascriptParser>(JS_SRC, "f.js", |m| {
7932            assert_eq!(m.loc.lloc() as usize, EXPECTED_LLOC);
7933        });
7934        check_metrics::<TypescriptParser>(JS_SRC, "f.ts", |m| {
7935            assert_eq!(m.loc.lloc() as usize, EXPECTED_LLOC);
7936        });
7937        check_metrics::<TsxParser>(JS_SRC, "f.tsx", |m| {
7938            assert_eq!(m.loc.lloc() as usize, EXPECTED_LLOC);
7939        });
7940    }
7941
7942    #[cfg(feature = "mozjs")]
7943    #[test]
7944    fn mozjs_for_lloc() {
7945        check_metrics::<MozjsParser>(
7946            "function f(n) {
7947            var s = 0;
7948            for (var i = 0; i < n; i++) {
7949                s += i;
7950            }
7951            return s;
7952        }",
7953            "foo.js",
7954            |metric| {
7955                assert_eq!(metric.loc.sloc(), 7);
7956                assert_eq!(metric.loc.ploc(), 7);
7957                assert_eq!(metric.loc.lloc(), 4);
7958                assert_eq!(metric.loc.cloc(), 0);
7959                assert_eq!(metric.loc.blank(), 0);
7960                insta::assert_json_snapshot!(metric.loc);
7961            },
7962        );
7963    }
7964
7965    #[cfg(feature = "bash")]
7966    #[test]
7967    fn bash_blank() {
7968        check_metrics::<BashParser>(
7969            "#!/bin/bash
7970
7971        f() {
7972
7973            echo hello
7974
7975        }",
7976            "foo.sh",
7977            |metric| {
7978                assert_eq!(metric.loc.sloc(), 7);
7979                assert_eq!(metric.loc.ploc(), 3);
7980                assert_eq!(metric.loc.lloc(), 2);
7981                assert_eq!(metric.loc.cloc(), 1);
7982                assert_eq!(metric.loc.blank(), 3);
7983                insta::assert_json_snapshot!(metric.loc);
7984            },
7985        );
7986    }
7987
7988    #[cfg(feature = "bash")]
7989    #[test]
7990    fn bash_cloc() {
7991        check_metrics::<BashParser>(
7992            "# header comment
7993        f() {
7994            # body comment
7995            echo hello
7996        }",
7997            "foo.sh",
7998            |metric| {
7999                assert_eq!(metric.loc.sloc(), 5);
8000                assert_eq!(metric.loc.ploc(), 3);
8001                assert_eq!(metric.loc.lloc(), 2);
8002                assert_eq!(metric.loc.cloc(), 2);
8003                assert_eq!(metric.loc.blank(), 0);
8004                insta::assert_json_snapshot!(metric.loc);
8005            },
8006        );
8007    }
8008
8009    #[cfg(feature = "bash")]
8010    #[test]
8011    fn bash_no_zero_blank() {
8012        // Blank line interleaved with code that carries trailing comments —
8013        // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
8014        check_metrics::<BashParser>(
8015            "f() {
8016  echo a
8017
8018  echo b # trailing
8019  echo c # trailing
8020}",
8021            "foo.sh",
8022            |metric| {
8023                assert_eq!(metric.loc.sloc(), 6);
8024                assert_eq!(metric.loc.ploc(), 5);
8025                assert_eq!(metric.loc.cloc(), 2);
8026                assert_eq!(metric.loc.blank(), 1);
8027                insta::assert_json_snapshot!(metric.loc);
8028            },
8029        );
8030    }
8031
8032    #[cfg(feature = "bash")]
8033    #[test]
8034    fn bash_comment_before_code_line_reclassified() {
8035        // Regression for #547: a standalone `#` comment sitting on a line
8036        // that the grammar *also* anchors a zero-width code leaf to (the
8037        // empty `word` tree-sitter-bash emits inside a `$(...)` command
8038        // substitution that contains only a comment) must reclassify that
8039        // row from comment-only to code-comment. The Bash `Loc` leaf arm
8040        // previously omitted `check_comment_ends_on_code_line` (unlike
8041        // Elixir and every other impl), so the row was credited to BOTH
8042        // `ploc` and the comment-only set and `blank` was undercounted by
8043        // one.
8044        //
8045        // Source rows: 0 `echo a`, 1 blank, 2 `echo "$(`, 3 `  # c`,
8046        // 4 `)"`. expected: sloc=5 (every physical row),
8047        // ploc=4 (rows 0/2/3/4 — row 3 carries the phantom code leaf),
8048        // lloc=3, cloc=1 (row 3, now a code-comment line, not comment-only),
8049        // blank=1 (row 1). Without the fix `blank` collapses to 0 because
8050        // row 3 is double-counted. Verified fail-on-revert per
8051        // .claude/rules/testing.md.
8052        check_metrics::<BashParser>("echo a\n\necho \"$(\n  # c\n)\"\n", "foo.sh", |metric| {
8053            assert_eq!(metric.loc.sloc(), 5);
8054            assert_eq!(metric.loc.ploc(), 4);
8055            assert_eq!(metric.loc.lloc(), 3);
8056            assert_eq!(metric.loc.cloc(), 1);
8057            assert_eq!(metric.loc.blank(), 1);
8058            insta::assert_json_snapshot!(
8059                metric.loc,
8060                @r#"
8061            {
8062              "sloc": 5,
8063              "ploc": 4,
8064              "lloc": 3,
8065              "cloc": 1,
8066              "blank": 1,
8067              "dloc": 0,
8068              "sloc_average": 5.0,
8069              "ploc_average": 4.0,
8070              "lloc_average": 3.0,
8071              "cloc_average": 1.0,
8072              "blank_average": 1.0,
8073              "dloc_average": 0.0,
8074              "sloc_min": 5,
8075              "sloc_max": 5,
8076              "cloc_min": 1,
8077              "cloc_max": 1,
8078              "ploc_min": 4,
8079              "ploc_max": 4,
8080              "lloc_min": 3,
8081              "lloc_max": 3,
8082              "blank_min": 1,
8083              "blank_max": 1,
8084              "dloc_min": 0,
8085              "dloc_max": 0
8086            }
8087            "#
8088            );
8089        });
8090    }
8091
8092    #[cfg(feature = "bash")]
8093    #[test]
8094    fn bash_if_lloc() {
8095        check_metrics::<BashParser>(
8096            "f() {
8097            if [ $1 -gt 0 ]; then
8098                echo positive
8099            else
8100                echo negative
8101            fi
8102        }",
8103            "foo.sh",
8104            |metric| {
8105                assert_eq!(metric.loc.sloc(), 7);
8106                assert_eq!(metric.loc.ploc(), 7);
8107                assert_eq!(metric.loc.lloc(), 4);
8108                assert_eq!(metric.loc.cloc(), 0);
8109                assert_eq!(metric.loc.blank(), 0);
8110                insta::assert_json_snapshot!(metric.loc);
8111            },
8112        );
8113    }
8114
8115    #[cfg(feature = "bash")]
8116    #[test]
8117    fn bash_for_lloc() {
8118        check_metrics::<BashParser>(
8119            "f() {
8120            for i in 1 2 3; do
8121                echo $i
8122            done
8123        }",
8124            "foo.sh",
8125            |metric| {
8126                assert_eq!(metric.loc.sloc(), 5);
8127                assert_eq!(metric.loc.ploc(), 5);
8128                assert_eq!(metric.loc.lloc(), 3);
8129                assert_eq!(metric.loc.cloc(), 0);
8130                assert_eq!(metric.loc.blank(), 0);
8131                insta::assert_json_snapshot!(metric.loc);
8132            },
8133        );
8134    }
8135
8136    #[cfg(feature = "bash")]
8137    #[test]
8138    fn bash_while_lloc() {
8139        check_metrics::<BashParser>(
8140            "f() {
8141            local n=5
8142            while [ $n -gt 0 ]; do
8143                echo $n
8144                n=$((n - 1))
8145            done
8146        }",
8147            "foo.sh",
8148            |metric| {
8149                assert_eq!(metric.loc.sloc(), 7);
8150                assert_eq!(metric.loc.ploc(), 7);
8151                // `f()`, `local n=5`, `while`, `echo $n`, and the
8152                // `n=$((n - 1))` assignment. That last one read as zero
8153                // until the LLOC arm learned the `variable_assignment`
8154                // alias the parser actually emits, so this asserted 4.
8155                assert_eq!(metric.loc.lloc(), 5);
8156                assert_eq!(metric.loc.cloc(), 0);
8157                assert_eq!(metric.loc.blank(), 0);
8158                insta::assert_json_snapshot!(metric.loc);
8159            },
8160        );
8161    }
8162
8163    /// A Bash assignment is a logical line where it stands as a
8164    /// statement, and no line of its own where it is part of one.
8165    ///
8166    /// The parser emits `variable_assignment` under the id the enum
8167    /// spells `VariableAssignment2`; the unsuffixed `VariableAssignment`
8168    /// it never emits, so the LLOC arm listing only that scored a bare
8169    /// `a=1` zero (`.claude/rules/grammar-dispatch.md` §1). Adding the
8170    /// alias ungated then double-counted the two positions where the
8171    /// assignment hangs off a node the same arm already counts — a
8172    /// `declaration_command` and a `command`'s environment prefix (§5).
8173    ///
8174    /// Both halves need a row: a table of standalone assignments alone
8175    /// passes with the parent gate deleted, and a table of wrapped ones
8176    /// alone passes with the alias never added.
8177    #[cfg(feature = "bash")]
8178    #[test]
8179    fn bash_assignment_counts_one_logical_line_per_statement() {
8180        for (source, lloc) in [
8181            (&b"a=1\nb=2\nc=3\n"[..], 3),
8182            (&b"local d=4\n"[..], 1),
8183            (&b"export e=5\n"[..], 1),
8184            (&b"declare -a g=(1 2)\n"[..], 1),
8185            (&b"X=1 cmd arg\n"[..], 1),
8186            (&b"(h=6)\n"[..], 1),
8187            (&b"for i in 1; do j=7; done\n"[..], 2),
8188        ] {
8189            let measured = metrics_verbatim(crate::LANG::Bash, source, MetricsOptions::default())
8190                .loc
8191                .lloc();
8192            assert_eq!(measured, lloc, "{:?}", String::from_utf8_lossy(source));
8193        }
8194    }
8195
8196    #[cfg(feature = "bash")]
8197    #[test]
8198    fn bash_case_lloc() {
8199        check_metrics::<BashParser>(
8200            "f() {
8201            case $1 in
8202                start) echo starting ;;
8203                stop)  echo stopping ;;
8204                *)     echo unknown  ;;
8205            esac
8206        }",
8207            "foo.sh",
8208            |metric| {
8209                assert_eq!(metric.loc.sloc(), 7);
8210                assert_eq!(metric.loc.ploc(), 7);
8211                assert_eq!(metric.loc.lloc(), 5);
8212                assert_eq!(metric.loc.cloc(), 0);
8213                assert_eq!(metric.loc.blank(), 0);
8214                insta::assert_json_snapshot!(metric.loc);
8215            },
8216        );
8217    }
8218
8219    #[cfg(feature = "bash")]
8220    #[test]
8221    fn bash_multiple_functions_loc() {
8222        check_metrics::<BashParser>(
8223            "f() {
8224            echo hello
8225        }
8226        g() {
8227            echo world
8228        }",
8229            "foo.sh",
8230            |metric| {
8231                assert_eq!(metric.loc.sloc(), 6);
8232                assert_eq!(metric.loc.ploc(), 6);
8233                assert_eq!(metric.loc.lloc(), 4);
8234                assert_eq!(metric.loc.cloc(), 0);
8235                assert_eq!(metric.loc.blank(), 0);
8236                insta::assert_json_snapshot!(metric.loc);
8237            },
8238        );
8239    }
8240
8241    #[cfg(feature = "bash")]
8242    #[test]
8243    fn bash_nested_function_loc() {
8244        check_metrics::<BashParser>(
8245            "outer() {
8246            inner() {
8247                echo inner
8248            }
8249            inner
8250            echo outer
8251        }",
8252            "foo.sh",
8253            |metric| {
8254                assert_eq!(metric.loc.sloc(), 7);
8255                assert_eq!(metric.loc.ploc(), 7);
8256                assert_eq!(metric.loc.lloc(), 5);
8257                assert_eq!(metric.loc.cloc(), 0);
8258                assert_eq!(metric.loc.blank(), 0);
8259                insta::assert_json_snapshot!(metric.loc);
8260            },
8261        );
8262    }
8263
8264    #[cfg(feature = "bash")]
8265    #[test]
8266    fn bash_heredoc_loc() {
8267        // expected: six physical rows (`f() {`, `cat <<EOF`, `line1`,
8268        // `line2`, `EOF`, `}`), all code and none blank — a heredoc body's
8269        // rows are real source text (#1260, following #778 / #415). This
8270        // test pinned `ploc 5 / blank 1` until #1260: `heredoc_body` is a
8271        // childless node spanning both body rows, so the leaf arm credited
8272        // only `line1` and `line2` reached neither PLOC nor CLOC.
8273        check_metrics::<BashParser>(
8274            "f() {
8275            cat <<EOF
8276line1
8277line2
8278EOF
8279        }",
8280            "foo.sh",
8281            |metric| {
8282                assert_eq!(metric.loc.sloc(), 6);
8283                assert_eq!(metric.loc.ploc(), 6);
8284                assert_eq!(metric.loc.lloc(), 2);
8285                assert_eq!(metric.loc.cloc(), 0);
8286                assert_eq!(metric.loc.blank(), 0);
8287                insta::assert_json_snapshot!(metric.loc);
8288            },
8289        );
8290    }
8291
8292    #[cfg(feature = "bash")]
8293    #[test]
8294    fn bash_multiline_literals_credit_every_row_to_ploc() {
8295        // Regression test for #1260. Bash routed no string kind to a
8296        // PLOC/CLOC bucket, so the interior rows of every multi-row literal
8297        // reached neither and `blank = sloc - ploc - cloc` claimed them.
8298        //
8299        // Each fixture isolates one arm of the new match, so a dropped kind
8300        // fails here rather than being covered by a sibling
8301        // (`.claude/rules/grammar-dispatch.md` section 11): `raw_string`,
8302        // `ansi_c_string` and `heredoc_body` are childless nodes the old
8303        // leaf arm credited only one row of, while `string` emits a
8304        // `string_content` child per row that *has* text and so skipped an
8305        // empty interior row.
8306
8307        // expected: 5 rows — `cat <<DOC`, `line one`, `line two`, `DOC`,
8308        // `echo done` — all code, none blank. lloc 2 for the two commands.
8309        check_metrics::<BashParser>(
8310            "cat <<DOC\nline one\nline two\nDOC\necho done",
8311            "foo.sh",
8312            |metric| {
8313                assert_eq!(metric.loc.sloc(), 5);
8314                assert_eq!(metric.loc.ploc(), 5);
8315                assert_eq!(metric.loc.lloc(), 2);
8316                assert_eq!(metric.loc.cloc(), 0);
8317                assert_eq!(metric.loc.blank(), 0);
8318            },
8319        );
8320
8321        // expected: 3 rows of a single-quoted `raw_string` — `s='one`,
8322        // `two`, `three'` — all code, none blank.
8323        check_metrics::<BashParser>("s='one\ntwo\nthree'", "foo.sh", |metric| {
8324            assert_eq!(metric.loc.sloc(), 3);
8325            assert_eq!(metric.loc.ploc(), 3);
8326            assert_eq!(metric.loc.cloc(), 0);
8327            assert_eq!(metric.loc.blank(), 0);
8328        });
8329
8330        // expected: 2 rows of an `ansi_c_string` (`$'…'`) — `s=$'one` and
8331        // `two'` — both code, neither blank.
8332        check_metrics::<BashParser>("s=$'one\ntwo'", "foo.sh", |metric| {
8333            assert_eq!(metric.loc.sloc(), 2);
8334            assert_eq!(metric.loc.ploc(), 2);
8335            assert_eq!(metric.loc.cloc(), 0);
8336            assert_eq!(metric.loc.blank(), 0);
8337        });
8338
8339        // expected: 3 rows of a double-quoted `string` whose middle row is
8340        // empty — `s="one`, ``, `three"`. The empty row is inside the
8341        // literal, so it is code, not blank: every one of the eighteen
8342        // languages that already route a multi-line literal answers 0 blank
8343        // for this shape, and Bash now agrees.
8344        check_metrics::<BashParser>("s=\"one\n\nthree\"", "foo.sh", |metric| {
8345            assert_eq!(metric.loc.sloc(), 3);
8346            assert_eq!(metric.loc.ploc(), 3);
8347            assert_eq!(metric.loc.cloc(), 0);
8348            assert_eq!(metric.loc.blank(), 0);
8349        });
8350
8351        // expected: 4 rows of a quoted, tab-stripping heredoc — the
8352        // `cat <<-'DOC'` opener, one body row, an empty body row, and the
8353        // `DOC` terminator — all code, none blank. `<<-'DOC'` suppresses
8354        // interpolation, which is a distinct grammar path from the bare
8355        // `<<DOC` above.
8356        check_metrics::<BashParser>("cat <<-'DOC'\n\tline one\n\nDOC", "foo.sh", |metric| {
8357            assert_eq!(metric.loc.sloc(), 4);
8358            assert_eq!(metric.loc.ploc(), 4);
8359            assert_eq!(metric.loc.lloc(), 1);
8360            assert_eq!(metric.loc.cloc(), 0);
8361            assert_eq!(metric.loc.blank(), 0);
8362        });
8363
8364        // expected: 2 rows for a heredoc with an empty body — `cat <<DOC`
8365        // and the `DOC` terminator. The body node is zero-width here, so
8366        // this pins that the new arm adds no phantom row.
8367        check_metrics::<BashParser>("cat <<DOC\nDOC", "foo.sh", |metric| {
8368            assert_eq!(metric.loc.sloc(), 2);
8369            assert_eq!(metric.loc.ploc(), 2);
8370            assert_eq!(metric.loc.cloc(), 0);
8371            assert_eq!(metric.loc.blank(), 0);
8372        });
8373    }
8374
8375    /// Analyses `source` byte-for-byte as Bash, for the #1412 fixtures
8376    /// `check_metrics` cannot carry: that shim trims trailing newlines,
8377    /// and a heredoc whose terminator is the last row is exactly the
8378    /// shape it normalises away (`.claude/rules/testing.md`).
8379    #[cfg(feature = "bash")]
8380    fn bash_loc_verbatim(source: &[u8]) -> Stats {
8381        metrics_verbatim(
8382            crate::LANG::Bash,
8383            source,
8384            crate::MetricsOptions::default().with_only(&[crate::Metric::Loc]),
8385        )
8386        .loc
8387    }
8388
8389    /// #1412: a heredoc body that is empty at the top, or empty
8390    /// throughout, read as blank. `heredoc_body`'s span begins at the
8391    /// first body row that *has* text and collapses to zero width when
8392    /// there is none, so the leading empty rows sat inside no node at
8393    /// all and `blank = sloc - ploc - cloc` claimed them.
8394    ///
8395    /// #1260 routed the body, which fixed only the interior of a body
8396    /// that has text — the shape all of its fixtures had. The arm now
8397    /// routes the `heredoc_redirect` wrapper, the node present for every
8398    /// spelling (`.claude/rules/grammar-dispatch.md` section 6), which
8399    /// is what #1396 did for PHP.
8400    #[cfg(feature = "bash")]
8401    #[test]
8402    fn bash_heredoc_bodies_that_open_or_stay_empty_are_not_blank() {
8403        // `(source, rows the file has, blank reported before #1412)`.
8404        // The last column is what makes each row a regression test
8405        // rather than a restatement of the fixed behaviour.
8406        //
8407        // `sloc` is the second axis the fixtures are anchored on
8408        // (`.claude/rules/testing.md`): trimming the heredoc out of any
8409        // of them leaves a bare `cat` — or, in the last row, a bare
8410        // function — which fails the `sloc` assertion before the `blank`
8411        // one can go vacuous.
8412        const FIXTURES: &[(&[u8], u64, u64)] = &[
8413            // The issue's four reproductions. The fourth is the control:
8414            // it is the shape #1260's own fixtures had and was already
8415            // correct, so a "fix" that simply stopped counting body rows
8416            // moves it off `ploc == rows`.
8417            (b"cat <<EOT\n\nEOT\n", 3, 1),
8418            (b"cat <<EOT\n\n\nEOT\n", 4, 2),
8419            (b"cat <<EOT\n\na\nEOT\n", 4, 1),
8420            (b"cat <<EOT\na\n\nEOT\n", 4, 0),
8421            // The spellings and positions those four do not reach, each
8422            // a different grammar path to the same wrapper: a body empty
8423            // *and* terminated at EOF (`heredoc_end` is the last byte,
8424            // which `check_metrics` cannot express), the tab-stripping
8425            // `<<-` opener, the quoted `<<'EOT'` opener that suppresses
8426            // interpolation, and a heredoc nested in a function body.
8427            (b"cat <<EOT\n\nEOT", 3, 1),
8428            (b"cat <<-EOT\n\n\tx\n\tEOT\n", 4, 1),
8429            (b"cat <<'EOT'\n\nx\nEOT\n", 4, 1),
8430            (b"f() {\n  cat <<EOT\n\n  x\nEOT\n}\n", 6, 1),
8431        ];
8432
8433        for &(source, rows, blank_before) in FIXTURES {
8434            let text = String::from_utf8_lossy(source);
8435            let loc = bash_loc_verbatim(source);
8436            assert_eq!(loc.sloc(), rows, "sloc for {text:?}");
8437            assert_eq!(loc.ploc(), rows, "ploc for {text:?}");
8438            assert_eq!(loc.cloc(), 0, "cloc for {text:?}");
8439            assert_eq!(
8440                loc.blank(),
8441                0,
8442                "blank for {text:?}, which reported {blank_before} before #1412"
8443            );
8444        }
8445    }
8446
8447    /// The wrapper's span must not reach past the heredoc: rows after
8448    /// the terminator are the caller's own, blank ones included. Routing
8449    /// a *container* rather than the body is exactly the change that
8450    /// could over-credit, so this is the upper bound for #1412 the way
8451    /// `a_multiline_literal_credits_no_row_past_its_own_span` is for
8452    /// #1260.
8453    #[cfg(feature = "bash")]
8454    #[test]
8455    fn bash_heredoc_wrapper_credits_no_row_past_its_terminator() {
8456        // rows: 0 `cat <<EOT`, 1 (empty body), 2 `EOT`, 3 (blank),
8457        // 4 `# note`, 5 `echo after`. The blank row and the comment row
8458        // are outside the literal and must stay that way.
8459        let loc = bash_loc_verbatim(b"cat <<EOT\n\nEOT\n\n# note\necho after\n");
8460        assert_eq!(loc.sloc(), 6);
8461        assert_eq!(loc.ploc(), 4);
8462        assert_eq!(loc.cloc(), 1);
8463        assert_eq!(loc.blank(), 1);
8464    }
8465
8466    /// A `\`-continuation row inside a heredoc's command prefix is code
8467    /// (#1443, #1445).
8468    ///
8469    /// `heredoc_redirect` spans the command-line prefix as well as the
8470    /// literal, so crediting its whole interior over-credits a blank or
8471    /// comment-only prefix row. #1443 narrowed the range to the literal's
8472    /// own rows to fix that, and the narrowing was reverted: the shape it
8473    /// fixed needs `bash -n` to reject the file, while the shape it broke
8474    /// — this one — runs.
8475    ///
8476    /// A continuation row holding only `\` carries no leaf for the
8477    /// leaf-gated catch-all to credit, so the wrapper's blanket range is
8478    /// the only thing covering it. Narrowing took it from `ploc 5` to
8479    /// `ploc 4`. The general gap is #1445 (`echo a | \` + `\` +
8480    /// `  grep b` loses the row with no heredoc involved); until that
8481    /// lands, this pins the row the blanket range saves, so a second
8482    /// attempt at the narrowing fails here rather than shipping.
8483    #[cfg(feature = "bash")]
8484    #[test]
8485    fn bash_heredoc_continuation_row_is_code() {
8486        // rows: 0 `cat <<EOT | \`, 1 `\`, 2 `  grep x`, 3 `body`, 4 `EOT`.
8487        // Valid Bash — `bash -n` accepts it — and every row is code.
8488        let continuation = bash_loc_verbatim(b"cat <<EOT | \\\n\\\n  grep x\nbody\nEOT\n");
8489        assert_eq!(continuation.sloc(), 5);
8490        assert_eq!(
8491            continuation.ploc(),
8492            5,
8493            "the `\\`-only continuation row is part of the command"
8494        );
8495        assert_eq!(continuation.cloc(), 0);
8496        assert_eq!(continuation.blank(), 0);
8497
8498        // The same command with content on the continuation row, which
8499        // the catch-all credits through its own leaves. Unaffected by the
8500        // range either way, and here so the pair brackets the shape.
8501        let with_content = bash_loc_verbatim(b"cat <<EOT | \\\n  grep x\nbody\nEOT\n");
8502        assert_eq!(
8503            (
8504                with_content.sloc(),
8505                with_content.ploc(),
8506                with_content.blank()
8507            ),
8508            (4, 4, 0)
8509        );
8510    }
8511
8512    /// The orphan `heredoc_body` — the shape that makes routing the
8513    /// wrapper an *addition* rather than a replacement.
8514    ///
8515    /// `node-types.json` lists `heredoc_body` only as a child of
8516    /// `heredoc_redirect`, which describes the well-formed grammar and
8517    /// says nothing about error recovery. A single-line compound
8518    /// carrying a heredoc parses to an `{ERROR}` root whose direct child
8519    /// is the body, with no wrapper anywhere — so an arm keyed on the
8520    /// wrapper alone leaves that node to the leaf-gated catch-all, which
8521    /// credits only its start row, and the terminator row falls to
8522    /// `blank`. That is #1412's own symptom.
8523    ///
8524    /// Every input here is valid, executable Bash. Measured: each
8525    /// reported `blank 1` while the body kinds were dropped from the arm
8526    /// and `blank 0` both before and after.
8527    #[cfg(feature = "bash")]
8528    #[test]
8529    fn bash_heredoc_body_without_a_wrapper_still_credits_every_row() {
8530        // Three rows each: the compound, the body, the terminator. All
8531        // code, none blank — the same answer the well-formed spellings
8532        // give above.
8533        for source in [
8534            &b"f() { cat <<EOT; }\nbody\nEOT\n"[..],
8535            &b"if true; then cat <<EOT; fi\nx\nEOT\n"[..],
8536            &b"for i in 1; do cat <<EOT; done\nx\nEOT\n"[..],
8537        ] {
8538            let loc = bash_loc_verbatim(source);
8539            let text = String::from_utf8_lossy(source);
8540            assert_eq!(loc.sloc(), 3, "sloc for {text:?}");
8541            assert_eq!(loc.ploc(), 3, "ploc for {text:?}");
8542            assert_eq!(loc.cloc(), 0, "cloc for {text:?}");
8543            assert_eq!(
8544                loc.blank(),
8545                0,
8546                "the terminator row of a wrapper-less heredoc body is code, not blank, for {text:?}"
8547            );
8548        }
8549    }
8550
8551    #[cfg(feature = "bash")]
8552    #[test]
8553    fn bash_literal_that_owns_its_opening_row_still_credits_it() {
8554        // The other branch of `add_multiline_string_ploc`'s parent gate,
8555        // and the one every fixture above misses: each of those writes the
8556        // literal after `s=`, so the opening row is already PLOC and the
8557        // gate's *skip* path is harmless. Here the literal is the only
8558        // thing on its row.
8559        //
8560        // Bash is the language where that distinction bites. The helper
8561        // skips the opening row when the parent starts on it, which is safe
8562        // only where the catch-all credits every node's start row — and
8563        // Bash's is leaf-gated, so `command` / `command_name` contribute
8564        // nothing and a childless `raw_string` is the sole node covering
8565        // its own row. Routing it through the helper alone reported
8566        // `ploc 0, blank 1` for a one-line file.
8567
8568        // expected: one row, one code row, nothing blank. `'ls'` parses as
8569        // `command → command_name → raw_string`, none of whose ancestors
8570        // reaches PLOC.
8571        check_metrics::<BashParser>("'ls'", "foo.sh", |metric| {
8572            assert_eq!(metric.loc.sloc(), 1);
8573            assert_eq!(metric.loc.ploc(), 1);
8574            assert_eq!(metric.loc.cloc(), 0);
8575            assert_eq!(metric.loc.blank(), 0);
8576        });
8577
8578        // expected: the same for an `ansi_c_string`, the other childless
8579        // single-row literal.
8580        check_metrics::<BashParser>("$'ls'", "foo.sh", |metric| {
8581            assert_eq!(metric.loc.sloc(), 1);
8582            assert_eq!(metric.loc.ploc(), 1);
8583            assert_eq!(metric.loc.cloc(), 0);
8584            assert_eq!(metric.loc.blank(), 0);
8585        });
8586
8587        // expected: 3 rows — `f() {`, `  'ls'`, `}` — all code. The literal
8588        // sits inside a function body, so a lost row shows as blank 1
8589        // rather than as an empty file.
8590        check_metrics::<BashParser>("f() {\n  'ls'\n}", "foo.sh", |metric| {
8591            assert_eq!(metric.loc.sloc(), 3);
8592            assert_eq!(metric.loc.ploc(), 3);
8593            assert_eq!(metric.loc.cloc(), 0);
8594            assert_eq!(metric.loc.blank(), 0);
8595        });
8596
8597        // expected: 2 rows — `# c` (comment-only) and `'ls'` (code). This
8598        // also covers the `check_comment_ends_on_code_line` call the gate
8599        // skipped along with the row: cloc stays 1 and blank stays 0.
8600        check_metrics::<BashParser>("# c\n'ls'", "foo.sh", |metric| {
8601            assert_eq!(metric.loc.sloc(), 2);
8602            assert_eq!(metric.loc.ploc(), 1);
8603            assert_eq!(metric.loc.cloc(), 1);
8604            assert_eq!(metric.loc.blank(), 0);
8605        });
8606
8607        // expected: 2 rows of a multi-row raw string that is the whole
8608        // command — `'a` and `b'`. Both code. Before #1260 this reported
8609        // `ploc 1` (the opening row only); the gate alone would have
8610        // reported `ploc 1` again, having traded the opening row for the
8611        // closing one.
8612        check_metrics::<BashParser>("'a\nb'", "foo.sh", |metric| {
8613            assert_eq!(metric.loc.sloc(), 2);
8614            assert_eq!(metric.loc.ploc(), 2);
8615            assert_eq!(metric.loc.cloc(), 0);
8616            assert_eq!(metric.loc.blank(), 0);
8617        });
8618    }
8619
8620    #[cfg(all(feature = "bash", feature = "tcl"))]
8621    #[test]
8622    fn a_multiline_literal_credits_no_row_past_its_own_span() {
8623        // The #1260 arms insert a *range* of rows, so the row after the
8624        // literal is the boundary worth pinning: an off-by-one in the range
8625        // end, or a literal node whose span is read one row too wide, would
8626        // pull the following comment row into PLOC. Each fixture puts a
8627        // comment-only row immediately after a closed literal, so that row
8628        // must stay comment-only.
8629
8630        // expected: 7 rows — `cat <<A`, `a1`, `A`, `# note`, `cat <<B`,
8631        // `b1`, `B`. Six are code and row 4 is comment-only: ploc 6,
8632        // cloc 1, blank 0. Two heredocs, so the row is bracketed on both
8633        // sides by a literal.
8634        check_metrics::<BashParser>(
8635            "cat <<A\na1\nA\n# note\ncat <<B\nb1\nB",
8636            "foo.sh",
8637            |metric| {
8638                assert_eq!(metric.loc.sloc(), 7);
8639                assert_eq!(metric.loc.ploc(), 6);
8640                assert_eq!(metric.loc.cloc(), 1);
8641                assert_eq!(metric.loc.blank(), 0);
8642            },
8643        );
8644
8645        // expected: 4 rows — `set a "x`, `y"`, `# note`, `set b 1` — with
8646        // row 3 comment-only: ploc 3, cloc 1, blank 0.
8647        check_metrics::<TclParser>("set a \"x\ny\"\n# note\nset b 1", "foo.tcl", |metric| {
8648            assert_eq!(metric.loc.sloc(), 4);
8649            assert_eq!(metric.loc.ploc(), 3);
8650            assert_eq!(metric.loc.cloc(), 1);
8651            assert_eq!(metric.loc.blank(), 0);
8652        });
8653    }
8654
8655    #[cfg(feature = "kotlin")]
8656    #[test]
8657    fn kotlin_loc_blank() {
8658        check_metrics::<KotlinParser>(
8659            "fun f(): Int {
8660
8661            val x = 1
8662
8663            return x
8664        }",
8665            "foo.kt",
8666            |metric| {
8667                assert_eq!(metric.loc.sloc(), 6);
8668                assert_eq!(metric.loc.ploc(), 4);
8669                assert_eq!(metric.loc.lloc(), 2);
8670                assert_eq!(metric.loc.cloc(), 0);
8671                assert_eq!(metric.loc.blank(), 2);
8672                insta::assert_json_snapshot!(metric.loc);
8673            },
8674        );
8675    }
8676
8677    #[cfg(feature = "kotlin")]
8678    #[test]
8679    fn kotlin_loc_cloc() {
8680        check_metrics::<KotlinParser>(
8681            "// header comment
8682        /* block
8683           comment */
8684        fun f(): Int {
8685            return 42 // inline
8686        }",
8687            "foo.kt",
8688            |metric| {
8689                assert_eq!(metric.loc.sloc(), 6);
8690                assert_eq!(metric.loc.ploc(), 3);
8691                assert_eq!(metric.loc.lloc(), 1);
8692                assert_eq!(metric.loc.cloc(), 4);
8693                assert_eq!(metric.loc.blank(), 0);
8694                insta::assert_json_snapshot!(metric.loc);
8695            },
8696        );
8697    }
8698
8699    #[cfg(feature = "kotlin")]
8700    #[test]
8701    fn kotlin_loc_no_zero_blank() {
8702        // Checks that the blank metric is not equal to 0 when there are some
8703        // comments next to code lines. Mirrors rust_no_zero_blank.
8704        check_metrics::<KotlinParser>(
8705            "fun connectToUpdateServer() {
8706              val pool = 0
8707
8708              val updateServer = -42
8709              val isConnected = false
8710              val currTry = 0
8711              val numRetries = 10  // Number of IPC connection retries before
8712                                    // giving up.
8713              val numTries = 20    // Number of IPC connection tries before
8714                                    // giving up.
8715            }",
8716            "foo.kt",
8717            |metric| {
8718                // Anchor the headline integer values; in particular
8719                // `blank() > 0` is the contract this test's name advertises.
8720                assert_eq!(metric.loc.sloc(), 11);
8721                assert_eq!(metric.loc.ploc(), 8);
8722                assert_eq!(metric.loc.cloc(), 4);
8723                assert_eq!(metric.loc.blank(), 1);
8724                insta::assert_json_snapshot!(
8725                    metric.loc,
8726                    @r#"
8727                {
8728                  "sloc": 11,
8729                  "ploc": 8,
8730                  "lloc": 6,
8731                  "cloc": 4,
8732                  "blank": 1,
8733                  "dloc": 0,
8734                  "sloc_average": 5.5,
8735                  "ploc_average": 4.0,
8736                  "lloc_average": 3.0,
8737                  "cloc_average": 2.0,
8738                  "blank_average": 0.5,
8739                  "dloc_average": 0.0,
8740                  "sloc_min": 11,
8741                  "sloc_max": 11,
8742                  "cloc_min": 4,
8743                  "cloc_max": 4,
8744                  "ploc_min": 8,
8745                  "ploc_max": 8,
8746                  "lloc_min": 6,
8747                  "lloc_max": 6,
8748                  "blank_min": 1,
8749                  "blank_max": 1,
8750                  "dloc_min": 0,
8751                  "dloc_max": 0
8752                }
8753                "#
8754                );
8755            },
8756        );
8757    }
8758
8759    #[cfg(feature = "kotlin")]
8760    #[test]
8761    fn kotlin_loc_blank_zero_sanity() {
8762        // Sanity: when the source has no blank lines, blank() must be 0.
8763        // Preserves the no-blank coverage previously held by
8764        // kotlin_loc_no_zero_blank before it was rewritten to assert the
8765        // positive case its name advertises.
8766        check_metrics::<KotlinParser>(
8767            "fun f(): Int {
8768            val x = 1 // x
8769            val y = 2 // y
8770            return x + y
8771        }",
8772            "foo.kt",
8773            |metric| {
8774                assert_eq!(metric.loc.sloc(), 5);
8775                assert_eq!(metric.loc.ploc(), 5);
8776                assert_eq!(metric.loc.lloc(), 3);
8777                assert_eq!(metric.loc.cloc(), 2);
8778                assert_eq!(metric.loc.blank(), 0);
8779            },
8780        );
8781    }
8782
8783    #[cfg(feature = "kotlin")]
8784    #[test]
8785    fn kotlin_loc_if_lloc() {
8786        check_metrics::<KotlinParser>(
8787            "fun classify(n: Int): String {
8788            if (n > 0) {
8789                return \"positive\"
8790            } else if (n < 0) {
8791                return \"negative\"
8792            }
8793            return \"zero\"
8794        }",
8795            "foo.kt",
8796            |metric| {
8797                assert_eq!(metric.loc.sloc(), 8);
8798                assert_eq!(metric.loc.ploc(), 8);
8799                assert_eq!(metric.loc.lloc(), 5);
8800                assert_eq!(metric.loc.cloc(), 0);
8801                assert_eq!(metric.loc.blank(), 0);
8802                insta::assert_json_snapshot!(metric.loc);
8803            },
8804        );
8805    }
8806
8807    #[cfg(feature = "kotlin")]
8808    #[test]
8809    fn kotlin_loc_for_lloc() {
8810        check_metrics::<KotlinParser>(
8811            "fun sum(n: Int): Int {
8812            var s = 0
8813            for (i in 1..n) {
8814                s += i
8815            }
8816            return s
8817        }",
8818            "foo.kt",
8819            |metric| {
8820                assert_eq!(metric.loc.sloc(), 7);
8821                assert_eq!(metric.loc.ploc(), 7);
8822                assert_eq!(metric.loc.lloc(), 4);
8823                assert_eq!(metric.loc.cloc(), 0);
8824                assert_eq!(metric.loc.blank(), 0);
8825                insta::assert_json_snapshot!(metric.loc);
8826            },
8827        );
8828    }
8829
8830    #[cfg(feature = "kotlin")]
8831    #[test]
8832    fn kotlin_loc_when_lloc() {
8833        check_metrics::<KotlinParser>(
8834            "fun describe(x: Int): String {
8835            return when (x) {
8836                1 -> \"one\"
8837                2 -> \"two\"
8838                else -> \"other\"
8839            }
8840        }",
8841            "foo.kt",
8842            |metric| {
8843                assert_eq!(metric.loc.sloc(), 7);
8844                assert_eq!(metric.loc.ploc(), 7);
8845                assert_eq!(metric.loc.lloc(), 2);
8846                assert_eq!(metric.loc.cloc(), 0);
8847                assert_eq!(metric.loc.blank(), 0);
8848                insta::assert_json_snapshot!(metric.loc);
8849            },
8850        );
8851    }
8852
8853    #[cfg(feature = "kotlin")]
8854    #[test]
8855    fn kotlin_loc_lambda_lloc() {
8856        check_metrics::<KotlinParser>(
8857            "fun f(list: List<Int>): List<Int> {
8858            return list.filter { it > 0 }
8859                       .map { it * 2 }
8860        }",
8861            "foo.kt",
8862            |metric| {
8863                assert_eq!(metric.loc.sloc(), 4);
8864                assert_eq!(metric.loc.ploc(), 4);
8865                assert_eq!(metric.loc.lloc(), 1);
8866                assert_eq!(metric.loc.cloc(), 0);
8867                assert_eq!(metric.loc.blank(), 0);
8868                insta::assert_json_snapshot!(metric.loc);
8869            },
8870        );
8871    }
8872
8873    #[cfg(feature = "kotlin")]
8874    #[test]
8875    fn kotlin_loc_class_loc() {
8876        check_metrics::<KotlinParser>(
8877            "class Counter {
8878            private var count = 0
8879            fun increment() { count++ }
8880            fun get(): Int = count
8881        }",
8882            "foo.kt",
8883            |metric| {
8884                assert_eq!(metric.loc.sloc(), 5);
8885                assert_eq!(metric.loc.ploc(), 5);
8886                assert_eq!(metric.loc.lloc(), 1);
8887                assert_eq!(metric.loc.cloc(), 0);
8888                assert_eq!(metric.loc.blank(), 0);
8889                insta::assert_json_snapshot!(metric.loc);
8890            },
8891        );
8892    }
8893
8894    #[cfg(feature = "kotlin")]
8895    #[test]
8896    fn kotlin_loc_multiple_functions_loc() {
8897        check_metrics::<KotlinParser>(
8898            "fun f(): Int {
8899            return 1
8900        }
8901        fun g(): Int {
8902            return 2
8903        }",
8904            "foo.kt",
8905            |metric| {
8906                assert_eq!(metric.loc.sloc(), 6);
8907                assert_eq!(metric.loc.ploc(), 6);
8908                assert_eq!(metric.loc.lloc(), 2);
8909                assert_eq!(metric.loc.cloc(), 0);
8910                assert_eq!(metric.loc.blank(), 0);
8911                insta::assert_json_snapshot!(metric.loc);
8912            },
8913        );
8914    }
8915
8916    #[cfg(feature = "kotlin")]
8917    #[test]
8918    fn kotlin_loc_while_lloc() {
8919        check_metrics::<KotlinParser>(
8920            "fun countdown(n: Int) {
8921            var i = n
8922            while (i > 0) {
8923                println(i)
8924                i--
8925            }
8926        }",
8927            "foo.kt",
8928            |metric| {
8929                assert_eq!(metric.loc.sloc(), 7);
8930                assert_eq!(metric.loc.ploc(), 7);
8931                assert_eq!(metric.loc.lloc(), 3);
8932                assert_eq!(metric.loc.cloc(), 0);
8933                assert_eq!(metric.loc.blank(), 0);
8934                insta::assert_json_snapshot!(metric.loc);
8935            },
8936        );
8937    }
8938
8939    #[cfg(feature = "typescript")]
8940    #[test]
8941    fn typescript_blank() {
8942        check_metrics::<TypescriptParser>(
8943            "function f(): void {
8944
8945            const x = 1;
8946
8947        }",
8948            "foo.ts",
8949            |metric| {
8950                assert_eq!(metric.loc.sloc(), 5);
8951                assert_eq!(metric.loc.ploc(), 3);
8952                assert_eq!(metric.loc.lloc(), 1);
8953                assert_eq!(metric.loc.cloc(), 0);
8954                assert_eq!(metric.loc.blank(), 2);
8955                insta::assert_json_snapshot!(metric.loc);
8956            },
8957        );
8958    }
8959
8960    #[cfg(feature = "typescript")]
8961    #[test]
8962    fn typescript_cloc() {
8963        check_metrics::<TypescriptParser>(
8964            "// header
8965        /* block
8966           comment */
8967        function f(): number {
8968            return 42; // inline
8969        }",
8970            "foo.ts",
8971            |metric| {
8972                assert_eq!(metric.loc.sloc(), 6);
8973                assert_eq!(metric.loc.ploc(), 3);
8974                assert_eq!(metric.loc.lloc(), 1);
8975                assert_eq!(metric.loc.cloc(), 4);
8976                assert_eq!(metric.loc.blank(), 0);
8977                insta::assert_json_snapshot!(metric.loc);
8978            },
8979        );
8980    }
8981
8982    #[cfg(feature = "typescript")]
8983    #[test]
8984    fn typescript_no_zero_blank() {
8985        // Blank line interleaved with code that carries trailing comments —
8986        // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
8987        check_metrics::<TypescriptParser>(
8988            "function f(): void {
8989  const a = 1;
8990
8991  const b = 2; // trailing
8992  const c = 3; // trailing
8993}",
8994            "foo.ts",
8995            |metric| {
8996                assert_eq!(metric.loc.sloc(), 6);
8997                assert_eq!(metric.loc.ploc(), 5);
8998                assert_eq!(metric.loc.cloc(), 2);
8999                assert_eq!(metric.loc.blank(), 1);
9000                insta::assert_json_snapshot!(metric.loc);
9001            },
9002        );
9003    }
9004
9005    #[cfg(feature = "typescript")]
9006    #[test]
9007    fn typescript_if_lloc() {
9008        check_metrics::<TypescriptParser>(
9009            "function classify(n: number): string {
9010            if (n > 0) {
9011                return 'positive';
9012            } else {
9013                return 'non-positive';
9014            }
9015        }",
9016            "foo.ts",
9017            |metric| {
9018                assert_eq!(metric.loc.sloc(), 7);
9019                assert_eq!(metric.loc.ploc(), 7);
9020                assert_eq!(metric.loc.lloc(), 3);
9021                assert_eq!(metric.loc.cloc(), 0);
9022                assert_eq!(metric.loc.blank(), 0);
9023                insta::assert_json_snapshot!(metric.loc);
9024            },
9025        );
9026    }
9027
9028    #[cfg(feature = "typescript")]
9029    #[test]
9030    fn typescript_for_lloc() {
9031        check_metrics::<TypescriptParser>(
9032            "function sum(n: number): number {
9033            let s = 0;
9034            for (let i = 0; i < n; i++) {
9035                s += i;
9036            }
9037            return s;
9038        }",
9039            "foo.ts",
9040            |metric| {
9041                assert_eq!(metric.loc.sloc(), 7);
9042                assert_eq!(metric.loc.ploc(), 7);
9043                assert_eq!(metric.loc.lloc(), 4);
9044                assert_eq!(metric.loc.cloc(), 0);
9045                assert_eq!(metric.loc.blank(), 0);
9046                insta::assert_json_snapshot!(metric.loc);
9047            },
9048        );
9049    }
9050
9051    #[cfg(feature = "typescript")]
9052    #[test]
9053    fn typescript_while_lloc() {
9054        check_metrics::<TypescriptParser>(
9055            "function countdown(n: number): void {
9056            let i = n;
9057            while (i > 0) {
9058                console.log(i);
9059                i--;
9060            }
9061        }",
9062            "foo.ts",
9063            |metric| {
9064                assert_eq!(metric.loc.sloc(), 7);
9065                assert_eq!(metric.loc.ploc(), 7);
9066                assert_eq!(metric.loc.lloc(), 4);
9067                assert_eq!(metric.loc.cloc(), 0);
9068                assert_eq!(metric.loc.blank(), 0);
9069                insta::assert_json_snapshot!(metric.loc);
9070            },
9071        );
9072    }
9073
9074    #[cfg(feature = "typescript")]
9075    #[test]
9076    fn typescript_switch_lloc() {
9077        check_metrics::<TypescriptParser>(
9078            "function describe(x: number): string {
9079            switch (x) {
9080                case 1: return 'one';
9081                case 2: return 'two';
9082                default: return 'other';
9083            }
9084        }",
9085            "foo.ts",
9086            |metric| {
9087                assert_eq!(metric.loc.sloc(), 7);
9088                assert_eq!(metric.loc.ploc(), 7);
9089                assert_eq!(metric.loc.lloc(), 4);
9090                assert_eq!(metric.loc.cloc(), 0);
9091                assert_eq!(metric.loc.blank(), 0);
9092                insta::assert_json_snapshot!(metric.loc);
9093            },
9094        );
9095    }
9096
9097    #[cfg(feature = "typescript")]
9098    #[test]
9099    fn typescript_class_loc() {
9100        check_metrics::<TypescriptParser>(
9101            "class Counter {
9102            private count: number = 0;
9103            increment(): void { this.count++; }
9104            get(): number { return this.count; }
9105        }",
9106            "foo.ts",
9107            |metric| {
9108                assert_eq!(metric.loc.sloc(), 5);
9109                assert_eq!(metric.loc.ploc(), 5);
9110                assert_eq!(metric.loc.lloc(), 2);
9111                assert_eq!(metric.loc.cloc(), 0);
9112                assert_eq!(metric.loc.blank(), 0);
9113                insta::assert_json_snapshot!(metric.loc);
9114            },
9115        );
9116    }
9117
9118    #[cfg(feature = "typescript")]
9119    #[test]
9120    fn typescript_arrow_function_loc() {
9121        check_metrics::<TypescriptParser>(
9122            "const add = (a: number, b: number): number => a + b;
9123        const greet = (name: string): string => {
9124            return `Hello, ${name}`;
9125        };",
9126            "foo.ts",
9127            |metric| {
9128                assert_eq!(metric.loc.sloc(), 4);
9129                assert_eq!(metric.loc.ploc(), 4);
9130                assert_eq!(metric.loc.lloc(), 3);
9131                assert_eq!(metric.loc.cloc(), 0);
9132                assert_eq!(metric.loc.blank(), 0);
9133                insta::assert_json_snapshot!(metric.loc);
9134            },
9135        );
9136    }
9137
9138    #[cfg(feature = "typescript")]
9139    #[test]
9140    fn typescript_interface_loc() {
9141        check_metrics::<TypescriptParser>(
9142            "interface Shape {
9143            area(): number;
9144            perimeter(): number;
9145        }
9146        function describe(s: Shape): string {
9147            return `area=${s.area()}`;
9148        }",
9149            "foo.ts",
9150            |metric| {
9151                assert_eq!(metric.loc.sloc(), 7);
9152                assert_eq!(metric.loc.ploc(), 7);
9153                assert_eq!(metric.loc.lloc(), 1);
9154                assert_eq!(metric.loc.cloc(), 0);
9155                assert_eq!(metric.loc.blank(), 0);
9156                insta::assert_json_snapshot!(metric.loc);
9157            },
9158        );
9159    }
9160
9161    #[cfg(feature = "typescript")]
9162    #[test]
9163    fn typescript_multiple_functions_loc() {
9164        check_metrics::<TypescriptParser>(
9165            "function f(): number {
9166            return 1;
9167        }
9168        function g(): number {
9169            return 2;
9170        }
9171        function h(): number {
9172            return 3;
9173        }",
9174            "foo.ts",
9175            |metric| {
9176                assert_eq!(metric.loc.sloc(), 9);
9177                assert_eq!(metric.loc.ploc(), 9);
9178                assert_eq!(metric.loc.lloc(), 3);
9179                assert_eq!(metric.loc.cloc(), 0);
9180                assert_eq!(metric.loc.blank(), 0);
9181                insta::assert_json_snapshot!(metric.loc);
9182            },
9183        );
9184    }
9185
9186    #[cfg(feature = "typescript")]
9187    #[test]
9188    fn typescript_try_catch_lloc() {
9189        check_metrics::<TypescriptParser>(
9190            "function safe(x: number): number {
9191            try {
9192                return 1 / x;
9193            } catch (e) {
9194                return 0;
9195            }
9196        }",
9197            "foo.ts",
9198            |metric| {
9199                assert_eq!(metric.loc.sloc(), 7);
9200                assert_eq!(metric.loc.ploc(), 7);
9201                assert_eq!(metric.loc.lloc(), 3);
9202                assert_eq!(metric.loc.cloc(), 0);
9203                assert_eq!(metric.loc.blank(), 0);
9204                insta::assert_json_snapshot!(metric.loc);
9205            },
9206        );
9207    }
9208
9209    #[cfg(feature = "typescript")]
9210    #[test]
9211    fn typescript_nested_functions_loc() {
9212        check_metrics::<TypescriptParser>(
9213            "function outer(x: number): number {
9214            function inner(y: number): number {
9215                return y * 2;
9216            }
9217            return inner(x) + 1;
9218        }",
9219            "foo.ts",
9220            |metric| {
9221                assert_eq!(metric.loc.sloc(), 6);
9222                assert_eq!(metric.loc.ploc(), 6);
9223                assert_eq!(metric.loc.lloc(), 2);
9224                assert_eq!(metric.loc.cloc(), 0);
9225                assert_eq!(metric.loc.blank(), 0);
9226                insta::assert_json_snapshot!(metric.loc);
9227            },
9228        );
9229    }
9230
9231    #[cfg(feature = "typescript")]
9232    #[test]
9233    fn typescript_generic_function_loc() {
9234        check_metrics::<TypescriptParser>(
9235            "function identity<T>(value: T): T {
9236            return value;
9237        }
9238        function first<T>(arr: T[]): T | undefined {
9239            return arr[0];
9240        }",
9241            "foo.ts",
9242            |metric| {
9243                assert_eq!(metric.loc.sloc(), 6);
9244                assert_eq!(metric.loc.ploc(), 6);
9245                assert_eq!(metric.loc.lloc(), 2);
9246                assert_eq!(metric.loc.cloc(), 0);
9247                assert_eq!(metric.loc.blank(), 0);
9248                insta::assert_json_snapshot!(metric.loc);
9249            },
9250        );
9251    }
9252
9253    #[cfg(feature = "typescript")]
9254    #[test]
9255    fn tsx_blank() {
9256        check_metrics::<TsxParser>(
9257            "function f(): void {
9258
9259            const x = 1;
9260
9261        }",
9262            "foo.tsx",
9263            |metric| {
9264                assert_eq!(metric.loc.sloc(), 5);
9265                assert_eq!(metric.loc.ploc(), 3);
9266                assert_eq!(metric.loc.lloc(), 1);
9267                assert_eq!(metric.loc.cloc(), 0);
9268                assert_eq!(metric.loc.blank(), 2);
9269                insta::assert_json_snapshot!(metric.loc);
9270            },
9271        );
9272    }
9273
9274    #[cfg(feature = "typescript")]
9275    #[test]
9276    fn tsx_cloc() {
9277        check_metrics::<TsxParser>(
9278            "// header
9279        /* block
9280           comment */
9281        function f(): number {
9282            return 42; // inline
9283        }",
9284            "foo.tsx",
9285            |metric| {
9286                assert_eq!(metric.loc.sloc(), 6);
9287                assert_eq!(metric.loc.ploc(), 3);
9288                assert_eq!(metric.loc.lloc(), 1);
9289                assert_eq!(metric.loc.cloc(), 4);
9290                assert_eq!(metric.loc.blank(), 0);
9291                insta::assert_json_snapshot!(metric.loc);
9292            },
9293        );
9294    }
9295
9296    #[cfg(feature = "typescript")]
9297    #[test]
9298    fn tsx_no_zero_blank() {
9299        // Blank line interleaved with code that carries trailing comments —
9300        // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
9301        check_metrics::<TsxParser>(
9302            "function f(): void {
9303  const a = 1;
9304
9305  const b = 2; // trailing
9306  const c = 3; // trailing
9307}",
9308            "foo.tsx",
9309            |metric| {
9310                assert_eq!(metric.loc.sloc(), 6);
9311                assert_eq!(metric.loc.ploc(), 5);
9312                assert_eq!(metric.loc.cloc(), 2);
9313                assert_eq!(metric.loc.blank(), 1);
9314                insta::assert_json_snapshot!(metric.loc);
9315            },
9316        );
9317    }
9318
9319    #[cfg(feature = "typescript")]
9320    #[test]
9321    fn tsx_if_lloc() {
9322        check_metrics::<TsxParser>(
9323            "function classify(n: number): string {
9324            if (n > 0) {
9325                return 'positive';
9326            } else {
9327                return 'non-positive';
9328            }
9329        }",
9330            "foo.tsx",
9331            |metric| {
9332                assert_eq!(metric.loc.sloc(), 7);
9333                assert_eq!(metric.loc.ploc(), 7);
9334                assert_eq!(metric.loc.lloc(), 3);
9335                assert_eq!(metric.loc.cloc(), 0);
9336                assert_eq!(metric.loc.blank(), 0);
9337                insta::assert_json_snapshot!(metric.loc);
9338            },
9339        );
9340    }
9341
9342    #[cfg(feature = "typescript")]
9343    #[test]
9344    fn tsx_for_lloc() {
9345        check_metrics::<TsxParser>(
9346            "function sum(n: number): number {
9347            let s = 0;
9348            for (let i = 0; i < n; i++) {
9349                s += i;
9350            }
9351            return s;
9352        }",
9353            "foo.tsx",
9354            |metric| {
9355                assert_eq!(metric.loc.sloc(), 7);
9356                assert_eq!(metric.loc.ploc(), 7);
9357                assert_eq!(metric.loc.lloc(), 4);
9358                assert_eq!(metric.loc.cloc(), 0);
9359                assert_eq!(metric.loc.blank(), 0);
9360                insta::assert_json_snapshot!(metric.loc);
9361            },
9362        );
9363    }
9364
9365    #[cfg(feature = "typescript")]
9366    #[test]
9367    fn tsx_while_lloc() {
9368        check_metrics::<TsxParser>(
9369            "function countdown(n: number): void {
9370            let i = n;
9371            while (i > 0) {
9372                console.log(i);
9373                i--;
9374            }
9375        }",
9376            "foo.tsx",
9377            |metric| {
9378                assert_eq!(metric.loc.sloc(), 7);
9379                assert_eq!(metric.loc.ploc(), 7);
9380                assert_eq!(metric.loc.lloc(), 4);
9381                assert_eq!(metric.loc.cloc(), 0);
9382                assert_eq!(metric.loc.blank(), 0);
9383                insta::assert_json_snapshot!(metric.loc);
9384            },
9385        );
9386    }
9387
9388    #[cfg(feature = "typescript")]
9389    #[test]
9390    fn tsx_switch_lloc() {
9391        check_metrics::<TsxParser>(
9392            "function describe(x: number): string {
9393            switch (x) {
9394                case 1: return 'one';
9395                case 2: return 'two';
9396                default: return 'other';
9397            }
9398        }",
9399            "foo.tsx",
9400            |metric| {
9401                assert_eq!(metric.loc.sloc(), 7);
9402                assert_eq!(metric.loc.ploc(), 7);
9403                assert_eq!(metric.loc.lloc(), 4);
9404                assert_eq!(metric.loc.cloc(), 0);
9405                assert_eq!(metric.loc.blank(), 0);
9406                insta::assert_json_snapshot!(metric.loc);
9407            },
9408        );
9409    }
9410
9411    #[cfg(feature = "typescript")]
9412    #[test]
9413    fn tsx_class_loc() {
9414        check_metrics::<TsxParser>(
9415            "class Counter {
9416            private count: number = 0;
9417            increment(): void { this.count++; }
9418            get(): number { return this.count; }
9419        }",
9420            "foo.tsx",
9421            |metric| {
9422                assert_eq!(metric.loc.sloc(), 5);
9423                assert_eq!(metric.loc.ploc(), 5);
9424                assert_eq!(metric.loc.lloc(), 2);
9425                assert_eq!(metric.loc.cloc(), 0);
9426                assert_eq!(metric.loc.blank(), 0);
9427                insta::assert_json_snapshot!(metric.loc);
9428            },
9429        );
9430    }
9431
9432    #[cfg(feature = "typescript")]
9433    #[test]
9434    fn tsx_arrow_function_loc() {
9435        check_metrics::<TsxParser>(
9436            "const add = (a: number, b: number): number => a + b;
9437        const greet = (name: string): string => {
9438            return `Hello, ${name}`;
9439        };",
9440            "foo.tsx",
9441            |metric| {
9442                assert_eq!(metric.loc.sloc(), 4);
9443                assert_eq!(metric.loc.ploc(), 4);
9444                assert_eq!(metric.loc.lloc(), 3);
9445                assert_eq!(metric.loc.cloc(), 0);
9446                assert_eq!(metric.loc.blank(), 0);
9447                insta::assert_json_snapshot!(metric.loc);
9448            },
9449        );
9450    }
9451
9452    #[cfg(feature = "typescript")]
9453    #[test]
9454    fn tsx_multiple_functions_loc() {
9455        check_metrics::<TsxParser>(
9456            "function f(): number {
9457            return 1;
9458        }
9459        function g(): number {
9460            return 2;
9461        }
9462        function h(): number {
9463            return 3;
9464        }",
9465            "foo.tsx",
9466            |metric| {
9467                assert_eq!(metric.loc.sloc(), 9);
9468                assert_eq!(metric.loc.ploc(), 9);
9469                assert_eq!(metric.loc.lloc(), 3);
9470                assert_eq!(metric.loc.cloc(), 0);
9471                assert_eq!(metric.loc.blank(), 0);
9472                insta::assert_json_snapshot!(metric.loc);
9473            },
9474        );
9475    }
9476
9477    #[cfg(feature = "typescript")]
9478    #[test]
9479    fn tsx_try_catch_lloc() {
9480        check_metrics::<TsxParser>(
9481            "function safe(x: number): number {
9482            try {
9483                return 1 / x;
9484            } catch (e) {
9485                return 0;
9486            }
9487        }",
9488            "foo.tsx",
9489            |metric| {
9490                assert_eq!(metric.loc.sloc(), 7);
9491                assert_eq!(metric.loc.ploc(), 7);
9492                assert_eq!(metric.loc.lloc(), 3);
9493                assert_eq!(metric.loc.cloc(), 0);
9494                assert_eq!(metric.loc.blank(), 0);
9495                insta::assert_json_snapshot!(metric.loc);
9496            },
9497        );
9498    }
9499
9500    #[cfg(feature = "typescript")]
9501    #[test]
9502    fn tsx_nested_functions_loc() {
9503        check_metrics::<TsxParser>(
9504            "function outer(x: number): number {
9505            function inner(y: number): number {
9506                return y * 2;
9507            }
9508            return inner(x) + 1;
9509        }",
9510            "foo.tsx",
9511            |metric| {
9512                assert_eq!(metric.loc.sloc(), 6);
9513                assert_eq!(metric.loc.ploc(), 6);
9514                assert_eq!(metric.loc.lloc(), 2);
9515                assert_eq!(metric.loc.cloc(), 0);
9516                assert_eq!(metric.loc.blank(), 0);
9517                insta::assert_json_snapshot!(metric.loc);
9518            },
9519        );
9520    }
9521
9522    #[cfg(feature = "typescript")]
9523    #[test]
9524    fn tsx_interface_loc() {
9525        check_metrics::<TsxParser>(
9526            "interface Shape {
9527            area(): number;
9528            perimeter(): number;
9529        }
9530        function describe(s: Shape): string {
9531            return `area=${s.area()}`;
9532        }",
9533            "foo.tsx",
9534            |metric| {
9535                assert_eq!(metric.loc.sloc(), 7);
9536                assert_eq!(metric.loc.ploc(), 7);
9537                assert_eq!(metric.loc.lloc(), 1);
9538                assert_eq!(metric.loc.cloc(), 0);
9539                assert_eq!(metric.loc.blank(), 0);
9540                insta::assert_json_snapshot!(metric.loc);
9541            },
9542        );
9543    }
9544
9545    #[cfg(feature = "typescript")]
9546    #[test]
9547    fn tsx_generic_function_loc() {
9548        check_metrics::<TsxParser>(
9549            "function identity<T>(value: T): T {
9550            return value;
9551        }
9552        function first<T>(arr: T[]): T | undefined {
9553            return arr[0];
9554        }",
9555            "foo.tsx",
9556            |metric| {
9557                assert_eq!(metric.loc.sloc(), 6);
9558                assert_eq!(metric.loc.ploc(), 6);
9559                assert_eq!(metric.loc.lloc(), 2);
9560                assert_eq!(metric.loc.cloc(), 0);
9561                assert_eq!(metric.loc.blank(), 0);
9562                insta::assert_json_snapshot!(metric.loc);
9563            },
9564        );
9565    }
9566
9567    #[cfg(feature = "php")]
9568    #[test]
9569    fn php_blank() {
9570        check_metrics::<PhpParser>(
9571            "<?php
9572
9573$a = 1;
9574
9575$b = 2;
9576
9577",
9578            "foo.php",
9579            |metric| {
9580                assert_eq!(metric.loc.sloc(), 5);
9581                assert_eq!(metric.loc.ploc(), 3);
9582                assert_eq!(metric.loc.lloc(), 2);
9583                assert_eq!(metric.loc.cloc(), 0);
9584                assert_eq!(metric.loc.blank(), 2);
9585                insta::assert_json_snapshot!(metric.loc);
9586            },
9587        );
9588    }
9589
9590    #[cfg(feature = "php")]
9591    #[test]
9592    fn php_no_zero_blank() {
9593        // Blank line interleaved with code that carries trailing comments —
9594        // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
9595        check_metrics::<PhpParser>(
9596            "<?php
9597$a = 1;
9598
9599$b = 2; // trailing
9600$c = 3; // trailing
9601",
9602            "foo.php",
9603            |metric| {
9604                assert_eq!(metric.loc.sloc(), 5);
9605                assert_eq!(metric.loc.ploc(), 4);
9606                assert_eq!(metric.loc.cloc(), 2);
9607                assert_eq!(metric.loc.blank(), 1);
9608                insta::assert_json_snapshot!(metric.loc);
9609            },
9610        );
9611    }
9612
9613    #[cfg(feature = "php")]
9614    #[test]
9615    fn php_cloc_double_slash() {
9616        check_metrics::<PhpParser>(
9617            "<?php
9618// first
9619// second
9620$a = 1; // trailing",
9621            "foo.php",
9622            |metric| {
9623                assert_eq!(metric.loc.sloc(), 4);
9624                assert_eq!(metric.loc.ploc(), 2);
9625                assert_eq!(metric.loc.lloc(), 1);
9626                assert_eq!(metric.loc.cloc(), 3);
9627                assert_eq!(metric.loc.blank(), 0);
9628                insta::assert_json_snapshot!(metric.loc);
9629            },
9630        );
9631    }
9632
9633    #[cfg(feature = "php")]
9634    #[test]
9635    fn php_cloc_hash() {
9636        check_metrics::<PhpParser>(
9637            "<?php
9638# first
9639# second
9640$a = 1;",
9641            "foo.php",
9642            |metric| {
9643                assert_eq!(metric.loc.sloc(), 4);
9644                assert_eq!(metric.loc.ploc(), 2);
9645                assert_eq!(metric.loc.lloc(), 1);
9646                assert_eq!(metric.loc.cloc(), 2);
9647                assert_eq!(metric.loc.blank(), 0);
9648                insta::assert_json_snapshot!(metric.loc);
9649            },
9650        );
9651    }
9652
9653    #[cfg(feature = "php")]
9654    #[test]
9655    fn php_cloc_block() {
9656        check_metrics::<PhpParser>(
9657            "<?php
9658/*
9659 * block
9660 * comment
9661 */
9662$a = 1;",
9663            "foo.php",
9664            |metric| {
9665                assert_eq!(metric.loc.sloc(), 6);
9666                assert_eq!(metric.loc.ploc(), 2);
9667                assert_eq!(metric.loc.lloc(), 1);
9668                assert_eq!(metric.loc.cloc(), 4);
9669                assert_eq!(metric.loc.blank(), 0);
9670                insta::assert_json_snapshot!(metric.loc);
9671            },
9672        );
9673    }
9674
9675    #[cfg(feature = "php")]
9676    #[test]
9677    fn php_lloc() {
9678        // Three statements: assignment, if (with body), echo.
9679        check_metrics::<PhpParser>(
9680            "<?php
9681$a = 1;
9682if ($a > 0) {
9683    echo $a;
9684}",
9685            "foo.php",
9686            |metric| {
9687                assert_eq!(metric.loc.sloc(), 5);
9688                assert_eq!(metric.loc.ploc(), 5);
9689                assert_eq!(metric.loc.lloc(), 3);
9690                assert_eq!(metric.loc.cloc(), 0);
9691                assert_eq!(metric.loc.blank(), 0);
9692                insta::assert_json_snapshot!(metric.loc);
9693            },
9694        );
9695    }
9696
9697    #[cfg(feature = "php")]
9698    #[test]
9699    fn php_no_parenthesized_expression_lloc() {
9700        // Parenthesized expression should not add an extra LLOC over the
9701        // surrounding expression_statement.
9702        check_metrics::<PhpParser>(
9703            "<?php
9704$a = (1 + 2);",
9705            "foo.php",
9706            |metric| {
9707                assert_eq!(metric.loc.sloc(), 2);
9708                assert_eq!(metric.loc.ploc(), 2);
9709                assert_eq!(metric.loc.lloc(), 1);
9710                assert_eq!(metric.loc.cloc(), 0);
9711                assert_eq!(metric.loc.blank(), 0);
9712                insta::assert_json_snapshot!(metric.loc);
9713            },
9714        );
9715    }
9716
9717    #[cfg(feature = "php")]
9718    #[test]
9719    fn php_no_compound_statement_lloc() {
9720        // Block wrappers (`{ … }`) are not LLOC themselves.
9721        check_metrics::<PhpParser>(
9722            "<?php
9723function f(): void {
9724    $a = 1;
9725}",
9726            "foo.php",
9727            |metric| {
9728                assert_eq!(metric.loc.sloc(), 4);
9729                assert_eq!(metric.loc.ploc(), 4);
9730                assert_eq!(metric.loc.lloc(), 1);
9731                assert_eq!(metric.loc.cloc(), 0);
9732                assert_eq!(metric.loc.blank(), 0);
9733                insta::assert_json_snapshot!(metric.loc);
9734            },
9735        );
9736    }
9737
9738    #[cfg(feature = "php")]
9739    #[test]
9740    fn php_no_colon_block_lloc() {
9741        // Alternative syntax (`if: … endif;`) uses ColonBlock instead of
9742        // CompoundStatement; it is also not LLOC.
9743        check_metrics::<PhpParser>(
9744            "<?php
9745if (true):
9746    $a = 1;
9747endif;",
9748            "foo.php",
9749            |metric| {
9750                assert_eq!(metric.loc.sloc(), 4);
9751                assert_eq!(metric.loc.ploc(), 4);
9752                assert_eq!(metric.loc.lloc(), 2);
9753                assert_eq!(metric.loc.cloc(), 0);
9754                assert_eq!(metric.loc.blank(), 0);
9755                insta::assert_json_snapshot!(metric.loc);
9756            },
9757        );
9758    }
9759
9760    #[cfg(feature = "php")]
9761    #[test]
9762    fn php_no_else_clause_lloc() {
9763        // ElseClause and ElseIfClause are sub-parts of IfStatement.
9764        check_metrics::<PhpParser>(
9765            "<?php
9766if ($x) {
9767    $a = 1;
9768} elseif ($y) {
9769    $a = 2;
9770} else {
9771    $a = 3;
9772}",
9773            "foo.php",
9774            |metric| {
9775                assert_eq!(metric.loc.sloc(), 8);
9776                assert_eq!(metric.loc.ploc(), 8);
9777                assert_eq!(metric.loc.lloc(), 4);
9778                assert_eq!(metric.loc.cloc(), 0);
9779                assert_eq!(metric.loc.blank(), 0);
9780                insta::assert_json_snapshot!(metric.loc);
9781            },
9782        );
9783    }
9784
9785    #[cfg(feature = "php")]
9786    #[test]
9787    fn php_no_case_statement_lloc() {
9788        // CaseStatement / DefaultStatement are switch arms, not separate
9789        // statements.
9790        check_metrics::<PhpParser>(
9791            "<?php
9792switch ($x) {
9793    case 1:
9794        $a = 1;
9795        break;
9796    case 2:
9797        $a = 2;
9798        break;
9799    default:
9800        $a = 0;
9801}",
9802            "foo.php",
9803            |metric| {
9804                assert_eq!(metric.loc.sloc(), 11);
9805                assert_eq!(metric.loc.ploc(), 11);
9806                assert_eq!(metric.loc.lloc(), 6);
9807                assert_eq!(metric.loc.cloc(), 0);
9808                assert_eq!(metric.loc.blank(), 0);
9809                insta::assert_json_snapshot!(metric.loc);
9810            },
9811        );
9812    }
9813
9814    #[cfg(feature = "php")]
9815    #[test]
9816    fn php_no_match_arm_lloc() {
9817        // MatchConditionalExpression / MatchDefaultExpression are arms;
9818        // only the surrounding expression_statement counts.
9819        check_metrics::<PhpParser>(
9820            "<?php
9821$a = match ($x) {
9822    1 => 'one',
9823    2 => 'two',
9824    default => 'other',
9825};",
9826            "foo.php",
9827            |metric| {
9828                assert_eq!(metric.loc.sloc(), 6);
9829                assert_eq!(metric.loc.ploc(), 6);
9830                assert_eq!(metric.loc.lloc(), 1);
9831                assert_eq!(metric.loc.cloc(), 0);
9832                assert_eq!(metric.loc.blank(), 0);
9833                insta::assert_json_snapshot!(metric.loc);
9834            },
9835        );
9836    }
9837
9838    #[cfg(feature = "php")]
9839    #[test]
9840    fn php_no_throw_in_expression_lloc() {
9841        // PHP 8 `throw` as expression: only the surrounding statement
9842        // counts (the `??` in this example), not the throw_expression.
9843        check_metrics::<PhpParser>(
9844            "<?php
9845$x = $y ?? throw new \\Exception('nope');",
9846            "foo.php",
9847            |metric| {
9848                assert_eq!(metric.loc.sloc(), 2);
9849                assert_eq!(metric.loc.ploc(), 2);
9850                assert_eq!(metric.loc.lloc(), 1);
9851                assert_eq!(metric.loc.cloc(), 0);
9852                assert_eq!(metric.loc.blank(), 0);
9853                insta::assert_json_snapshot!(metric.loc);
9854            },
9855        );
9856    }
9857
9858    #[cfg(feature = "php")]
9859    #[test]
9860    fn php_no_closure_in_assignment_lloc() {
9861        // Anonymous function as RHS does not add an LLOC; only the
9862        // expression_statement counts. The closure body's statements are
9863        // counted in its own FuncSpace.
9864        check_metrics::<PhpParser>(
9865            "<?php
9866$f = function (): int {
9867    return 42;
9868};",
9869            "foo.php",
9870            |metric| {
9871                assert_eq!(metric.loc.sloc(), 4);
9872                assert_eq!(metric.loc.ploc(), 4);
9873                assert_eq!(metric.loc.lloc(), 2);
9874                assert_eq!(metric.loc.cloc(), 0);
9875                assert_eq!(metric.loc.blank(), 0);
9876                insta::assert_json_snapshot!(metric.loc);
9877            },
9878        );
9879    }
9880
9881    #[cfg(feature = "php")]
9882    #[test]
9883    fn php_for_lloc() {
9884        // The for_statement contributes 1 LLOC; init/cond/update are NOT
9885        // separate statements in PHP's grammar.
9886        check_metrics::<PhpParser>(
9887            "<?php
9888for ($i = 0; $i < 10; $i++) {
9889    echo $i;
9890}",
9891            "foo.php",
9892            |metric| {
9893                assert_eq!(metric.loc.sloc(), 4);
9894                assert_eq!(metric.loc.ploc(), 4);
9895                assert_eq!(metric.loc.lloc(), 2);
9896                assert_eq!(metric.loc.cloc(), 0);
9897                assert_eq!(metric.loc.blank(), 0);
9898                insta::assert_json_snapshot!(metric.loc);
9899            },
9900        );
9901    }
9902
9903    #[cfg(feature = "php")]
9904    #[test]
9905    fn php_foreach_lloc() {
9906        check_metrics::<PhpParser>(
9907            "<?php
9908foreach ($items as $k => $v) {
9909    echo $v;
9910}",
9911            "foo.php",
9912            |metric| {
9913                assert_eq!(metric.loc.sloc(), 4);
9914                assert_eq!(metric.loc.ploc(), 4);
9915                assert_eq!(metric.loc.lloc(), 2);
9916                assert_eq!(metric.loc.cloc(), 0);
9917                assert_eq!(metric.loc.blank(), 0);
9918                insta::assert_json_snapshot!(metric.loc);
9919            },
9920        );
9921    }
9922
9923    #[cfg(feature = "php")]
9924    #[test]
9925    fn php_try_lloc() {
9926        check_metrics::<PhpParser>(
9927            "<?php
9928try {
9929    $a = 1;
9930} catch (\\Exception $e) {
9931    $a = 0;
9932} finally {
9933    $b = 2;
9934}",
9935            "foo.php",
9936            |metric| {
9937                assert_eq!(metric.loc.sloc(), 8);
9938                assert_eq!(metric.loc.ploc(), 8);
9939                assert_eq!(metric.loc.lloc(), 4);
9940                assert_eq!(metric.loc.cloc(), 0);
9941                assert_eq!(metric.loc.blank(), 0);
9942                insta::assert_json_snapshot!(metric.loc);
9943            },
9944        );
9945    }
9946
9947    #[cfg(feature = "php")]
9948    #[test]
9949    fn php_class_loc() {
9950        check_metrics::<PhpParser>(
9951            "<?php
9952class A {
9953    public int $x = 0;
9954    private const Y = 1;
9955    public function f(): int {
9956        return $this->x;
9957    }
9958}",
9959            "foo.php",
9960            |metric| {
9961                assert_eq!(metric.loc.sloc(), 8);
9962                assert_eq!(metric.loc.ploc(), 8);
9963                assert_eq!(metric.loc.lloc(), 3);
9964                assert_eq!(metric.loc.cloc(), 0);
9965                assert_eq!(metric.loc.blank(), 0);
9966                insta::assert_json_snapshot!(metric.loc);
9967            },
9968        );
9969    }
9970
9971    #[cfg(feature = "php")]
9972    #[test]
9973    fn php_namespace_use_lloc() {
9974        check_metrics::<PhpParser>(
9975            "<?php
9976namespace App;
9977use App\\Foo;
9978use App\\Bar;
9979$a = 1;",
9980            "foo.php",
9981            |metric| {
9982                assert_eq!(metric.loc.sloc(), 5);
9983                assert_eq!(metric.loc.ploc(), 5);
9984                assert_eq!(metric.loc.lloc(), 3);
9985                assert_eq!(metric.loc.cloc(), 0);
9986                assert_eq!(metric.loc.blank(), 0);
9987                insta::assert_json_snapshot!(metric.loc);
9988            },
9989        );
9990    }
9991
9992    #[cfg(feature = "php")]
9993    #[test]
9994    fn php_general_loc() {
9995        check_metrics::<PhpParser>(
9996            "<?php
9997// header
9998namespace App;
9999use App\\Foo;
10000
10001class Bar {
10002    public int $n = 0;
10003
10004    public function add(int $x): int {
10005        if ($x > 0) {
10006            return $this->n + $x;
10007        }
10008        return $this->n;
10009    }
10010}",
10011            "foo.php",
10012            |metric| {
10013                assert_eq!(metric.loc.sloc(), 15);
10014                assert_eq!(metric.loc.ploc(), 12);
10015                assert_eq!(metric.loc.lloc(), 5);
10016                assert_eq!(metric.loc.cloc(), 1);
10017                assert_eq!(metric.loc.blank(), 2);
10018                insta::assert_json_snapshot!(metric.loc);
10019            },
10020        );
10021    }
10022
10023    #[cfg(feature = "php")]
10024    #[test]
10025    fn php_match_in_expression_lloc() {
10026        // Match inside another expression (e.g. assignment RHS) — the
10027        // outer expression_statement counts, the inner match arms do not.
10028        check_metrics::<PhpParser>(
10029            "<?php
10030$y = 10 + match ($x) { 1 => 2, default => 0 };",
10031            "foo.php",
10032            |metric| {
10033                assert_eq!(metric.loc.sloc(), 2);
10034                assert_eq!(metric.loc.ploc(), 2);
10035                assert_eq!(metric.loc.lloc(), 1);
10036                assert_eq!(metric.loc.cloc(), 0);
10037                assert_eq!(metric.loc.blank(), 0);
10038                insta::assert_json_snapshot!(metric.loc);
10039            },
10040        );
10041    }
10042
10043    #[cfg(feature = "php")]
10044    #[test]
10045    fn php_html_island_ploc() {
10046        // Embedded HTML between PHP tags ("text interpolation"). HTML
10047        // rows must contribute to PLOC (they are not blank and not a
10048        // PHP comment); this test locks that behavior so a future
10049        // grammar bump or impl tweak that excludes `text` nodes from
10050        // the default PLOC branch is caught.
10051        check_metrics::<PhpParser>(
10052            "<?php if ($cond): ?>
10053<div>hello</div>
10054<p>world</p>
10055<?php endif; ?>",
10056            "foo.php",
10057            |metric| {
10058                assert_eq!(metric.loc.sloc(), 4);
10059                assert_eq!(metric.loc.ploc(), 3);
10060                assert_eq!(metric.loc.lloc(), 1);
10061                assert_eq!(metric.loc.cloc(), 0);
10062                assert_eq!(metric.loc.blank(), 1);
10063                insta::assert_json_snapshot!(metric.loc);
10064            },
10065        );
10066    }
10067
10068    #[cfg(feature = "php")]
10069    #[test]
10070    fn php_short_echo_tag_ploc() {
10071        // `<?=` is the same `php_tag` kind as `<?php` per
10072        // tree-sitter-php 0.25.1. A regression that re-classified `<?=`
10073        // would shift PLOC; this test pins the current behavior.
10074        check_metrics::<PhpParser>("<p><?= $name ?></p>", "foo.php", |metric| {
10075            assert_eq!(metric.loc.sloc(), 1);
10076            assert_eq!(metric.loc.ploc(), 1);
10077            assert_eq!(metric.loc.lloc(), 1);
10078            assert_eq!(metric.loc.cloc(), 0);
10079            assert_eq!(metric.loc.blank(), 0);
10080            insta::assert_json_snapshot!(metric.loc);
10081        });
10082    }
10083
10084    /// Analyses `source` byte-for-byte as PHP, for the #1396 tests
10085    /// `check_metrics` cannot carry — one ends at EOF, which that shim
10086    /// normalises away, and one loops over labelled cases, which its bare
10087    /// `fn` callback cannot close over — and for the bounds test beside
10088    /// them.
10089    #[cfg(feature = "php")]
10090    fn php_loc(source: &[u8]) -> Stats {
10091        metrics_verbatim(
10092            crate::LANG::Php,
10093            source,
10094            crate::MetricsOptions::default().with_only(&[crate::Metric::Loc]),
10095        )
10096        .loc
10097    }
10098
10099    /// #1396: a heredoc row that is empty *inside* the literal read as
10100    /// blank. tree-sitter-php emits one `string_content` child per body
10101    /// row that **has text**, so a row empty inside the literal held no
10102    /// node at all and `blank = sloc - ploc - cloc` claimed it. #778
10103    /// routed PHP's quoted literals through `add_multiline_string_ploc`
10104    /// but excluded the heredoc, on the premise that its inner nodes
10105    /// already covered every row.
10106    ///
10107    /// `sloc` is the fixture anchor (`.claude/rules/testing.md`):
10108    /// deleting the empty row drops it to 5 and fails this test, rather
10109    /// than leaving it quietly asserting `blank 0` about a literal with
10110    /// no interior gap left to misclassify.
10111    #[cfg(feature = "php")]
10112    #[test]
10113    fn php_heredoc_empty_interior_row_is_code_not_blank() {
10114        // expected: six rows, every one code — `<?php`, the opening
10115        // `<<<EOT`, `a`, the empty body row, `b`, and the closing marker.
10116        check_metrics::<PhpParser>("<?php\n$s = <<<EOT\na\n\nb\nEOT;\n", "foo.php", |metric| {
10117            assert_eq!(metric.loc.sloc(), 6);
10118            assert_eq!(metric.loc.ploc(), 6);
10119            assert_eq!(metric.loc.cloc(), 0);
10120            assert_eq!(metric.loc.lloc(), 1);
10121            assert_eq!(metric.loc.blank(), 0);
10122        });
10123    }
10124
10125    /// The nowdoc half of #1396, which the issue did not measure and
10126    /// which was worse: it reported `ploc 4, blank 2` on this fixture
10127    /// against the heredoc's `ploc 5, blank 1`. The grammar shape
10128    /// differs: tree-sitter-php 0.25.1 emits one `nowdoc_string` per body
10129    /// line, but each after the first starts at the *end of the row
10130    /// before it*, so the catch-all's start-row insertion credited every
10131    /// body row to its predecessor and the last one (`b`) to nothing, on
10132    /// top of the empty row.
10133    ///
10134    /// Same `sloc` fixture anchor as the heredoc test above.
10135    #[cfg(feature = "php")]
10136    #[test]
10137    fn php_nowdoc_empty_interior_row_is_code_not_blank() {
10138        // expected: the heredoc fixture's six rows, all code.
10139        check_metrics::<PhpParser>(
10140            "<?php\n$s = <<<'EOT'\na\n\nb\nEOT;\n",
10141            "foo.php",
10142            |metric| {
10143                assert_eq!(metric.loc.sloc(), 6);
10144                assert_eq!(metric.loc.ploc(), 6);
10145                assert_eq!(metric.loc.cloc(), 0);
10146                assert_eq!(metric.loc.lloc(), 1);
10147                assert_eq!(metric.loc.blank(), 0);
10148            },
10149        );
10150    }
10151
10152    /// The four #1396 spellings the two tests above do not cover, each
10153    /// exercising a different part of the new arm.
10154    ///
10155    /// The all-empty body is why the arm routes the `heredoc` / `nowdoc`
10156    /// wrapper rather than `heredoc_body` / `nowdoc_body`: for a body of
10157    /// one empty row the grammar emits **no body node at all**, so an
10158    /// arm keyed on the body would still report that row blank
10159    /// (`.claude/rules/grammar-dispatch.md` section 6). The interpolated
10160    /// form checks that an interior `{$v}` — whose own nodes cover only
10161    /// their row — does not stop the empty row above it being credited.
10162    /// The PHP 7.3 indented closing marker is syntax coverage rather
10163    /// than a distinct code path: `heredoc_end` is credited by the
10164    /// catch-all whatever its indentation, so nothing here can observe
10165    /// an end-row error. It earns its place by failing pre-fix on its
10166    /// interior empty row, like the rest.
10167    ///
10168    /// The backtick `shell_command_expression` is the fifth PHP literal
10169    /// that can span rows and had the nowdoc shape — one multi-row
10170    /// `string_content` — so it lost interior rows too.
10171    ///
10172    /// Every row of every fixture is code, so each case's `sloc` is the
10173    /// fixture anchor: trimming a row from any of them fails that row's
10174    /// case by name. `php_heredoc_credits_only_the_literals_rows` below
10175    /// carries the other half — that the arm credits no row the literal
10176    /// does not span.
10177    #[cfg(feature = "php")]
10178    #[test]
10179    fn php_heredoc_spellings_credit_every_row_to_ploc() {
10180        // (label, source, expected sloc) — expected ploc is that same
10181        // sloc and expected blank is 0 for all five, which is the
10182        // property under test.
10183        let cases = [
10184            ("all-empty heredoc body", "<?php\n$s = <<<EOT\n\nEOT;\n", 4),
10185            ("all-empty nowdoc body", "<?php\n$s = <<<'EOT'\n\nEOT;\n", 4),
10186            (
10187                "interpolated heredoc",
10188                "<?php\n$v = 1;\n$s = <<<EOT\na\n\n{$v}\nEOT;\n",
10189                7,
10190            ),
10191            (
10192                "indented closing marker",
10193                "<?php\n$s = <<<EOT\n    a\n\n    b\n    EOT;\n",
10194                6,
10195            ),
10196            ("backtick shell command", "<?php\n$s = `ls\n\n-l`;\n", 4),
10197        ];
10198        for (label, source, sloc) in cases {
10199            let loc = php_loc(source.as_bytes());
10200            assert_eq!(loc.sloc(), sloc, "{label} sloc");
10201            assert_eq!(loc.ploc(), sloc, "{label} ploc");
10202            assert_eq!(loc.cloc(), 0, "{label} cloc");
10203            assert_eq!(loc.blank(), 0, "{label} blank");
10204        }
10205    }
10206
10207    /// The upper bound the other #1396 tests cannot state. Every fixture
10208    /// above is 100% code, so `ploc == sloc` and `blank == 0` hold just
10209    /// as well for an arm that credits *more* than the literal — an arm
10210    /// inserting `start - 1`, or the whole file from row 0, passes all
10211    /// of them and the rest of the lib suite besides (measured).
10212    ///
10213    /// This fixture carries a comment row and a genuine blank row
10214    /// *outside* the literal, so `cloc` and `blank` are the axes that
10215    /// only a correctly-bounded arm can hit. Both over-crediting
10216    /// perturbations above drive `blank` to 0 and fail here.
10217    ///
10218    /// It is also a #1396 regression test in its own right: pre-fix the
10219    /// heredoc's empty interior row made this `ploc 5, blank 2`.
10220    #[cfg(feature = "php")]
10221    #[test]
10222    fn php_heredoc_credits_only_the_literals_rows() {
10223        // rows: 1 `<?php`, 2 comment, 3 blank, 4 opening, 5 `a`,
10224        // 6 empty-inside-the-literal, 7 `b`, 8 closing marker.
10225        // expected: ploc 6 (1, 4-8), cloc 1 (row 2), blank 1 (row 3).
10226        let loc = php_loc(b"<?php\n// note\n\n$s = <<<EOT\na\n\nb\nEOT;\n");
10227        assert_eq!(loc.sloc(), 8);
10228        assert_eq!(loc.ploc(), 6);
10229        assert_eq!(loc.cloc(), 1);
10230        assert_eq!(loc.blank(), 1);
10231    }
10232
10233    /// #1396 in the input class both metric harnesses normalise away: a
10234    /// heredoc whose closing marker is the last byte of the file.
10235    /// `check_metrics` trims and re-appends a trailing newline, so that
10236    /// shape cannot reach the parser through it at all
10237    /// (`.claude/rules/testing.md`); `metrics_verbatim` hands the bytes
10238    /// over untouched.
10239    ///
10240    /// Unlike #1051 and #1067, no *known* defect hides in this class for
10241    /// #1396 — the trailing-newline twin above already fails against
10242    /// unfixed code, and no perturbation yet found separates the two.
10243    /// A distinct input class costs one call, so it stays.
10244    #[cfg(feature = "php")]
10245    #[test]
10246    fn php_heredoc_ending_at_eof_credits_every_row() {
10247        // expected: the six rows of the issue's fixture, all code, with
10248        // no newline after `EOT;`.
10249        let loc = php_loc(b"<?php\n$s = <<<EOT\na\n\nb\nEOT;");
10250        assert_eq!(loc.sloc(), 6);
10251        assert_eq!(loc.ploc(), 6);
10252        assert_eq!(loc.cloc(), 0);
10253        assert_eq!(loc.blank(), 0);
10254    }
10255
10256    /// The #1414 fixture for [`add_multiline_string_ploc`]'s parent gate:
10257    /// two multi-row Rust literals, the first opening on row 1 under an
10258    /// `array_expression` that started on row 0, the second opening on
10259    /// row 4 alongside the `const_item` that owns it.
10260    ///
10261    /// Returned as a [`crate::Ast`] rather than as a pair of [`Node`]s
10262    /// because the nodes borrow the held tree.
10263    #[cfg(feature = "rust")]
10264    fn multiline_string_gate_fixture() -> crate::Ast {
10265        crate::test_support::parse_named(
10266            LANG::Rust,
10267            "lib.rs",
10268            "const A: [&str; 1] = [\n    \"a\nb\",\n];\nconst B: &str = \"c\nd\";\n",
10269        )
10270    }
10271
10272    /// Returns the `index`-th `string_literal` of
10273    /// [`multiline_string_gate_fixture`], asserting the fixture still
10274    /// holds both and that this one still spans `rows`.
10275    ///
10276    /// The span check is the fixture's own guard: a grammar bump that
10277    /// re-shaped the literal would otherwise turn the row assertions
10278    /// below into claims about some other node.
10279    #[cfg(feature = "rust")]
10280    fn multiline_string_gate_literal<'a>(
10281        root: &Node<'a>,
10282        index: usize,
10283        rows: (usize, usize),
10284    ) -> Node<'a> {
10285        let literals = root.descendants_by_kind(&["string_literal"]);
10286        assert_eq!(
10287            literals.len(),
10288            2,
10289            "fixture drift: expected exactly two string literals"
10290        );
10291        let literal = literals[index];
10292        assert_eq!(
10293            (literal.start_row(), literal.end_row()),
10294            rows,
10295            "fixture drift: literal {index} no longer spans the expected rows"
10296        );
10297        literal
10298    }
10299
10300    /// [`add_multiline_string_ploc`] credits the opening row when the
10301    /// parent started earlier — the branch #1414 measured as unguarded.
10302    ///
10303    /// End-to-end neither half of that gate is observable, which is why
10304    /// this test is at the helper's level rather than through a
10305    /// fixture. Every language routing a literal through this helper
10306    /// ends its match with an unconditional catch-all that has already
10307    /// inserted the literal's start row, and both `LineSet::insert` and
10308    /// `check_comment_ends_on_code_line` are idempotent — so crediting
10309    /// the row twice reads the same as crediting it once, and skipping
10310    /// it drops nothing the catch-all does not put back. Before these
10311    /// tests, removing the gate and inverting it each failed zero of
10312    /// the ~3,330 lib tests. Only a call that reaches the helper with
10313    /// the row still absent separates them.
10314    #[cfg(feature = "rust")]
10315    #[test]
10316    fn multiline_string_credits_an_opening_row_its_parent_did_not_start() {
10317        let ast = multiline_string_gate_fixture();
10318        let root = ast.root_node();
10319        let literal = multiline_string_gate_literal(&root, 0, (1, 2));
10320
10321        let mut stats = Stats::default();
10322        // Seed the row a real caller's catch-all would already hold, so
10323        // nothing below can pass against a default-empty set.
10324        stats.ploc.lines.insert(0);
10325
10326        add_multiline_string_ploc(
10327            &literal,
10328            Ancestors::unknown(),
10329            &mut stats,
10330            literal.start_row(),
10331        );
10332
10333        assert!(stats.ploc.lines.contains(0), "the seeded row must survive");
10334        assert!(
10335            stats.ploc.lines.contains(1),
10336            "the `array_expression` parent starts on row 0, so nothing but \
10337             this helper covers the literal's opening row"
10338        );
10339        assert!(
10340            stats.ploc.lines.contains(2),
10341            "the literal's closing row is interior to it"
10342        );
10343        assert!(
10344            !stats.ploc.lines.contains(3),
10345            "row 3 is `];`, past the literal"
10346        );
10347    }
10348
10349    /// The gate's other side: [`add_multiline_string_ploc`] leaves the
10350    /// opening row alone when the parent started on it, and still runs
10351    /// the interior insertion. See the sibling above for why this is
10352    /// invisible end-to-end.
10353    #[cfg(feature = "rust")]
10354    #[test]
10355    fn multiline_string_leaves_an_opening_row_its_parent_started() {
10356        let ast = multiline_string_gate_fixture();
10357        let root = ast.root_node();
10358        let literal = multiline_string_gate_literal(&root, 1, (4, 5));
10359
10360        let mut stats = Stats::default();
10361        stats.ploc.lines.insert(0);
10362
10363        add_multiline_string_ploc(
10364            &literal,
10365            Ancestors::unknown(),
10366            &mut stats,
10367            literal.start_row(),
10368        );
10369
10370        assert!(stats.ploc.lines.contains(0), "the seeded row must survive");
10371        assert!(
10372            !stats.ploc.lines.contains(4),
10373            "the `const_item` parent starts on row 4 and already covers it, \
10374             so the gate must skip it"
10375        );
10376        assert!(
10377            stats.ploc.lines.contains(5),
10378            "the interior insertion runs whichever way the gate goes"
10379        );
10380    }
10381
10382    /// [`add_string_interior_ploc`] stops at the last row the literal
10383    /// *occupies*, not at its raw end row — the third #1414 branch, and
10384    /// the one that helper's own doc comment argues for.
10385    ///
10386    /// The two spellings differ only for a node whose end column is 0,
10387    /// and the one *string literal* known to have that shape here is a
10388    /// Bash `heredoc_body`: it absorbs the newline after its last
10389    /// content row and so ends at column 0 of the terminator's row.
10390    /// Everywhere else a literal stops just past its closing delimiter,
10391    /// where `end_line() - 1` and `end_row()` agree. Since #1423 the
10392    /// C-family `preproc_arg` is a second caller of this helper that
10393    /// reaches column 0 — a macro body ending on a dangling backslash —
10394    /// and `a_dangling_macro_continuation_does_not_credit_the_row_below`
10395    /// covers that one end-to-end.
10396    ///
10397    /// The call is direct rather than through `bca metrics`
10398    /// deliberately — which node `bash.rs` routes is a separate
10399    /// question (#1412), and an end-to-end fixture would stop
10400    /// discriminating the moment that answer changed.
10401    #[cfg(feature = "bash")]
10402    #[test]
10403    fn string_interior_stops_at_the_last_row_the_literal_occupies() {
10404        // rows: 0 `cat <<EOF`, 1 `a`, 2 `b`, 3 `EOF`.
10405        let ast = crate::test_support::parse_named(LANG::Bash, "f.sh", "cat <<EOF\na\nb\nEOF\n");
10406        let root = ast.root_node();
10407        let bodies = root.descendants_by_kind(&["heredoc_body"]);
10408        let body = bodies.first().expect("the fixture holds one heredoc body");
10409        assert_eq!(
10410            (body.start_position(), body.end_position()),
10411            ((1, 0), (3, 0)),
10412            "fixture drift: the end column must be 0 for this test to mean anything"
10413        );
10414
10415        let mut stats = Stats::default();
10416        // Row 0 is the `cat <<EOF` line, which Bash's arm credits
10417        // through a different node; seeding it keeps every assertion
10418        // below distinguishable from the default-empty set.
10419        stats.ploc.lines.insert(0);
10420
10421        add_string_interior_ploc(body, &mut stats, body.start_row());
10422
10423        assert!(stats.ploc.lines.contains(0), "the seeded row must survive");
10424        assert!(
10425            !stats.ploc.lines.contains(1),
10426            "this helper leaves its `start` row to the caller"
10427        );
10428        assert!(
10429            stats.ploc.lines.contains(2),
10430            "row 2 is the body's last content row"
10431        );
10432        assert!(
10433            !stats.ploc.lines.contains(3),
10434            "row 3 holds the `EOF` terminator: the body ends at its column 0 \
10435             and so does not occupy it"
10436        );
10437    }
10438
10439    #[cfg(feature = "elixir")]
10440    #[test]
10441    fn elixir_blank() {
10442        // Two blank lines separate three top-level expressions.
10443        check_metrics::<ElixirParser>(
10444            "defmodule Foo do\n\n  def a, do: :a\n\n  def b, do: :b\nend\n",
10445            "foo.ex",
10446            |metric| {
10447                assert_eq!(metric.loc.sloc(), 6);
10448                assert_eq!(metric.loc.ploc(), 4);
10449                assert_eq!(metric.loc.lloc(), 3);
10450                assert_eq!(metric.loc.cloc(), 0);
10451                assert_eq!(metric.loc.blank(), 2);
10452                insta::assert_json_snapshot!(
10453                    metric.loc,
10454                    @r#"
10455                {
10456                  "sloc": 6,
10457                  "ploc": 4,
10458                  "lloc": 3,
10459                  "cloc": 0,
10460                  "blank": 2,
10461                  "dloc": 0,
10462                  "sloc_average": 1.5,
10463                  "ploc_average": 1.0,
10464                  "lloc_average": 0.75,
10465                  "cloc_average": 0.0,
10466                  "blank_average": 0.5,
10467                  "dloc_average": 0.0,
10468                  "sloc_min": 1,
10469                  "sloc_max": 6,
10470                  "cloc_min": 0,
10471                  "cloc_max": 0,
10472                  "ploc_min": 1,
10473                  "ploc_max": 4,
10474                  "lloc_min": 1,
10475                  "lloc_max": 3,
10476                  "blank_min": 0,
10477                  "blank_max": 2,
10478                  "dloc_min": 0,
10479                  "dloc_max": 0
10480                }
10481                "#
10482                );
10483            },
10484        );
10485    }
10486
10487    #[cfg(feature = "elixir")]
10488    #[test]
10489    fn elixir_no_zero_blank() {
10490        // Blank line interleaved with code that carries trailing comments —
10491        // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
10492        check_metrics::<ElixirParser>(
10493            "defmodule Foo do\n  def f, do: :ok\n\n  def g, do: :ok # trailing\n  def h, do: :ok # trailing\nend\n",
10494            "foo.ex",
10495            |metric| {
10496                assert_eq!(metric.loc.sloc(), 6);
10497                assert_eq!(metric.loc.ploc(), 5);
10498                assert_eq!(metric.loc.cloc(), 2);
10499                assert_eq!(metric.loc.blank(), 1);
10500            },
10501        );
10502    }
10503
10504    #[cfg(feature = "elixir")]
10505    #[test]
10506    fn elixir_blank_zero_sanity() {
10507        // Sanity check: blank must report 0, never go negative, when the
10508        // input has no blank lines.
10509        check_metrics::<ElixirParser>(
10510            "defmodule Foo do\n  def f, do: :ok\nend\n",
10511            "foo.ex",
10512            |metric| {
10513                assert_eq!(metric.loc.blank(), 0);
10514            },
10515        );
10516    }
10517
10518    #[cfg(feature = "elixir")]
10519    #[test]
10520    fn elixir_cloc() {
10521        // Mix of standalone comments and a comment on the same line as
10522        // code. Elixir has no block comment syntax — only `#` lines.
10523        check_metrics::<ElixirParser>(
10524            "# top\ndefmodule Foo do\n  # body\n  def f, do: :ok # trailing\nend\n",
10525            "foo.ex",
10526            |metric| {
10527                assert_eq!(metric.loc.cloc(), 3);
10528            },
10529        );
10530    }
10531
10532    #[cfg(feature = "elixir")]
10533    #[test]
10534    fn elixir_lloc() {
10535        // Two statements at the top level of the module body — the
10536        // `defmodule` call itself counts as one statement (since its
10537        // parent is `Source`), and each `def` inside its `do_block`
10538        // counts too: 1 + 2 = 3.
10539        check_metrics::<ElixirParser>(
10540            "defmodule Foo do\n  def a, do: 1\n  def b, do: 2\nend\n",
10541            "foo.ex",
10542            |metric| {
10543                assert_eq!(metric.loc.lloc(), 3);
10544            },
10545        );
10546    }
10547
10548    #[cfg(feature = "elixir")]
10549    #[test]
10550    fn elixir_no_nested_call_lloc() {
10551        // Calls nested inside another call's arguments are NOT direct
10552        // children of a statement container, so they do not bump LLOC.
10553        // Three syntactic calls (`defmodule`, `def`, `IO.puts`) → 3.
10554        check_metrics::<ElixirParser>(
10555            "defmodule Foo do\n  def f do\n    IO.puts(Enum.join([1, 2, 3], \", \"))\n  end\nend\n",
10556            "foo.ex",
10557            |metric| {
10558                assert_eq!(metric.loc.lloc(), 3);
10559            },
10560        );
10561    }
10562
10563    #[cfg(feature = "elixir")]
10564    #[test]
10565    fn elixir_no_binary_operator_inside_call_lloc() {
10566        // Binary operators inside call arguments are sub-expressions,
10567        // not statements. A single `def` body containing `IO.puts(a + b)`
10568        // produces 3 LLOC (defmodule, def, IO.puts) — the `a + b`
10569        // binary_operator is not a direct child of any statement
10570        // container.
10571        check_metrics::<ElixirParser>(
10572            "defmodule Foo do\n  def f(a, b) do\n    IO.puts(a + b)\n  end\nend\n",
10573            "foo.ex",
10574            |metric| {
10575                assert_eq!(metric.loc.lloc(), 3);
10576            },
10577        );
10578    }
10579
10580    #[cfg(feature = "elixir")]
10581    #[test]
10582    fn elixir_stab_clause_counts_lloc() {
10583        // Each `stab_clause` arm in a `case do ... end` is a direct
10584        // child of the inner `do_block`, so each one is its own LLOC.
10585        // defmodule + def + case + 3 arms = 6 logical lines.
10586        check_metrics::<ElixirParser>(
10587            "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",
10588            "foo.ex",
10589            |metric| {
10590                assert_eq!(metric.loc.lloc(), 6);
10591            },
10592        );
10593    }
10594
10595    #[cfg(feature = "elixir")]
10596    #[test]
10597    fn elixir_no_comment_lloc() {
10598        // Comments are direct children of a statement container but
10599        // are routed through the dedicated `Comment` arm in `compute`,
10600        // so they MUST NOT bump LLOC. Only `defmodule` and `def`
10601        // contribute LLOC here.
10602        check_metrics::<ElixirParser>(
10603            "# leading\ndefmodule Foo do\n  # inside\n  def f, do: :ok\n  # trailing\nend\n",
10604            "foo.ex",
10605            |metric| {
10606                assert_eq!(metric.loc.lloc(), 2);
10607            },
10608        );
10609    }
10610
10611    #[cfg(feature = "elixir")]
10612    #[test]
10613    fn elixir_no_do_token_lloc() {
10614        // The `do` and `end` keyword tokens are unnamed leaves inside a
10615        // `do_block`; they must not be counted as statements. A body
10616        // with one expression produces exactly 2 LLOC (defmodule and
10617        // the inner expression).
10618        check_metrics::<ElixirParser>("defmodule Foo do\n  :ok\nend\n", "foo.ex", |metric| {
10619            // `:ok` is an `Atom` whose parent is the module-call's
10620            // `do_block`; that counts. Plus the `defmodule` call.
10621            assert_eq!(metric.loc.lloc(), 2);
10622        });
10623    }
10624
10625    #[cfg(feature = "elixir")]
10626    #[test]
10627    fn elixir_no_keyword_pair_lloc() {
10628        // `key: value` keyword pairs inside an argument list (`def f,
10629        // do: :ok`) are children of an `arguments` / `keywords` node,
10630        // not a statement container, so they don't bump LLOC.
10631        check_metrics::<ElixirParser>(
10632            "defmodule Foo do\n  def add(a, b), do: a + b\nend\n",
10633            "foo.ex",
10634            |metric| {
10635                // defmodule (1) + def (1) = 2
10636                assert_eq!(metric.loc.lloc(), 2);
10637            },
10638        );
10639    }
10640
10641    #[cfg(feature = "elixir")]
10642    #[test]
10643    fn elixir_no_string_content_lloc() {
10644        // `quoted_content` chunks inside a heredoc / regular string are
10645        // structural and don't represent statements. A `@moduledoc`
10646        // attribute call with a multi-line string contributes exactly
10647        // one LLOC (the `@moduledoc` call), not one per content line.
10648        check_metrics::<ElixirParser>(
10649            "defmodule Foo do\n  @moduledoc \"\"\"\n  line one\n  line two\n  \"\"\"\n  def f, do: :ok\nend\n",
10650            "foo.ex",
10651            |metric| {
10652                // defmodule + @moduledoc + def = 3
10653                assert_eq!(metric.loc.lloc(), 3);
10654            },
10655        );
10656    }
10657
10658    #[cfg(feature = "elixir")]
10659    #[test]
10660    fn elixir_multiline_string_forms_credit_every_row_to_ploc() {
10661        // Regression test for #1260. `quoted_content` — the literal text of
10662        // every Elixir string form — is childless, so the leaf branch of
10663        // the catch-all credited only its opening row and the interior rows
10664        // reached neither PLOC nor CLOC.
10665
10666        // expected: 3 rows of a plain `"…"` literal — `x = "line one`,
10667        // `line two`, `line three"` — all code, none blank.
10668        check_metrics::<ElixirParser>(
10669            "x = \"line one\nline two\nline three\"",
10670            "foo.ex",
10671            |metric| {
10672                assert_eq!(metric.loc.sloc(), 3);
10673                assert_eq!(metric.loc.ploc(), 3);
10674                assert_eq!(metric.loc.cloc(), 0);
10675                assert_eq!(metric.loc.blank(), 0);
10676            },
10677        );
10678
10679        // expected: 4 rows of a `'''` charlist whose third row is empty
10680        // inside the literal — `c = '''`, `c1`, ``, `'''`. Code, not blank.
10681        check_metrics::<ElixirParser>("c = '''\nc1\n\n'''", "foo.ex", |metric| {
10682            assert_eq!(metric.loc.sloc(), 4);
10683            assert_eq!(metric.loc.ploc(), 4);
10684            assert_eq!(metric.loc.cloc(), 0);
10685            assert_eq!(metric.loc.blank(), 0);
10686        });
10687
10688        // expected: 4 rows of a `~s"""` sigil heredoc — `s = ~s"""`, `s1`,
10689        // `s2`, `"""`. A sigil is a distinct grammar node from `string` and
10690        // `charlist`; all three wrap the same `quoted_content`, which is
10691        // why one arm covers them.
10692        check_metrics::<ElixirParser>("s = ~s\"\"\"\ns1\ns2\n\"\"\"", "foo.ex", |metric| {
10693            assert_eq!(metric.loc.sloc(), 4);
10694            assert_eq!(metric.loc.ploc(), 4);
10695            assert_eq!(metric.loc.cloc(), 0);
10696            assert_eq!(metric.loc.blank(), 0);
10697        });
10698    }
10699
10700    #[cfg(feature = "elixir")]
10701    #[test]
10702    fn elixir_module_attribute_docstring_rows_are_ploc_not_cloc() {
10703        // #1260 had to choose a bucket for `@doc` / `@moduledoc` heredoc
10704        // rows, and this pins it: PLOC.
10705        //
10706        // Python's one carve-out to CLOC is a *bare* string expression
10707        // statement whose value is discarded — a docstring by position. An
10708        // Elixir module attribute is not that shape: `@doc "…"` is an
10709        // assignment whose value the compiler stores and
10710        // `Code.fetch_docs/1` reads back, so its Python analogue is
10711        // `x = """…"""`, which Python counts as PLOC (#415).
10712        //
10713        // expected: 7 rows — `defmodule M do`, `  @doc """`, `  docs one`,
10714        // `  docs two`, `  """`, `  def f, do: :ok`, `end` — all code, none
10715        // blank and none comment. lloc 3 = defmodule + @doc + def.
10716        check_metrics::<ElixirParser>(
10717            "defmodule M do\n  @doc \"\"\"\n  docs one\n  docs two\n  \"\"\"\n  def f, do: :ok\nend",
10718            "foo.ex",
10719            |metric| {
10720                assert_eq!(metric.loc.sloc(), 7);
10721                assert_eq!(metric.loc.ploc(), 7);
10722                assert_eq!(metric.loc.lloc(), 3);
10723                assert_eq!(metric.loc.cloc(), 0);
10724                // The same decision keeps `@doc` out of `loc.dloc`:
10725                // that field reports doc comments, docstrings and POD,
10726                // and an `@doc` assignment is none of those shapes
10727                // (#1597).
10728                assert_eq!(metric.loc.dloc(), 0);
10729                assert_eq!(metric.loc.blank(), 0);
10730            },
10731        );
10732    }
10733
10734    #[cfg(feature = "elixir")]
10735    #[test]
10736    fn elixir_rescue_arm_counts_lloc() {
10737        // Each rescue arm's body has a single expression (e.g. `:bad`)
10738        // that counts as one LLOC; the `stab_clause` header itself is
10739        // skipped. The rescue_block named node is also a direct child
10740        // of try's do_block, so it contributes one LLOC too.
10741        // Total: defmodule + def + try + do_it() + rescue_block
10742        //        + 2 arm bodies = 7.
10743        check_metrics::<ElixirParser>(
10744            "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",
10745            "foo.ex",
10746            |metric| {
10747                assert_eq!(metric.loc.lloc(), 7);
10748            },
10749        );
10750    }
10751
10752    #[cfg(feature = "elixir")]
10753    #[test]
10754    fn elixir_no_arg_punctuation_lloc() {
10755        // Function-call arguments (`a, b` inside `def add(a, b)`) are
10756        // children of an `arguments` node, not of a statement container.
10757        // They MUST NOT inflate LLOC.
10758        check_metrics::<ElixirParser>(
10759            "defmodule Foo do\n  def add(a, b, c, d) do\n    a + b + c + d\n  end\nend\n",
10760            "foo.ex",
10761            |metric| {
10762                // defmodule + def + (a+b+c+d) = 3
10763                assert_eq!(metric.loc.lloc(), 3);
10764            },
10765        );
10766    }
10767
10768    #[cfg(feature = "elixir")]
10769    #[test]
10770    fn elixir_no_list_element_lloc() {
10771        // List literal elements live under a `list` node, not a
10772        // statement container — they must not bump LLOC.
10773        check_metrics::<ElixirParser>(
10774            "defmodule Foo do\n  def f do\n    [:a, :b, :c, :d]\n  end\nend\n",
10775            "foo.ex",
10776            |metric| {
10777                // defmodule + def + the list expression = 3
10778                assert_eq!(metric.loc.lloc(), 3);
10779            },
10780        );
10781    }
10782
10783    #[cfg(feature = "elixir")]
10784    #[test]
10785    fn elixir_no_map_field_lloc() {
10786        // Map `pair`s live under `map`, not a statement container.
10787        check_metrics::<ElixirParser>(
10788            "defmodule Foo do\n  def f do\n    %{a: 1, b: 2, c: 3}\n  end\nend\n",
10789            "foo.ex",
10790            |metric| {
10791                assert_eq!(metric.loc.lloc(), 3);
10792            },
10793        );
10794    }
10795
10796    #[cfg(feature = "elixir")]
10797    #[test]
10798    fn elixir_anonymous_fn_body_lloc() {
10799        // `lloc()` on the Unit space returns the aggregate (own +
10800        // nested-space) count. Even though the anonymous_function is
10801        // its own function space, the merge step pulls its `lloc` back
10802        // into the parent. Counts:
10803        //   Unit own: defmodule, def, `add = fn ...`, final `add` = 4
10804        //   anon-fn:  `x + 1` body expression                       = 1
10805        //   aggregated total                                        = 5
10806        check_metrics::<ElixirParser>(
10807            "defmodule Foo do\n  def f do\n    add = fn x -> x + 1 end\n    add\n  end\nend\n",
10808            "foo.ex",
10809            |metric| {
10810                assert_eq!(metric.loc.lloc(), 5);
10811            },
10812        );
10813    }
10814
10815    #[cfg(feature = "ruby")]
10816    #[test]
10817    fn ruby_blank() {
10818        // The parser's root span starts at the first non-blank line, so
10819        // a blank line must sit BETWEEN code lines to be counted.
10820        // expected: line 3 is blank → blank = 1.
10821        check_metrics::<RubyParser>("def foo\n  a = 1\n\n  a + 1\nend\n", "foo.rb", |metric| {
10822            assert_eq!(metric.loc.blank(), 1);
10823        });
10824    }
10825
10826    #[cfg(feature = "ruby")]
10827    #[test]
10828    fn ruby_no_zero_blank() {
10829        // Mirrors `rust_no_zero_blank`: the blank counter must stay
10830        // non-zero when blank lines sit between code lines that carry
10831        // trailing comments. Catches regressions in the SLOC −
10832        // (PLOC ∪ CLOC) union math when PLOC and CLOC line-sets
10833        // overlap.
10834        check_metrics::<RubyParser>(
10835            "def foo  # entry\n  pool = 0\n\n  server = -42  # negative\n\n  ok = false\nend\n",
10836            "foo.rb",
10837            |metric| {
10838                assert_eq!(metric.loc.blank(), 2);
10839            },
10840        );
10841    }
10842
10843    #[cfg(feature = "ruby")]
10844    #[test]
10845    fn ruby_cloc() {
10846        // 3 comment lines.
10847        check_metrics::<RubyParser>(
10848            "# one\n# two\n# three\ndef foo\nend\n",
10849            "foo.rb",
10850            |metric| {
10851                assert_eq!(metric.loc.cloc(), 3);
10852            },
10853        );
10854    }
10855
10856    #[cfg(feature = "ruby")]
10857    #[test]
10858    fn ruby_lloc() {
10859        // expected: 3 logical lines = `def` (Method) + `if` (If) +
10860        // `while` (While). Bare expression-statements (assignments,
10861        // calls) are intentionally NOT counted.
10862        check_metrics::<RubyParser>(
10863            "def foo(a)\n  if a\n    a += 1\n  end\n  while a > 0\n    a -= 1\n  end\nend\n",
10864            "foo.rb",
10865            |metric| {
10866                assert_eq!(metric.loc.lloc(), 3);
10867            },
10868        );
10869    }
10870
10871    #[cfg(feature = "ruby")]
10872    #[test]
10873    fn ruby_no_call_lloc() {
10874        // expected: 1 logical line (the surrounding `def`). The bare
10875        // method calls `puts 'hello'` and `puts 'world'` are
10876        // intentionally NOT counted — there is no expression_statement
10877        // wrapper to disambiguate them from sub-expressions.
10878        check_metrics::<RubyParser>(
10879            "def foo\n  puts 'hello'\n  puts 'world'\nend\n",
10880            "foo.rb",
10881            |metric| {
10882                assert_eq!(metric.loc.lloc(), 1);
10883            },
10884        );
10885    }
10886
10887    #[cfg(feature = "ruby")]
10888    #[test]
10889    fn ruby_no_assignment_lloc() {
10890        // Same rationale as `ruby_no_call_lloc`. expected: 1 lloc
10891        // (the `def`); raw assignments aren't counted.
10892        check_metrics::<RubyParser>(
10893            "def foo\n  a = 1\n  b = 2\n  c = a + b\nend\n",
10894            "foo.rb",
10895            |metric| {
10896                assert_eq!(metric.loc.lloc(), 1);
10897            },
10898        );
10899    }
10900
10901    #[cfg(feature = "ruby")]
10902    #[test]
10903    fn ruby_modifier_lloc() {
10904        // Postfix modifier forms each count as one logical line. A
10905        // `return … if …` parses as an `IfModifier` wrapping a `Return`;
10906        // both fire the LLOC arm so the modifier line contributes +2.
10907        // expected: def(1) + if_modifier(1) + inner return(1)
10908        // + while_modifier(1) + rescue_modifier(1) = 5.
10909        check_metrics::<RubyParser>(
10910            "def foo(a)\n  return a if a.nil?\n  a -= 1 while a > 0\n  parse(a) rescue nil\nend\n",
10911            "foo.rb",
10912            |metric| {
10913                assert_eq!(metric.loc.lloc(), 5);
10914            },
10915        );
10916    }
10917
10918    #[cfg(feature = "ruby")]
10919    #[test]
10920    fn ruby_class_lloc() {
10921        // expected: 1 class + 1 module + 2 methods = 4.
10922        check_metrics::<RubyParser>(
10923            "module M\n  class C\n    def foo\n    end\n    def bar\n    end\n  end\nend\n",
10924            "foo.rb",
10925            |metric| {
10926                assert_eq!(metric.loc.lloc(), 4);
10927            },
10928        );
10929    }
10930
10931    #[cfg(feature = "ruby")]
10932    #[test]
10933    fn ruby_begin_rescue_lloc() {
10934        // expected: 1 def + 1 begin = 2. Rescue clauses are part of
10935        // the begin construct and not separately counted; the bare
10936        // expression body lines are not statements.
10937        check_metrics::<RubyParser>(
10938            "def foo\n  begin\n    risky\n  rescue StandardError\n    nil\n  end\nend\n",
10939            "foo.rb",
10940            |metric| {
10941                assert_eq!(metric.loc.lloc(), 2);
10942            },
10943        );
10944    }
10945
10946    #[cfg(feature = "ruby")]
10947    #[test]
10948    fn ruby_nested_defs_lloc() {
10949        // Each `Method` declaration contributes one logical line.
10950        // expected: outer `def` + inner `def` = 2.
10951        check_metrics::<RubyParser>(
10952            "def outer\n  def inner\n    1\n  end\nend\n",
10953            "foo.rb",
10954            |metric| {
10955                assert_eq!(metric.loc.lloc(), 2);
10956            },
10957        );
10958    }
10959
10960    #[cfg(feature = "ruby")]
10961    #[test]
10962    fn ruby_no_block_body_lloc() {
10963        // A top-level `[1,2,3].each do |x| puts x end` produces zero
10964        // logical lines: the surrounding `.each` is a `Call` (not in
10965        // the LLOC arm), the `DoBlock` is a closure (also not a
10966        // statement), and the `puts x` inside is another call. This
10967        // pins the documented expression-statement exclusion.
10968        check_metrics::<RubyParser>(
10969            "[1, 2, 3].each do |x|\n  puts x\nend\n",
10970            "foo.rb",
10971            |metric| {
10972                assert_eq!(metric.loc.lloc(), 0);
10973            },
10974        );
10975    }
10976
10977    #[cfg(feature = "ruby")]
10978    #[test]
10979    fn ruby_no_lambda_body_lloc() {
10980        // `add = ->(a, b) { a + b }` produces zero logical lines for
10981        // the same reason as `ruby_no_block_body_lloc`: assignments,
10982        // calls, and lambda bodies are intentionally not statements
10983        // in this impl.
10984        check_metrics::<RubyParser>("add = ->(a, b) {\n  a + b\n}\n", "foo.rb", |metric| {
10985            assert_eq!(metric.loc.lloc(), 0);
10986        });
10987    }
10988
10989    #[cfg(feature = "ruby")]
10990    #[test]
10991    fn ruby_heredoc_lloc_and_blank() {
10992        // A `<<~TXT` heredoc contributes: SLOC = every line in the file
10993        // (including the heredoc body); LLOC = just the surrounding `def`.
10994        // #778: the heredoc-body rows (`one`, `two`) hold real string text,
10995        // so they are credited to PLOC like Python's multi-line strings
10996        // (#415) rather than mislabelled as blank. Every row is now code.
10997        // expected: sloc = 7, ploc = 7, lloc = 1, blank = 0.
10998        check_metrics::<RubyParser>(
10999            "def foo\n  msg = <<~TXT\n    one\n    two\n  TXT\n  msg\nend\n",
11000            "foo.rb",
11001            |metric| {
11002                assert_eq!(metric.loc.sloc(), 7);
11003                assert_eq!(metric.loc.ploc(), 7);
11004                assert_eq!(metric.loc.lloc(), 1);
11005                assert_eq!(metric.loc.blank(), 0);
11006            },
11007        );
11008    }
11009
11010    #[cfg(feature = "ruby")]
11011    #[test]
11012    fn ruby_semicolon_multistatement_lloc_undercount() {
11013        // Documented limitation: Ruby has no `expression_statement`
11014        // wrapper, so `;`-separated multi-statement lines collapse to
11015        // a single LLOC bump (the surrounding `def`). A future
11016        // statement-counter that walks BlockBody children would
11017        // change this — pin the current behaviour so the regression
11018        // is visible.
11019        check_metrics::<RubyParser>(
11020            "def foo\n  a = 1; b = 2; a + b\nend\n",
11021            "foo.rb",
11022            |metric| {
11023                assert_eq!(metric.loc.lloc(), 1);
11024            },
11025        );
11026    }
11027
11028    #[cfg(feature = "ruby")]
11029    #[test]
11030    fn ruby_ploc_skips_comments_and_blanks() {
11031        // PLOC counts physical instruction lines: code-bearing lines
11032        // only. Comments and blanks are excluded.
11033        check_metrics::<RubyParser>("# header\n\ndef foo\n  a = 1\nend\n", "foo.rb", |metric| {
11034            assert_eq!(metric.loc.ploc(), 3);
11035            assert_eq!(metric.loc.cloc(), 1);
11036            assert_eq!(metric.loc.blank(), 1);
11037        });
11038    }
11039
11040    // -----------------------------------------------------------------
11041    // Issue #195: nested-function/closure LLOC tests across languages.
11042    // Mirrors the prior art for Rust (`rust_function_in_loop_lloc`,
11043    // `rust_closure_expression_lloc`), Mozjs (`mozjs_nested_function_loc`),
11044    // Bash (`bash_nested_function_loc`), and TypeScript
11045    // (`typescript_nested_functions_loc`, `tsx_nested_functions_loc`).
11046    // -----------------------------------------------------------------
11047
11048    #[cfg(feature = "python")]
11049    #[test]
11050    fn python_nested_def_lloc() {
11051        // Nested `def`: the inner function declaration plus the outer
11052        // body's `return inner()` are both LLOC; the outer `def` header
11053        // and the inner `return 1` belong to their own function spaces.
11054        check_metrics::<PythonParser>(
11055            "def outer():\n    def inner():\n        return 1\n    return inner()\n",
11056            "foo.py",
11057            |metric| {
11058                assert_eq!(metric.loc.sloc(), 4);
11059                assert_eq!(metric.loc.ploc(), 4);
11060                assert_eq!(metric.loc.lloc(), 2);
11061                assert_eq!(metric.loc.cloc(), 0);
11062                assert_eq!(metric.loc.blank(), 0);
11063                insta::assert_json_snapshot!(metric.loc);
11064            },
11065        );
11066    }
11067
11068    #[cfg(feature = "python")]
11069    #[test]
11070    fn python_lambda_in_def_lloc() {
11071        // `lambda x: x + 1` is an expression, not a Python `function_definition`,
11072        // so it does not start a new function space. The two LLOC come from
11073        // the assignment `f = lambda ...` and the `return f(2)` statement.
11074        check_metrics::<PythonParser>(
11075            "def outer():\n    f = lambda x: x + 1\n    return f(2)\n",
11076            "foo.py",
11077            |metric| {
11078                assert_eq!(metric.loc.sloc(), 3);
11079                assert_eq!(metric.loc.ploc(), 3);
11080                assert_eq!(metric.loc.lloc(), 2);
11081                assert_eq!(metric.loc.cloc(), 0);
11082                assert_eq!(metric.loc.blank(), 0);
11083                insta::assert_json_snapshot!(metric.loc);
11084            },
11085        );
11086    }
11087
11088    #[cfg(feature = "python")]
11089    #[test]
11090    fn python_match_statement_lloc() {
11091        // `match` (PEP 634) is a control-flow statement that must add one
11092        // LLOC like `if`/`try`, plus each `return` in a case body (#462).
11093        // Its `case_clause` children add nothing, mirroring how
11094        // `elif_clause`/`else_clause` are absent from the LLOC arm: the
11095        // construct counts once and the statements inside count via their
11096        // own arms. Here: match(1) + two `return`s = 3.
11097        check_metrics::<PythonParser>(
11098            "def f(x):\n    match x:\n        case 1: return 1\n        case _: return 0\n",
11099            "foo.py",
11100            |metric| {
11101                assert_eq!(metric.loc.lloc(), 3);
11102                insta::assert_json_snapshot!(metric.loc);
11103            },
11104        );
11105    }
11106
11107    #[cfg(feature = "python")]
11108    #[test]
11109    fn python_match_lloc_matches_if_else() {
11110        // Parity with the equivalent two-branch `if`/`else`: both have the
11111        // construct keyword (1) plus two `return` bodies (2), so each must
11112        // report an identical LLOC of 3. The match form previously
11113        // undercounted (2) because `match_statement` was absent from the
11114        // LLOC arm (#462). `check_metrics` takes a non-capturing `fn`, so
11115        // the shared expectation is pinned to the same literal in both
11116        // closures rather than threaded through a captured variable.
11117        check_metrics::<PythonParser>(
11118            "def f(x):\n    if x == 1: return 1\n    else: return 0\n",
11119            "foo.py",
11120            |metric| assert_eq!(metric.loc.lloc(), 3),
11121        );
11122        check_metrics::<PythonParser>(
11123            "def f(x):\n    match x:\n        case 1: return 1\n        case _: return 0\n",
11124            "foo.py",
11125            |metric| assert_eq!(metric.loc.lloc(), 3),
11126        );
11127    }
11128
11129    #[cfg(feature = "python")]
11130    #[test]
11131    fn python_type_alias_lloc() {
11132        // A `type` alias (PEP 695) is a leaf statement, counted like an
11133        // assignment. `type Alias = int` followed by `x = 1` is two LLOC;
11134        // before #462 the alias fell through to the `_` arm and the file
11135        // reported only 1.
11136        check_metrics::<PythonParser>("type Alias = int\nx = 1\n", "foo.py", |metric| {
11137            assert_eq!(metric.loc.lloc(), 2);
11138            insta::assert_json_snapshot!(metric.loc);
11139        });
11140    }
11141
11142    #[cfg(feature = "java")]
11143    #[test]
11144    fn java_local_class_in_method_lloc() {
11145        // A `class` declared inside a method body produces its own function
11146        // space, so the outer method's LLOC only sees `return new Local().v();`
11147        // and the body of `v()` contributes the second LLOC.
11148        check_metrics::<JavaParser>(
11149            "class Foo {\n    int bar() {\n        class Local {\n            int v() { return 1; }\n        }\n        return new Local().v();\n    }\n}\n",
11150            "foo.java",
11151            |metric| {
11152                assert_eq!(metric.loc.sloc(), 8);
11153                assert_eq!(metric.loc.ploc(), 8);
11154                assert_eq!(metric.loc.lloc(), 2);
11155                assert_eq!(metric.loc.cloc(), 0);
11156                assert_eq!(metric.loc.blank(), 0);
11157                insta::assert_json_snapshot!(metric.loc);
11158            },
11159        );
11160    }
11161
11162    #[cfg(feature = "java")]
11163    #[test]
11164    fn java_lambda_in_method_lloc() {
11165        // Java lambdas are expressions; the two LLOC come from the
11166        // `IntUnaryOperator f = x -> x + 1;` declaration and the
11167        // `f.applyAsInt(3);` expression statement.
11168        check_metrics::<JavaParser>(
11169            "class Foo {\n    void bar() {\n        java.util.function.IntUnaryOperator f = x -> x + 1;\n        f.applyAsInt(3);\n    }\n}\n",
11170            "foo.java",
11171            |metric| {
11172                assert_eq!(metric.loc.sloc(), 6);
11173                assert_eq!(metric.loc.ploc(), 6);
11174                assert_eq!(metric.loc.lloc(), 2);
11175                assert_eq!(metric.loc.cloc(), 0);
11176                assert_eq!(metric.loc.blank(), 0);
11177                insta::assert_json_snapshot!(metric.loc);
11178            },
11179        );
11180    }
11181
11182    #[cfg(feature = "groovy")]
11183    #[test]
11184    fn groovy_blank() {
11185        // Blank lines + simple statements. Newlines act as the
11186        // statement terminator; PLOC counts the two declaration lines.
11187        check_metrics::<GroovyParser>("int x = 1\n\n\nint y = 2", "foo.groovy", |metric| {
11188            assert_eq!(metric.loc.sloc(), 4);
11189            assert_eq!(metric.loc.ploc(), 2);
11190            assert_eq!(metric.loc.lloc(), 2);
11191            assert_eq!(metric.loc.blank(), 2);
11192        });
11193    }
11194
11195    #[cfg(feature = "groovy")]
11196    #[test]
11197    fn groovy_no_zero_blank() {
11198        // A single line with no blanks: blank() == 0.
11199        check_metrics::<GroovyParser>("int x = 1", "foo.groovy", |metric| {
11200            assert_eq!(metric.loc.sloc(), 1);
11201            assert_eq!(metric.loc.blank(), 0);
11202        });
11203    }
11204
11205    #[cfg(feature = "groovy")]
11206    #[test]
11207    fn groovy_cloc_line_comments() {
11208        check_metrics::<GroovyParser>(
11209            "// first comment
11210            int x = 1
11211            // second comment
11212            int y = 2",
11213            "foo.groovy",
11214            |metric| {
11215                assert_eq!(metric.loc.cloc(), 2);
11216                assert_eq!(metric.loc.ploc(), 2);
11217            },
11218        );
11219    }
11220
11221    #[cfg(feature = "groovy")]
11222    #[test]
11223    fn groovy_cloc_block_comment() {
11224        check_metrics::<GroovyParser>(
11225            "/* multi
11226               line
11227               comment */
11228            int x = 1",
11229            "foo.groovy",
11230            |metric| {
11231                // Block comment spans 3 lines → cloc == 3.
11232                assert_eq!(metric.loc.cloc(), 3);
11233            },
11234        );
11235    }
11236
11237    #[cfg(feature = "groovy")]
11238    #[test]
11239    fn groovy_cloc_groovydoc_comment() {
11240        // Groovy `/** … */` `groovydoc_comment` counts as CLOC. The
11241        // `Loc` arm already handled it; this pins it alongside the
11242        // restored `is_comment` parity (#697).
11243        check_metrics::<GroovyParser>(
11244            "/** groovydoc */
11245class A {
11246  int x = 1
11247}",
11248            "foo.groovy",
11249            |metric| {
11250                assert_eq!(metric.loc.cloc(), 1);
11251                assert_eq!(metric.loc.ploc(), 3);
11252            },
11253        );
11254    }
11255
11256    #[cfg(feature = "groovy")]
11257    #[test]
11258    fn groovy_simple_lloc() {
11259        // One LLOC per simple expression statement.
11260        check_metrics::<GroovyParser>(
11261            "int a = 1
11262            int b = 2
11263            int c = 3",
11264            "foo.groovy",
11265            |metric| {
11266                assert_eq!(metric.loc.lloc(), 3);
11267            },
11268        );
11269    }
11270
11271    #[cfg(feature = "groovy")]
11272    #[test]
11273    fn groovy_no_local_variable_declaration_in_for_lloc() {
11274        // The variable declaration inside a classic `for` init slot
11275        // does NOT count as an LLOC (it's an expression part of the
11276        // for-loop). Same gating as Java's `java_for_lloc`.
11277        check_metrics::<GroovyParser>(
11278            "for (int i = 0; i < 10; i++) {
11279                println(i)
11280            }",
11281            "foo.groovy",
11282            |metric| {
11283                // for-statement (1) + expression-statement `println(i)` (1) = 2
11284                assert_eq!(metric.loc.lloc(), 2);
11285            },
11286        );
11287    }
11288
11289    #[cfg(feature = "groovy")]
11290    #[test]
11291    fn groovy_lambda_in_method_lloc() {
11292        // Closures contain a statement list — the dekobon grammar wraps
11293        // a single-expression body in `expression_statement` rather than
11294        // emitting the expression directly (as Java's `lambda_expression`
11295        // does), so a one-line closure body counts as its own LLOC.
11296        // Declaration `def f = …` (1) + closure body `x + 1` (1) +
11297        // call `f(3)` (1) = 3.
11298        check_metrics::<GroovyParser>(
11299            "class Foo {
11300                void bar() {
11301                    def f = { x -> x + 1 }
11302                    f(3)
11303                }
11304            }",
11305            "foo.groovy",
11306            |metric| {
11307                assert_eq!(metric.loc.lloc(), 3);
11308            },
11309        );
11310    }
11311
11312    #[cfg(feature = "groovy")]
11313    #[test]
11314    fn groovy_try_lloc() {
11315        // try-statement counts as one LLOC; the catch body's
11316        // statements count separately.
11317        check_metrics::<GroovyParser>(
11318            "void f() {
11319                try {
11320                    risky()
11321                } catch (Exception e) {
11322                    handle(e)
11323                }
11324            }",
11325            "foo.groovy",
11326            |metric| {
11327                // try(1) + risky() expr-stmt(1) + handle() expr-stmt(1) = 3
11328                assert_eq!(metric.loc.lloc(), 3);
11329            },
11330        );
11331    }
11332
11333    #[cfg(feature = "groovy")]
11334    #[test]
11335    fn groovy_class_loc() {
11336        // Source-file-level totals across multiple methods.
11337        check_metrics::<GroovyParser>(
11338            "class A {
11339                void f() {
11340                    int x = 1
11341                }
11342                void g() {
11343                    int y = 2
11344                }
11345            }",
11346            "foo.groovy",
11347            |metric| {
11348                // 8 lines of non-comment content: `class A {`, two
11349                // `void` headers, two `int … = …` body statements,
11350                // three closing braces.
11351                assert_eq!(metric.loc.ploc(), 8);
11352                assert_eq!(metric.loc.cloc(), 0);
11353                // Two expression-statement LLOCs (`int x = 1`,
11354                // `int y = 2`).
11355                assert_eq!(metric.loc.lloc(), 2);
11356            },
11357        );
11358    }
11359
11360    #[cfg(feature = "groovy")]
11361    #[test]
11362    fn groovy_partial_parse_recovers_unit() {
11363        // Malformed input parses with ERROR but still emits a Unit
11364        // root via `spaces.rs` fallback (lesson 9). The single
11365        // source line is counted as SLOC even when the parse fails
11366        // mid-expression.
11367        check_metrics::<GroovyParser>("def x = (((", "foo.groovy", |metric| {
11368            assert_eq!(metric.loc.sloc(), 1);
11369            assert_eq!(metric.loc.blank(), 0);
11370        });
11371    }
11372
11373    #[cfg(feature = "groovy")]
11374    #[test]
11375    fn groovy_sloc() {
11376        // Mirrors `java_sloc`: basic per-line count across a mix of
11377        // statements and a blank line.
11378        check_metrics::<GroovyParser>(
11379            "int a = 1
11380            int b = 2
11381
11382            int c = 3",
11383            "foo.groovy",
11384            |metric| {
11385                assert_eq!(metric.loc.sloc(), 4);
11386                assert_eq!(metric.loc.ploc(), 3);
11387                assert_eq!(metric.loc.blank(), 1);
11388            },
11389        );
11390    }
11391
11392    #[cfg(feature = "groovy")]
11393    #[test]
11394    fn groovy_single_ploc() {
11395        // Mirrors `java_single_ploc`: one non-blank, non-comment
11396        // line of code => ploc == 1.
11397        check_metrics::<GroovyParser>("int x = 42", "foo.groovy", |metric| {
11398            assert_eq!(metric.loc.ploc(), 1);
11399            assert_eq!(metric.loc.cloc(), 0);
11400        });
11401    }
11402
11403    #[cfg(feature = "groovy")]
11404    #[test]
11405    fn groovy_multi_ploc() {
11406        // Multiple statements on separate lines all contribute to
11407        // PLOC. Mirrors `java_multi_ploc`.
11408        check_metrics::<GroovyParser>(
11409            "int a = 1
11410            int b = 2
11411            int c = 3
11412            int d = 4",
11413            "foo.groovy",
11414            |metric| {
11415                assert_eq!(metric.loc.ploc(), 4);
11416                assert_eq!(metric.loc.lloc(), 4);
11417            },
11418        );
11419    }
11420
11421    #[cfg(feature = "groovy")]
11422    #[test]
11423    fn groovy_single_statement_lloc() {
11424        // A single expression statement contributes one LLOC.
11425        // Mirrors `java_single_statement_lloc`.
11426        check_metrics::<GroovyParser>("println 'hi'", "foo.groovy", |metric| {
11427            assert_eq!(metric.loc.lloc(), 1);
11428        });
11429    }
11430
11431    #[cfg(feature = "groovy")]
11432    #[test]
11433    fn groovy_for_lloc() {
11434        // The classical `for` statement itself counts as one LLOC;
11435        // the body's `println(i)` adds another. The init-slot
11436        // var-decl is suppressed by the LocalVariableDeclaration
11437        // ancestor-check (same rule as `java_for_lloc`).
11438        check_metrics::<GroovyParser>(
11439            "for (int i = 0; i < 100; i++) {
11440                println(i)
11441            }",
11442            "foo.groovy",
11443            |metric| {
11444                // ForStatement(1) + println-expr(1) = 2
11445                assert_eq!(metric.loc.lloc(), 2);
11446            },
11447        );
11448    }
11449
11450    #[cfg(feature = "groovy")]
11451    #[test]
11452    fn groovy_foreach_lloc() {
11453        // `for (item in list)` parses as `enhanced_for_statement` —
11454        // counts as one LLOC.
11455        check_metrics::<GroovyParser>(
11456            "for (item in items) {
11457                println(item)
11458            }",
11459            "foo.groovy",
11460            |metric| {
11461                // EnhancedForStatement(1) + println(1) = 2
11462                assert_eq!(metric.loc.lloc(), 2);
11463            },
11464        );
11465    }
11466
11467    #[cfg(feature = "groovy")]
11468    #[test]
11469    fn groovy_while_lloc() {
11470        // `while` itself is one LLOC; each body statement adds
11471        // another. Mirrors `java_while_lloc`.
11472        check_metrics::<GroovyParser>(
11473            "int i = 0
11474            while (i < 10) {
11475                i++
11476                println(i)
11477            }",
11478            "foo.groovy",
11479            |metric| {
11480                // int i = 0 (1) + while (1) + i++ (1) + println (1) = 4
11481                assert_eq!(metric.loc.lloc(), 4);
11482            },
11483        );
11484    }
11485
11486    #[cfg(feature = "groovy")]
11487    #[test]
11488    fn groovy_do_while_lloc() {
11489        // `do…while` is one LLOC plus its body. Mirrors
11490        // `java_do_while_lloc`.
11491        check_metrics::<GroovyParser>(
11492            "int i = 0
11493            do {
11494                i++
11495            } while (i < 5)",
11496            "foo.groovy",
11497            |metric| {
11498                // int i = 0 (1) + do (1) + i++ (1) = 3
11499                assert_eq!(metric.loc.lloc(), 3);
11500            },
11501        );
11502    }
11503
11504    #[cfg(feature = "groovy")]
11505    #[test]
11506    fn groovy_continue_lloc() {
11507        // `continue` is an LLOC. Same gating as `java_continue_lloc`.
11508        check_metrics::<GroovyParser>(
11509            "for (int i = 0; i < 10; i++) {
11510                if (i == 5) {
11511                    continue
11512                }
11513                println(i)
11514            }",
11515            "foo.groovy",
11516            |metric| {
11517                // for(1) + if(1) + continue(1) + println(1) = 4
11518                assert_eq!(metric.loc.lloc(), 4);
11519            },
11520        );
11521    }
11522
11523    #[cfg(feature = "groovy")]
11524    #[test]
11525    fn groovy_expressions_lloc() {
11526        // A bag of expression statements: each independent
11527        // expr-stmt is one LLOC. Mirrors `java_expressions_lloc`.
11528        check_metrics::<GroovyParser>(
11529            "int a = 1
11530            a = 2
11531            a += 3
11532            println(a)
11533            doSomething()",
11534            "foo.groovy",
11535            |metric| {
11536                // 5 expression-statement lines.
11537                assert_eq!(metric.loc.lloc(), 5);
11538            },
11539        );
11540    }
11541
11542    #[cfg(feature = "groovy")]
11543    #[test]
11544    fn groovy_throw_lloc() {
11545        // `throw` is one LLOC via the `ThrowStatement` arm.
11546        check_metrics::<GroovyParser>(
11547            "throw new RuntimeException('bad')",
11548            "foo.groovy",
11549            |metric| {
11550                assert_eq!(metric.loc.lloc(), 1);
11551            },
11552        );
11553    }
11554
11555    #[cfg(feature = "groovy")]
11556    #[test]
11557    fn groovy_general_loc() {
11558        // Comprehensive mix: class + method + control flow.
11559        // Mirrors `java_general_loc`'s coverage shape.
11560        //
11561        // LLOC = 4, fully attributable:
11562        //   IfStatement (the outer if/else):     +1
11563        //   `println(x)`     (JuxtFunctionCall):  +1
11564        //   `println 'neg'` (JuxtFunctionCall):  +1
11565        //   `return`        (ReturnStatement):   +1
11566        // The else-branch's `expression_statement (closure)`
11567        // wrapper does NOT count — see the bare-Closure carve-out
11568        // in `impl Loc for GroovyCode::compute`.
11569        check_metrics::<GroovyParser>(
11570            "class A {
11571                void f(int x) {
11572                    if (x > 0) {
11573                        println(x)
11574                    } else {
11575                        println 'neg'
11576                    }
11577                    return
11578                }
11579            }",
11580            "foo.groovy",
11581            |metric| {
11582                assert_eq!(metric.loc.lloc(), 4);
11583                assert_eq!(metric.loc.cloc(), 0);
11584            },
11585        );
11586    }
11587
11588    #[cfg(feature = "csharp")]
11589    #[test]
11590    fn csharp_local_function_in_method_lloc() {
11591        // C# local functions (`int Inner(int x) { ... }` inside `Bar()`)
11592        // open their own function space, so the outer method sees only
11593        // `return Inner(2);` plus the inner body's `return x + 1;`.
11594        check_metrics::<CsharpParser>(
11595            "class Foo {\n    int Bar() {\n        int Inner(int x) { return x + 1; }\n        return Inner(2);\n    }\n}\n",
11596            "foo.cs",
11597            |metric| {
11598                assert_eq!(metric.loc.sloc(), 6);
11599                assert_eq!(metric.loc.ploc(), 6);
11600                assert_eq!(metric.loc.lloc(), 2);
11601                assert_eq!(metric.loc.cloc(), 0);
11602                assert_eq!(metric.loc.blank(), 0);
11603                insta::assert_json_snapshot!(metric.loc);
11604            },
11605        );
11606    }
11607
11608    #[cfg(feature = "csharp")]
11609    #[test]
11610    fn csharp_lambda_in_method_lloc() {
11611        // C# lambdas are expressions: the two LLOC come from the
11612        // `Func<int,int> f = x => x + 1;` declaration and the `f(3);` call.
11613        check_metrics::<CsharpParser>(
11614            "class Foo {\n    void Bar() {\n        System.Func<int, int> f = x => x + 1;\n        f(3);\n    }\n}\n",
11615            "foo.cs",
11616            |metric| {
11617                assert_eq!(metric.loc.sloc(), 6);
11618                assert_eq!(metric.loc.ploc(), 6);
11619                assert_eq!(metric.loc.lloc(), 2);
11620                assert_eq!(metric.loc.cloc(), 0);
11621                assert_eq!(metric.loc.blank(), 0);
11622                insta::assert_json_snapshot!(metric.loc);
11623            },
11624        );
11625    }
11626
11627    #[cfg(feature = "cpp")]
11628    #[test]
11629    fn cpp_lambda_in_function_lloc() {
11630        // C++11 lambdas are expressions. The outer function `bar()` produces
11631        // two LLOC for the body: `auto f = [](int x) { return x + 1; };` and
11632        // `return f(2);`. The lambda's inner `return x + 1;` is part of the
11633        // lambda body inside the same function space (lambdas do not open a
11634        // new FuncSpace in this implementation), so it adds a third LLOC.
11635        // Closes the parity gap with #195 (which covered 11 other
11636        // languages but omitted C++).
11637        check_metrics::<CppParser>(
11638            "int bar() {\n    auto f = [](int x) { return x + 1; };\n    return f(2);\n}\n",
11639            "foo.cpp",
11640            |metric| {
11641                assert_eq!(metric.loc.sloc(), 4);
11642                assert_eq!(metric.loc.ploc(), 4);
11643                assert_eq!(metric.loc.lloc(), 3);
11644                assert_eq!(metric.loc.cloc(), 0);
11645                assert_eq!(metric.loc.blank(), 0);
11646                insta::assert_json_snapshot!(metric.loc);
11647            },
11648        );
11649    }
11650
11651    #[cfg(feature = "javascript")]
11652    #[test]
11653    fn javascript_nested_function_lloc() {
11654        // Nested function_declaration: 4 LLOC = outer's `return inner();`,
11655        // inner's `return 1;`, plus the two function declarations
11656        // themselves (the JS Checker counts function declarations as LLOC).
11657        check_metrics::<JavascriptParser>(
11658            "function outer() {\n    function inner() {\n        return 1;\n    }\n    return inner();\n}\n",
11659            "foo.js",
11660            |metric| {
11661                assert_eq!(metric.loc.sloc(), 6);
11662                assert_eq!(metric.loc.ploc(), 6);
11663                assert_eq!(metric.loc.lloc(), 2);
11664                assert_eq!(metric.loc.cloc(), 0);
11665                assert_eq!(metric.loc.blank(), 0);
11666                insta::assert_json_snapshot!(metric.loc);
11667            },
11668        );
11669    }
11670
11671    #[cfg(feature = "javascript")]
11672    #[test]
11673    fn javascript_arrow_function_lloc() {
11674        // The arrow function `(x) => x + 1` is an expression: the LLOC
11675        // come from `const inner = ...;` and `return inner(2);`.
11676        check_metrics::<JavascriptParser>(
11677            "function outer() {\n    const inner = (x) => x + 1;\n    return inner(2);\n}\n",
11678            "foo.js",
11679            |metric| {
11680                assert_eq!(metric.loc.sloc(), 4);
11681                assert_eq!(metric.loc.ploc(), 4);
11682                assert_eq!(metric.loc.lloc(), 2);
11683                assert_eq!(metric.loc.cloc(), 0);
11684                assert_eq!(metric.loc.blank(), 0);
11685                insta::assert_json_snapshot!(metric.loc);
11686            },
11687        );
11688    }
11689
11690    #[cfg(feature = "kotlin")]
11691    #[test]
11692    fn kotlin_lambda_literal_in_fun_lloc() {
11693        // A lambda literal (`{ x -> x + 1 }`) assigned to a `val` plus the
11694        // following call yields two LLOC at the outer function.
11695        check_metrics::<KotlinParser>(
11696            "fun outer() {\n    val f: (Int) -> Int = { x -> x + 1 }\n    f(3)\n}\n",
11697            "foo.kt",
11698            |metric| {
11699                assert_eq!(metric.loc.sloc(), 4);
11700                assert_eq!(metric.loc.ploc(), 4);
11701                assert_eq!(metric.loc.lloc(), 2);
11702                assert_eq!(metric.loc.cloc(), 0);
11703                assert_eq!(metric.loc.blank(), 0);
11704                insta::assert_json_snapshot!(metric.loc);
11705            },
11706        );
11707    }
11708
11709    #[cfg(feature = "kotlin")]
11710    #[test]
11711    fn kotlin_local_fun_in_fun_lloc() {
11712        // Kotlin's local `fun inner(...)` is also a function_declaration,
11713        // so it opens its own space; the outer LLOC reduces to `inner(3)`,
11714        // and the inner body contributes the second LLOC.
11715        check_metrics::<KotlinParser>(
11716            "fun outer() {\n    fun inner(x: Int): Int { return x + 1 }\n    inner(3)\n}\n",
11717            "foo.kt",
11718            |metric| {
11719                assert_eq!(metric.loc.sloc(), 4);
11720                assert_eq!(metric.loc.ploc(), 4);
11721                assert_eq!(metric.loc.lloc(), 2);
11722                assert_eq!(metric.loc.cloc(), 0);
11723                assert_eq!(metric.loc.blank(), 0);
11724                insta::assert_json_snapshot!(metric.loc);
11725            },
11726        );
11727    }
11728
11729    #[cfg(feature = "kotlin")]
11730    #[test]
11731    fn kotlin_object_expression_in_fun_lloc() {
11732        // An `object : Runnable { ... }` expression with an overridden
11733        // method whose body invokes `println("hi")`. LLOC: `val r = ...`,
11734        // the override's body call, and the outer `r.run()` call = 3.
11735        check_metrics::<KotlinParser>(
11736            "fun outer() {\n    val r = object : Runnable { override fun run() { println(\"hi\") } }\n    r.run()\n}\n",
11737            "foo.kt",
11738            |metric| {
11739                assert_eq!(metric.loc.sloc(), 4);
11740                assert_eq!(metric.loc.ploc(), 4);
11741                assert_eq!(metric.loc.lloc(), 3);
11742                assert_eq!(metric.loc.cloc(), 0);
11743                assert_eq!(metric.loc.blank(), 0);
11744                insta::assert_json_snapshot!(metric.loc);
11745            },
11746        );
11747    }
11748
11749    #[cfg(feature = "go")]
11750    #[test]
11751    fn go_function_literal_initializer_lloc() {
11752        // `inner := func(x int) int { return x + 1 }` — the function
11753        // literal opens its own space; LLOC visible on the outer space:
11754        // the assignment + `return inner(2)` = 2, plus the literal's
11755        // `return x + 1` body = 3 aggregated.
11756        check_metrics::<GoParser>(
11757            "package main\nfunc outer() int {\n    inner := func(x int) int { return x + 1 }\n    return inner(2)\n}\n",
11758            "foo.go",
11759            |metric| {
11760                assert_eq!(metric.loc.sloc(), 5);
11761                assert_eq!(metric.loc.ploc(), 5);
11762                assert_eq!(metric.loc.lloc(), 3);
11763                assert_eq!(metric.loc.cloc(), 0);
11764                assert_eq!(metric.loc.blank(), 0);
11765                insta::assert_json_snapshot!(metric.loc);
11766            },
11767        );
11768    }
11769
11770    #[cfg(feature = "php")]
11771    #[test]
11772    fn php_anonymous_function_in_function_lloc() {
11773        // Anonymous function `function ($x) { return $x + 1; }`: outer
11774        // sees the assignment + `return $f(2);`, the closure body adds
11775        // `return $x + 1;` for 3 LLOC aggregated.
11776        check_metrics::<PhpParser>(
11777            "<?php\nfunction outer() {\n    $f = function ($x) { return $x + 1; };\n    return $f(2);\n}\n",
11778            "foo.php",
11779            |metric| {
11780                assert_eq!(metric.loc.sloc(), 5);
11781                assert_eq!(metric.loc.ploc(), 5);
11782                assert_eq!(metric.loc.lloc(), 3);
11783                assert_eq!(metric.loc.cloc(), 0);
11784                assert_eq!(metric.loc.blank(), 0);
11785                insta::assert_json_snapshot!(metric.loc);
11786            },
11787        );
11788    }
11789
11790    #[cfg(feature = "php")]
11791    #[test]
11792    fn php_arrow_function_in_function_lloc() {
11793        // The `fn ($x) => $x + 1` arrow function is an expression; the
11794        // outer function sees only its assignment and the `return $f(2);`.
11795        check_metrics::<PhpParser>(
11796            "<?php\nfunction outer() {\n    $f = fn ($x) => $x + 1;\n    return $f(2);\n}\n",
11797            "foo.php",
11798            |metric| {
11799                assert_eq!(metric.loc.sloc(), 5);
11800                assert_eq!(metric.loc.ploc(), 5);
11801                assert_eq!(metric.loc.lloc(), 2);
11802                assert_eq!(metric.loc.cloc(), 0);
11803                assert_eq!(metric.loc.blank(), 0);
11804                insta::assert_json_snapshot!(metric.loc);
11805            },
11806        );
11807    }
11808
11809    #[cfg(feature = "lua")]
11810    #[test]
11811    fn lua_nested_local_function_lloc() {
11812        // Two nested `local function` declarations: outer + inner both
11813        // count as `function_declaration` LLOC, plus the two `return`
11814        // statements = 4 aggregated.
11815        check_metrics::<LuaParser>(
11816            "local function outer()\n    local function inner()\n        return 1\n    end\n    return inner()\nend\n",
11817            "foo.lua",
11818            |metric| {
11819                assert_eq!(metric.loc.sloc(), 6);
11820                assert_eq!(metric.loc.ploc(), 6);
11821                assert_eq!(metric.loc.lloc(), 4);
11822                assert_eq!(metric.loc.cloc(), 0);
11823                assert_eq!(metric.loc.blank(), 0);
11824                insta::assert_json_snapshot!(metric.loc);
11825            },
11826        );
11827    }
11828
11829    #[cfg(feature = "lua")]
11830    #[test]
11831    fn lua_function_expression_in_local_decl_lloc() {
11832        // `local f = function (x) return x + 1 end` — the function
11833        // expression is its own space; aggregated LLOC: outer
11834        // declaration, the inner expression's declaration, the inner
11835        // `return x + 1`, and the outer `return f(2)` = 4.
11836        check_metrics::<LuaParser>(
11837            "local function outer()\n    local f = function (x) return x + 1 end\n    return f(2)\nend\n",
11838            "foo.lua",
11839            |metric| {
11840                assert_eq!(metric.loc.sloc(), 4);
11841                assert_eq!(metric.loc.ploc(), 4);
11842                assert_eq!(metric.loc.lloc(), 4);
11843                assert_eq!(metric.loc.cloc(), 0);
11844                assert_eq!(metric.loc.blank(), 0);
11845                insta::assert_json_snapshot!(metric.loc);
11846            },
11847        );
11848    }
11849
11850    #[cfg(feature = "tcl")]
11851    #[test]
11852    fn tcl_apply_closure_lloc() {
11853        // `apply $f 2` is a regular Tcl command, not a separate function
11854        // space — tree-sitter-tcl does not model `apply { ... }` as a
11855        // closure construct distinct from any other command. We assert
11856        // the observed LLOC (proc, set, apply, plus the nested `expr`
11857        // command substitution inside the lambda body) so any future
11858        // change to lambda-body counting is caught here.
11859        check_metrics::<TclParser>(
11860            "proc outer {} {\n    set f [list x {return [expr {$x + 1}]}]\n    apply $f 2\n}\n",
11861            "foo.tcl",
11862            |metric| {
11863                assert_eq!(metric.loc.sloc(), 4);
11864                assert_eq!(metric.loc.ploc(), 4);
11865                assert_eq!(metric.loc.lloc(), 4);
11866                assert_eq!(metric.loc.cloc(), 0);
11867                assert_eq!(metric.loc.blank(), 0);
11868                insta::assert_json_snapshot!(metric.loc);
11869            },
11870        );
11871    }
11872
11873    #[cfg(feature = "perl")]
11874    #[test]
11875    fn perl_anonymous_sub_in_sub_lloc() {
11876        // Anonymous sub `sub { ... }` opens its own function space; the
11877        // outer LLOC counts the `my $f = ...;` declaration plus
11878        // `return $f->(2);`, and the anonymous sub contributes
11879        // `return $_[0] + 1;` for 2 LLOC.
11880        //
11881        // NOTE: a prior LLOC for this construct exists as
11882        // `perl_lloc_anonymous_function` (top-level form) — this test
11883        // asserts the same shape *inside* another sub, exercising space
11884        // nesting.
11885        check_metrics::<PerlParser>(
11886            "sub outer {\n    my $f = sub { return $_[0] + 1 };\n    return $f->(2);\n}\n",
11887            "foo.pl",
11888            |metric| {
11889                assert_eq!(metric.loc.sloc(), 4);
11890                assert_eq!(metric.loc.ploc(), 4);
11891                assert_eq!(metric.loc.lloc(), 2);
11892                assert_eq!(metric.loc.cloc(), 0);
11893                assert_eq!(metric.loc.blank(), 0);
11894                insta::assert_json_snapshot!(metric.loc);
11895            },
11896        );
11897    }
11898
11899    #[cfg(feature = "perl")]
11900    #[test]
11901    fn perl_named_sub_in_sub_lloc() {
11902        // Perl `sub` declarations are not LLOC (see
11903        // `perl_lloc_function_definition_not_counted`); inside `outer`,
11904        // only `return inner();` is LLOC, and `inner`'s `return 1` is in
11905        // its own space contributing one more aggregated LLOC.
11906        // Total aggregated LLOC: 1.
11907        //
11908        // Observation: lloc=1, not 2. Perl LLOC is anchored on `;`
11909        // tokens whose parent is `SourceFile` or `Block` (see
11910        // `PerlCode::compute` in this file). The bare `return 1` inside
11911        // `sub inner { ... }` has no trailing `;`, so it does not bump
11912        // LLOC. The outer `return inner();` carries the only SEMI.
11913        // This is intentional Perl behaviour and not a bug — Perl
11914        // requires `;` between statements; a single trailing statement
11915        // before `}` is syntactically optional. Asserted as-is.
11916        check_metrics::<PerlParser>(
11917            "sub outer {\n    sub inner { return 1 }\n    return inner();\n}\n",
11918            "foo.pl",
11919            |metric| {
11920                assert_eq!(metric.loc.sloc(), 4);
11921                assert_eq!(metric.loc.ploc(), 4);
11922                assert_eq!(metric.loc.lloc(), 1);
11923                assert_eq!(metric.loc.cloc(), 0);
11924                assert_eq!(metric.loc.blank(), 0);
11925                insta::assert_json_snapshot!(metric.loc);
11926            },
11927        );
11928    }
11929
11930    #[cfg(feature = "elixir")]
11931    #[test]
11932    fn elixir_fn_inside_def_lloc() {
11933        // `fn x -> x + 1 end` inside a `def`: defmodule + def +
11934        // `f = fn ...` + `f.(2)` = 4 own LLOC for the Unit space, plus
11935        // the anonymous fn body `x + 1` = 1 nested, aggregated 5.
11936        check_metrics::<ElixirParser>(
11937            "defmodule Foo do\n  def outer do\n    f = fn x -> x + 1 end\n    f.(2)\n  end\nend\n",
11938            "foo.ex",
11939            |metric| {
11940                assert_eq!(metric.loc.sloc(), 6);
11941                assert_eq!(metric.loc.ploc(), 6);
11942                assert_eq!(metric.loc.lloc(), 5);
11943                assert_eq!(metric.loc.cloc(), 0);
11944                assert_eq!(metric.loc.blank(), 0);
11945                insta::assert_json_snapshot!(metric.loc);
11946            },
11947        );
11948    }
11949
11950    /// Regression for #437: `Loc` min/max must fold each nested function
11951    /// space's *own* min/max, not its aggregate value, so the smallest
11952    /// (and largest) leaf function propagates to the root. Before the fix,
11953    /// merge folded `other.sloc()` and a guarded `compute_minmax` skipped
11954    /// containers, so `sloc_min` reflected only top-level spaces.
11955    ///
11956    /// Layout: Unit (whole file) -> class C -> two methods of *different*
11957    /// sizes. The smaller method is the global minimum; the file/class
11958    /// spans are larger; `sloc_min` must be the small leaf, not the class
11959    /// or unit span. Verified against the pre-fix code by reverting the
11960    /// merge/compute_minmax change (it reports the unit span instead).
11961    #[cfg(feature = "rust")]
11962    #[test]
11963    fn rust_nested_min_max_propagates() {
11964        check_metrics::<RustParser>(
11965            "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",
11966            "c.rs",
11967            |metric| {
11968                // Spaces: Unit + impl C + small() + big().
11969                // small() spans 3 rows (signature .. closing brace),
11970                // big() spans 5 rows. The Unit/impl spans are larger
11971                // still. sloc_min must be the smallest leaf (3), not the
11972                // top-level span; sloc_max must be the largest space.
11973                let loc = &metric.loc;
11974                assert!(
11975                    loc.sloc_min() <= loc.sloc(),
11976                    "sloc_min {} must not exceed unit sloc {}",
11977                    loc.sloc_min(),
11978                    loc.sloc()
11979                );
11980                assert_eq!(loc.sloc_min(), 3, "smallest leaf method span");
11981                assert_eq!(loc.sloc_max(), loc.sloc(), "largest space is the unit");
11982                // The smallest leaf has one statement; min must reflect it.
11983                assert_eq!(loc.lloc_min(), 1);
11984            },
11985        );
11986    }
11987
11988    /// Java sibling of `rust_nested_min_max_propagates` (#437). The
11989    /// `java_class_loc` snapshot above showed the bug directly: a class
11990    /// with methods reported `sloc_min == sloc` (the unit span). Here we
11991    /// assert the smallest method propagates.
11992    #[cfg(feature = "java")]
11993    #[test]
11994    fn java_nested_min_max_propagates() {
11995        check_metrics::<JavaParser>(
11996            "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",
11997            "C.java",
11998            |metric| {
11999                // Spaces: Unit + class C + small() + big().
12000                let loc = &metric.loc;
12001                assert!(loc.sloc_min() <= loc.sloc());
12002                assert_eq!(loc.sloc_min(), 3, "smallest leaf method span");
12003                assert_eq!(loc.sloc_max(), loc.sloc());
12004                assert_eq!(loc.lloc_min(), 1);
12005            },
12006        );
12007    }
12008
12009    /// Python sibling of `rust_nested_min_max_propagates` (#437). Python
12010    /// nesting is class -> method, mirroring the worked example in the
12011    /// issue (file -> class C -> method m).
12012    #[cfg(feature = "python")]
12013    #[test]
12014    fn python_nested_min_max_propagates() {
12015        check_metrics::<PythonParser>(
12016            "class C:\n    def small(self):\n        x = 1\n    def big(self):\n        a = 1\n        b = 2\n        c = 3\n",
12017            "c.py",
12018            |metric| {
12019                // Spaces: Unit + class C + small() + big().
12020                let loc = &metric.loc;
12021                assert!(loc.sloc_min() <= loc.sloc());
12022                assert_eq!(loc.sloc_min(), 2, "smallest leaf method span");
12023                assert_eq!(loc.sloc_max(), loc.sloc());
12024                assert_eq!(loc.lloc_min(), 1);
12025            },
12026        );
12027    }
12028
12029    /// `blank()` is `sloc - ploc - only_comment_lines`, an f64 subtraction
12030    /// that can go negative when a space's physical and comment line
12031    /// attribution overlaps its span row count. It must clamp at 0 so the
12032    /// serialized value is never negative (#437).
12033    ///
12034    /// Built from a synthetic [`Stats`] rather than a parsed fixture: the
12035    /// current grammars do not emit a parsed space whose root `sloc` is
12036    /// smaller than `ploc + only_comment_lines`, so a source fixture cannot
12037    /// drive the subtraction negative and would pass against the pre-clamp
12038    /// code (proving nothing). Setting `sloc = ploc = only_comment_lines = 1`
12039    /// yields a pre-clamp `blank` of `1 - 1 - 1 = -1`; reverting the
12040    /// `.max(0.0)` makes this assertion fail with `-1`.
12041    #[test]
12042    fn blank_clamps_negative_to_zero() {
12043        let mut stats = Stats::default();
12044        // A single-row span ending mid-line => sloc() of 1 row.
12045        stats.sloc.start = 0;
12046        // End row 0 ending mid-line, so that row counts: sloc() == 1.
12047        stats.sloc.end_line = 1;
12048        // ploc() is the cardinality of the physical-line set => 1.
12049        stats.ploc.lines.insert(0);
12050        // One comment-only line on the same single row.
12051        stats.cloc.only_comment_line_starts.insert(0);
12052
12053        // Pre-clamp this is 1 - 1 - 1 = -1.
12054        assert_eq!(stats.sloc(), 1);
12055        assert_eq!(stats.ploc(), 1);
12056        assert_eq!(
12057            stats.blank(),
12058            0,
12059            "blank() must clamp the negative subtraction to 0"
12060        );
12061    }
12062
12063    /// A physical line shared by two *sibling* function spaces must
12064    /// count once in the parent that merges them.
12065    ///
12066    /// This is the semantics `Ploc::merge` / `Cloc::merge` encode, and
12067    /// the reason the per-space line stores are sets rather than
12068    /// counters. It is asserted here across four language families —
12069    /// Rust, the C family, the JS family, and Python's indentation-based
12070    /// spaces — because the merge is shared by all of them and each
12071    /// family reaches it through a different `Loc::compute` body.
12072    ///
12073    /// Every fixture puts both spaces on one row, so a merge that summed
12074    /// instead of unioning would report `ploc`/`cloc` of 2 against an
12075    /// `sloc` of 1 — an impossible reading that also drives `blank`
12076    /// negative.
12077    #[cfg(all(
12078        feature = "cpp",
12079        feature = "javascript",
12080        feature = "python",
12081        feature = "rust"
12082    ))]
12083    #[test]
12084    fn sibling_spaces_sharing_a_line_count_it_once() {
12085        check_metrics::<RustParser>(
12086            "fn a() { let x = 1; } fn b() { let y = 2; }",
12087            "foo.rs",
12088            |metric| {
12089                assert_eq!(metric.loc.sloc(), 1);
12090                assert_eq!(metric.loc.ploc(), 1, "one physical row, two spaces");
12091                assert_eq!(metric.loc.blank(), 0);
12092            },
12093        );
12094
12095        check_metrics::<CppParser>(
12096            "int a() { return 1; } int b() { return 2; }",
12097            "foo.cpp",
12098            |metric| {
12099                assert_eq!(metric.loc.sloc(), 1);
12100                assert_eq!(metric.loc.ploc(), 1, "one physical row, two spaces");
12101                assert_eq!(metric.loc.blank(), 0);
12102            },
12103        );
12104
12105        check_metrics::<JavascriptParser>(
12106            "function a() { let x = 1; } function b() { let y = 2; }",
12107            "foo.js",
12108            |metric| {
12109                assert_eq!(metric.loc.sloc(), 1);
12110                assert_eq!(metric.loc.ploc(), 1, "one physical row, two spaces");
12111                assert_eq!(metric.loc.blank(), 0);
12112            },
12113        );
12114
12115        // Python cannot open two spaces on one row, so the shared row is
12116        // the nested `def`'s: it belongs to the inner space's span and to
12117        // the outer space's, and the unit merges both.
12118        check_metrics::<PythonParser>(
12119            "def a():
12120    def b(): return 1",
12121            "foo.py",
12122            |metric| {
12123                assert_eq!(metric.loc.sloc(), 2);
12124                assert_eq!(metric.loc.ploc(), 2, "two physical rows, three spaces");
12125                assert_eq!(metric.loc.blank(), 0);
12126            },
12127        );
12128    }
12129
12130    /// The same union property for comment lines: two sibling spaces that
12131    /// each carry a comment on the one shared row must yield `cloc == 1`.
12132    ///
12133    /// Summing rather than unioning would report `cloc == 2` against
12134    /// `sloc == 1`, the `cloc > sloc` state that pushes MI's comment
12135    /// ratio above 1 (the failure mode of issue #461,
12136    /// here across the space merge rather than within one space).
12137    #[cfg(all(feature = "cpp", feature = "rust"))]
12138    #[test]
12139    fn sibling_spaces_sharing_a_comment_line_count_it_once() {
12140        check_metrics::<CppParser>(
12141            "int a() { /*x*/ return 1; } int b() { /*y*/ return 2; }",
12142            "foo.cpp",
12143            |metric| {
12144                assert_eq!(metric.loc.sloc(), 1);
12145                assert_eq!(metric.loc.cloc(), 1, "one comment row, two spaces");
12146                assert!(
12147                    metric.loc.cloc() <= metric.loc.sloc(),
12148                    "cloc must never exceed sloc"
12149                );
12150            },
12151        );
12152
12153        check_metrics::<RustParser>(
12154            "fn a() { /*x*/ let p = 1; } fn b() { /*y*/ let q = 2; }",
12155            "foo.rs",
12156            |metric| {
12157                assert_eq!(metric.loc.sloc(), 1);
12158                assert_eq!(metric.loc.cloc(), 1, "one comment row, two spaces");
12159                assert!(
12160                    metric.loc.cloc() <= metric.loc.sloc(),
12161                    "cloc must never exceed sloc"
12162                );
12163            },
12164        );
12165    }
12166
12167    /// A row inside a chain of nested spaces is folded upward once per
12168    /// level, and must still count once at the top.
12169    ///
12170    /// Fifteen levels rather than two: at one level a union and a sum
12171    /// agree whenever the sets happen to be disjoint, and the point of
12172    /// #1109 is the repeated fold. The body row belongs to every level's
12173    /// span, so a merge that accumulated would report `ploc == 15`.
12174    #[cfg(feature = "rust")]
12175    #[test]
12176    fn a_row_folded_through_nested_spaces_counts_once() {
12177        const DEPTH: usize = 15;
12178        let source = format!(
12179            "{}let x = 1;{}",
12180            "fn f() { ".repeat(DEPTH),
12181            "} ".repeat(DEPTH)
12182        );
12183
12184        check_metrics::<RustParser>(&source, "foo.rs", |metric| {
12185            assert_eq!(metric.loc.sloc(), 1);
12186            assert_eq!(metric.loc.ploc(), 1, "one physical row, {DEPTH} spaces");
12187            assert_eq!(metric.loc.ploc_max(), 1);
12188            assert_eq!(metric.loc.blank(), 0);
12189        });
12190    }
12191
12192    /// Two inline block comments on a single code line must count as a
12193    /// single comment line, not one per comment node. Pre-fix this
12194    /// reported `cloc = 2` (one increment per node) for a one-line
12195    /// construct, violating `cloc <= sloc`/`cloc <= ploc` and pushing
12196    /// the MI comment ratio above 1 (issue #461). Reverting
12197    /// the per-line de-dup in `add_code_comment_line` makes the
12198    /// `cloc == 1` assertions fail with `2`.
12199    #[cfg(feature = "cpp")]
12200    #[test]
12201    fn cloc_multiple_block_comments_one_line_cpp() {
12202        check_metrics::<CppParser>(
12203            "int f(int /*a*/, int /*b*/) { return 1; }",
12204            "foo.cpp",
12205            |metric| {
12206                assert_eq!(metric.loc.cloc(), 1, "two inline comments => 1 cloc");
12207                assert!(
12208                    metric.loc.cloc() <= metric.loc.sloc(),
12209                    "cloc must not exceed sloc"
12210                );
12211                assert!(
12212                    metric.loc.cloc() <= metric.loc.ploc(),
12213                    "cloc must not exceed ploc for a single-line construct"
12214                );
12215            },
12216        );
12217    }
12218
12219    /// Sibling-language coverage: `add_cloc_lines` is shared across
12220    /// every block-comment language, so the Rust path must behave
12221    /// identically to C++ (issue #461).
12222    #[cfg(feature = "rust")]
12223    #[test]
12224    fn cloc_multiple_block_comments_one_line_rust() {
12225        check_metrics::<RustParser>(
12226            "fn f(/*a*/ x: i32, /*b*/ y: i32) -> i32 { 1 }",
12227            "foo.rs",
12228            |metric| {
12229                assert_eq!(metric.loc.cloc(), 1, "two inline comments => 1 cloc");
12230                assert!(
12231                    metric.loc.cloc() <= metric.loc.sloc(),
12232                    "cloc must not exceed sloc"
12233                );
12234            },
12235        );
12236    }
12237
12238    /// Guard against over-de-dup: a single multi-line block comment
12239    /// must still contribute one comment line per physical line it
12240    /// spans. The de-dup keys on the start row only, so the three
12241    /// independent continuation lines are unaffected (issue #461).
12242    #[cfg(feature = "cpp")]
12243    #[test]
12244    fn cloc_multiline_block_comment_counts_each_line() {
12245        check_metrics::<CppParser>(
12246            "int g() {\n  /* l1\n     l2\n     l3 */\n  return 0;\n}",
12247            "foo.cpp",
12248            |metric| {
12249                assert_eq!(
12250                    metric.loc.cloc(),
12251                    3,
12252                    "a 3-line block comment counts 3 comment lines"
12253                );
12254            },
12255        );
12256    }
12257
12258    /// Two *standalone* block comments on a single physical line (no
12259    /// code) must count as one comment line, not one per node. #461
12260    /// deduped only inline co-located comments via the code-comment
12261    /// path; the standalone path bumped `only_comment_lines` per node,
12262    /// so `/*a*/ /*b*/` reported `cloc = 2` for a single line —
12263    /// violating `cloc <= sloc`. Reverting the per-line set in
12264    /// `add_only_comment_lines` makes the `cloc == 1` assertion fail
12265    /// with `2` (verified by reverting to `only_comment_lines += …`).
12266    #[cfg(feature = "cpp")]
12267    #[test]
12268    fn cloc_multiple_standalone_block_comments_one_line_cpp() {
12269        check_metrics::<CppParser>("/*a*/ /*b*/", "foo.cpp", |metric| {
12270            assert_eq!(
12271                metric.loc.cloc(),
12272                1,
12273                "two standalone comments on one line => 1 cloc"
12274            );
12275            assert_eq!(metric.loc.sloc(), 1);
12276            assert!(
12277                metric.loc.cloc() <= metric.loc.sloc(),
12278                "cloc must not exceed sloc"
12279            );
12280        });
12281    }
12282
12283    /// Sibling-language coverage: the standalone de-dup lives in the
12284    /// shared `add_only_comment_lines` helper, so Rust must match C++
12285    /// (issue #461 follow-up).
12286    #[cfg(feature = "rust")]
12287    #[test]
12288    fn cloc_multiple_standalone_block_comments_one_line_rust() {
12289        check_metrics::<RustParser>("/*a*/ /*b*/", "foo.rs", |metric| {
12290            assert_eq!(
12291                metric.loc.cloc(),
12292                1,
12293                "two standalone comments on one line => 1 cloc"
12294            );
12295            assert_eq!(metric.loc.sloc(), 1);
12296            assert!(
12297                metric.loc.cloc() <= metric.loc.sloc(),
12298                "cloc must not exceed sloc"
12299            );
12300        });
12301    }
12302
12303    /// File-level guard: a comment-only physical line followed by a
12304    /// real code line still counts the comment line once and never
12305    /// exceeds sloc, even after the per-space merge that previously
12306    /// summed `code_comment_lines` (and double-counted a boundary line)
12307    /// rather than reading the per-line set. Three standalone comments
12308    /// share line 1, so the whole file has exactly one comment line.
12309    #[cfg(feature = "cpp")]
12310    #[test]
12311    fn cloc_standalone_comments_then_code_no_double_count() {
12312        check_metrics::<CppParser>("/*a*/ /*b*/ /*c*/\nint x = 1;\n", "foo.cpp", |metric| {
12313            assert_eq!(metric.loc.sloc(), 2);
12314            assert_eq!(
12315                metric.loc.cloc(),
12316                1,
12317                "only line 1 carries comments => 1 cloc"
12318            );
12319            assert!(
12320                metric.loc.cloc() <= metric.loc.sloc(),
12321                "cloc must not exceed sloc"
12322            );
12323        });
12324    }
12325
12326    #[cfg(feature = "irules")]
12327    #[test]
12328    fn irules_multiline_quoted_word_credits_every_row_to_ploc() {
12329        // Regression test for #1260, the dialect half of the Tcl fix: a
12330        // `quoted_word` carries no child per row, so its interior rows
12331        // reached neither PLOC nor CLOC and
12332        // `blank = sloc - ploc - cloc` claimed them.
12333        //
12334        // expected: 6 rows — `when HTTP_REQUEST {`,
12335        // `    set msg "line one`, `line two`, `line three"`,
12336        // `    log local0. $msg`, `}` — all code, none blank. lloc 3 for
12337        // the handler, the `set` and the `log` command.
12338        check_metrics::<IrulesParser>(
12339            "when HTTP_REQUEST {\n    set msg \"line one\nline two\nline three\"\n    log local0. $msg\n}",
12340            "foo.irule",
12341            |metric| {
12342                assert_eq!(metric.loc.sloc(), 6);
12343                assert_eq!(metric.loc.ploc(), 6);
12344                assert_eq!(metric.loc.lloc(), 3);
12345                assert_eq!(metric.loc.cloc(), 0);
12346                assert_eq!(metric.loc.blank(), 0);
12347            },
12348        );
12349
12350        // expected: 3 rows whose middle row is empty *inside* the literal —
12351        // `set msg "one`, ``, `three"` — code, not blank.
12352        check_metrics::<IrulesParser>("set msg \"one\n\nthree\"", "foo.irule", |metric| {
12353            assert_eq!(metric.loc.sloc(), 3);
12354            assert_eq!(metric.loc.ploc(), 3);
12355            assert_eq!(metric.loc.cloc(), 0);
12356            assert_eq!(metric.loc.blank(), 0);
12357        });
12358    }
12359
12360    /// Interior blank lines are counted as BLANK and excluded from PLOC.
12361    /// sloc 6 (every line) / ploc 4 (4 code lines) / lloc 3 (handler + 2
12362    /// `set`s) / cloc 0 / blank 2.
12363    ///
12364    /// Also the dialect's half of the #1260 carve-out: the handler body is
12365    /// a `braced_word`, which the grammar parses as a script, so its blank
12366    /// rows stay blank — only `quoted_word` is routed to PLOC.
12367    #[cfg(feature = "irules")]
12368    #[test]
12369    fn irules_blank() {
12370        check_metrics::<IrulesParser>(
12371            "when X {\n\n    set x 1\n\n    set y 2\n}\n",
12372            "foo.irule",
12373            |metric| {
12374                assert_eq!(metric.loc.sloc(), 6);
12375                assert_eq!(metric.loc.ploc(), 4);
12376                assert_eq!(metric.loc.lloc(), 3);
12377                assert_eq!(metric.loc.cloc(), 0);
12378                assert_eq!(metric.loc.blank(), 2);
12379            },
12380        );
12381    }
12382
12383    /// A handler body with no blank lines reports zero BLANK. lloc 3 =
12384    /// handler + `set` + `log` command.
12385    #[cfg(feature = "irules")]
12386    #[test]
12387    fn irules_no_zero_blank() {
12388        check_metrics::<IrulesParser>(
12389            "when HTTP_REQUEST {\n    set x 1\n    log local0. $x\n}\n",
12390            "foo.irule",
12391            |metric| {
12392                assert_eq!(metric.loc.sloc(), 4);
12393                assert_eq!(metric.loc.ploc(), 4);
12394                assert_eq!(metric.loc.lloc(), 3);
12395                assert_eq!(metric.loc.blank(), 0);
12396            },
12397        );
12398    }
12399
12400    /// `#`-prefixed comment lines are counted as CLOC (iRules has no block
12401    /// comments, so each comment node spans exactly one line).
12402    ///
12403    /// expected: rows 0-2 are comment-only, row 3 is the sole code row.
12404    /// `ploc` and `sloc` are asserted, not just `cloc`/`blank`: without
12405    /// them this test passed all the way through #1135, which credited
12406    /// each of the three comment rows to PLOC as well (`ploc == 4`,
12407    /// `cloc + ploc == 7` against `sloc == 4`).
12408    #[cfg(feature = "irules")]
12409    #[test]
12410    fn irules_cloc() {
12411        check_metrics::<IrulesParser>(
12412            "# a\n# b\n# c\nwhen X { set x 1 }\n",
12413            "foo.irule",
12414            |metric| {
12415                assert_eq!(metric.loc.sloc(), 4);
12416                assert_eq!(metric.loc.ploc(), 1);
12417                assert_eq!(metric.loc.cloc(), 3);
12418                assert_eq!(metric.loc.blank(), 0);
12419            },
12420        );
12421    }
12422
12423    /// LLOC counts each statement once: handler header, `if`, `set`, and the
12424    /// generic `log` command = 4. The `switch_arm` headers are not counted
12425    /// (their bodies' commands are), verified in `irules_switch_lloc`.
12426    #[cfg(feature = "irules")]
12427    #[test]
12428    fn irules_lloc() {
12429        check_metrics::<IrulesParser>(
12430            "when X {\n    if { $a } {\n        set x 1\n    }\n    log local0. done\n}\n",
12431            "foo.irule",
12432            |metric| {
12433                assert_eq!(metric.loc.lloc(), 4);
12434            },
12435        );
12436    }
12437
12438    /// A command inside `[...]` (`command_substitution`) is a sub-expression,
12439    /// not a top-level statement, so it does not add to LLOC. Here lloc 2 =
12440    /// handler + `set`; the inner `expr` is NOT counted. Removing the
12441    /// `CommandSubstitution` guard would push lloc to 3 — this is the loc
12442    /// gating-decision regression test.
12443    #[cfg(feature = "irules")]
12444    #[test]
12445    fn irules_no_command_substitution_lloc() {
12446        check_metrics::<IrulesParser>(
12447            "when X {\n    set y [expr { 1 + 2 }]\n}\n",
12448            "foo.irule",
12449            |metric| {
12450                assert_eq!(metric.loc.lloc(), 2);
12451            },
12452        );
12453    }
12454
12455    /// `switch` counts once; each arm's *body* command counts, but the
12456    /// `switch_arm` pattern/body pair itself is not a logical line. lloc 4 =
12457    /// handler + `switch` + two `set`s (one per arm body).
12458    #[cfg(feature = "irules")]
12459    #[test]
12460    fn irules_switch_lloc() {
12461        check_metrics::<IrulesParser>(
12462            "when X {\n    switch $h {\n        a { set r 1 }\n        b { set r 2 }\n    }\n}\n",
12463            "foo.irule",
12464            |metric| {
12465                assert_eq!(metric.loc.lloc(), 4);
12466            },
12467        );
12468    }
12469
12470    /// A `proc` definition and its `return` command are each one logical
12471    /// line: lloc 2.
12472    #[cfg(feature = "irules")]
12473    #[test]
12474    fn irules_proc_lloc() {
12475        check_metrics::<IrulesParser>(
12476            "proc f { a } {\n    return $a\n}\n",
12477            "foo.irule",
12478            |metric| {
12479                assert_eq!(metric.loc.lloc(), 2);
12480            },
12481        );
12482    }
12483
12484    /// Objective-C blank-line accounting: two code lines separated by
12485    /// blank lines.
12486    #[cfg(feature = "objc")]
12487    #[test]
12488    fn objc_blank() {
12489        check_metrics::<ObjcParser>(
12490            "
12491
12492            int a = 42;
12493
12494            int b = 43;
12495
12496            ",
12497            "foo.m",
12498            |metric| {
12499                assert_eq!(metric.loc.blank(), 1);
12500                insta::assert_json_snapshot!(metric.loc, @r#"
12501                {
12502                  "sloc": 3,
12503                  "ploc": 2,
12504                  "lloc": 2,
12505                  "cloc": 0,
12506                  "blank": 1,
12507                  "dloc": 0,
12508                  "sloc_average": 3.0,
12509                  "ploc_average": 2.0,
12510                  "lloc_average": 2.0,
12511                  "cloc_average": 0.0,
12512                  "blank_average": 1.0,
12513                  "dloc_average": 0.0,
12514                  "sloc_min": 3,
12515                  "sloc_max": 3,
12516                  "cloc_min": 0,
12517                  "cloc_max": 0,
12518                  "ploc_min": 2,
12519                  "ploc_max": 2,
12520                  "lloc_min": 2,
12521                  "lloc_max": 2,
12522                  "blank_min": 1,
12523                  "blank_max": 1,
12524                  "dloc_min": 0,
12525                  "dloc_max": 0
12526                }
12527                "#);
12528            },
12529        );
12530    }
12531
12532    /// Objective-C comment accounting: a block comment and a line
12533    /// comment each contribute to `cloc`.
12534    #[cfg(feature = "objc")]
12535    #[test]
12536    fn objc_cloc() {
12537        check_metrics::<ObjcParser>(
12538            "/* Block comment
12539            still the block */
12540            // Line comment
12541            int a = 42; // trailing",
12542            "foo.m",
12543            |metric| {
12544                insta::assert_json_snapshot!(metric.loc, @r#"
12545                {
12546                  "sloc": 4,
12547                  "ploc": 1,
12548                  "lloc": 1,
12549                  "cloc": 4,
12550                  "blank": 0,
12551                  "dloc": 0,
12552                  "sloc_average": 4.0,
12553                  "ploc_average": 1.0,
12554                  "lloc_average": 1.0,
12555                  "cloc_average": 4.0,
12556                  "blank_average": 0.0,
12557                  "dloc_average": 0.0,
12558                  "sloc_min": 4,
12559                  "sloc_max": 4,
12560                  "cloc_min": 4,
12561                  "cloc_max": 4,
12562                  "ploc_min": 1,
12563                  "ploc_max": 1,
12564                  "lloc_min": 1,
12565                  "lloc_max": 1,
12566                  "blank_min": 0,
12567                  "blank_max": 0,
12568                  "dloc_min": 0,
12569                  "dloc_max": 0
12570                }
12571                "#);
12572            },
12573        );
12574    }
12575
12576    /// Objective-C logical-line accounting: a method whose body has three
12577    /// statements. The `method_definition` opens a function space but is
12578    /// not itself a logical line; each statement adds one.
12579    #[cfg(feature = "objc")]
12580    #[test]
12581    fn objc_lloc() {
12582        check_metrics::<ObjcParser>(
12583            "@implementation Foo
12584- (int)bar {
12585    int a = 1;
12586    int b = 2;
12587    return a + b;
12588}
12589@end
12590",
12591            "foo.m",
12592            |metric| {
12593                // expected: decl `int a` (1) + decl `int b` (1) +
12594                // `return` (1) = 3.
12595                assert_eq!(metric.loc.lloc(), 3);
12596                insta::assert_json_snapshot!(metric.loc, @r#"
12597                {
12598                  "sloc": 7,
12599                  "ploc": 7,
12600                  "lloc": 3,
12601                  "cloc": 0,
12602                  "blank": 0,
12603                  "dloc": 0,
12604                  "sloc_average": 2.3333333333333335,
12605                  "ploc_average": 2.3333333333333335,
12606                  "lloc_average": 1.0,
12607                  "cloc_average": 0.0,
12608                  "blank_average": 0.0,
12609                  "dloc_average": 0.0,
12610                  "sloc_min": 5,
12611                  "sloc_max": 7,
12612                  "cloc_min": 0,
12613                  "cloc_max": 0,
12614                  "ploc_min": 5,
12615                  "ploc_max": 7,
12616                  "lloc_min": 3,
12617                  "lloc_max": 3,
12618                  "blank_min": 0,
12619                  "blank_max": 0,
12620                  "dloc_min": 0,
12621                  "dloc_max": 0
12622                }
12623                "#);
12624            },
12625        );
12626    }
12627
12628    /// Objective-C for-header gating: the `int i = 0` declaration in a
12629    /// classic `for` init slot is part of the `for` statement's logical
12630    /// line and must NOT add a second one (mirrors the C / C++ gate).
12631    /// Reverting the `count_specific_ancestors` gate would push lloc from
12632    /// 2 to 3.
12633    #[cfg(feature = "objc")]
12634    #[test]
12635    fn objc_no_declaration_in_for_header_lloc() {
12636        check_metrics::<ObjcParser>(
12637            "@implementation Foo
12638- (void)bar {
12639    for (int i = 0; i < 10; ++i) {
12640        [self use:i];
12641    }
12642}
12643@end
12644",
12645            "foo.m",
12646            |metric| {
12647                // expected: for-statement (1) + body expression
12648                // `[self use:i]` (1) = 2. The header `int i = 0`
12649                // declaration is gated out; without the gate this is 3.
12650                assert_eq!(metric.loc.lloc(), 2);
12651                insta::assert_json_snapshot!(metric.loc, @r#"
12652                {
12653                  "sloc": 7,
12654                  "ploc": 7,
12655                  "lloc": 2,
12656                  "cloc": 0,
12657                  "blank": 0,
12658                  "dloc": 0,
12659                  "sloc_average": 2.3333333333333335,
12660                  "ploc_average": 2.3333333333333335,
12661                  "lloc_average": 0.6666666666666666,
12662                  "cloc_average": 0.0,
12663                  "blank_average": 0.0,
12664                  "dloc_average": 0.0,
12665                  "sloc_min": 5,
12666                  "sloc_max": 7,
12667                  "cloc_min": 0,
12668                  "cloc_max": 0,
12669                  "ploc_min": 5,
12670                  "ploc_max": 7,
12671                  "lloc_min": 2,
12672                  "lloc_max": 2,
12673                  "blank_min": 0,
12674                  "blank_max": 0,
12675                  "dloc_min": 0,
12676                  "dloc_max": 0
12677                }
12678                "#);
12679            },
12680        );
12681    }
12682
12683    #[cfg(feature = "objc")]
12684    #[test]
12685    fn objc_at_directives_lloc() {
12686        // The only ObjC-specific LLOC work the impl does beyond the C
12687        // inheritance: `@synchronized` is a dedicated `synchronized_statement`
12688        // node (counts as a logical line), but `@autoreleasepool` emits
12689        // only a keyword token with no wrapping node, so its *header* adds
12690        // nothing and only its inner statements count.
12691        check_metrics::<ObjcParser>(
12692            "@implementation Foo
12693- (void)bar {
12694    @synchronized (self) {
12695        [self use];
12696    }
12697    @autoreleasepool {
12698        [self use];
12699    }
12700}
12701@end
12702",
12703            "foo.m",
12704            |metric| {
12705                // expected: @synchronized statement (1) + its body
12706                // `[self use]` (1) + the @autoreleasepool body `[self use]`
12707                // (1) = 3. The `@autoreleasepool` header contributes no
12708                // logical line (it has no node); if it did, this would be 4.
12709                assert_eq!(metric.loc.lloc(), 3);
12710                insta::assert_json_snapshot!(metric.loc, @r#"
12711                {
12712                  "sloc": 10,
12713                  "ploc": 10,
12714                  "lloc": 3,
12715                  "cloc": 0,
12716                  "blank": 0,
12717                  "dloc": 0,
12718                  "sloc_average": 3.3333333333333335,
12719                  "ploc_average": 3.3333333333333335,
12720                  "lloc_average": 1.0,
12721                  "cloc_average": 0.0,
12722                  "blank_average": 0.0,
12723                  "dloc_average": 0.0,
12724                  "sloc_min": 8,
12725                  "sloc_max": 10,
12726                  "cloc_min": 0,
12727                  "cloc_max": 0,
12728                  "ploc_min": 8,
12729                  "ploc_max": 10,
12730                  "lloc_min": 3,
12731                  "lloc_max": 3,
12732                  "blank_min": 0,
12733                  "blank_max": 0,
12734                  "dloc_min": 0,
12735                  "dloc_max": 0
12736                }
12737                "#);
12738            },
12739        );
12740    }
12741
12742    /// Analyses `source` byte-for-byte as Rust.
12743    ///
12744    /// Goes through `metrics_verbatim` rather than `check_metrics`
12745    /// because the #1051 cases end at EOF; see that helper for why.
12746    #[cfg(feature = "rust")]
12747    fn rust_loc(source: &[u8]) -> Stats {
12748        metrics_verbatim(crate::LANG::Rust, source, crate::MetricsOptions::default()).loc
12749    }
12750
12751    /// #1051: a Rust doc comment ending at EOF has no trailing newline for
12752    /// the scanner to consume, so its `LineComment` node ends on its own
12753    /// start row and discounting a row underflowed. On row 0 that panicked
12754    /// (debug: the subtraction; release: a hash-table capacity overflow in
12755    /// `add_only_comment_lines`). On any later row release did not crash —
12756    /// it silently reported one `cloc` too few.
12757    #[cfg(feature = "rust")]
12758    #[test]
12759    fn rust_doc_comment_at_eof_does_not_underflow() {
12760        // `end == start == 0` — underflowed at the subtraction itself.
12761        // expected: the sole row is one comment-only line, no code.
12762        let outer = rust_loc(b"/// x");
12763        assert_eq!(outer.cloc(), 1);
12764        assert_eq!(outer.ploc(), 0);
12765
12766        let inner = rust_loc(b"//! x");
12767        assert_eq!(inner.cloc(), 1);
12768        assert_eq!(inner.ploc(), 0);
12769
12770        // `end == start > 0` — the subtraction succeeded but drove `end`
12771        // below `start`. Release silently counted `cloc == 0` here.
12772        // expected: row 0 is code, row 1 is comment-only.
12773        let after_code = rust_loc(b"fn f(){}\n/// x");
12774        assert_eq!(after_code.cloc(), 1);
12775        assert_eq!(after_code.ploc(), 1);
12776
12777        // Two doc comments, the second at EOF: both rows are comment-only.
12778        // Pre-fix release reported 1 here, not 2.
12779        assert_eq!(rust_loc(b"/// a\n/// b").cloc(), 2);
12780
12781        // A doc comment sharing its row with code. `let` is not valid at
12782        // file scope, but tree-sitter parses it as a clean `let_declaration`
12783        // + `line_comment`, which is the only way to reach the
12784        // comment-after-code branch (no *valid* Rust puts `///` after code).
12785        let trailing = rust_loc(b"let x = 1; /// d");
12786        assert_eq!(trailing.cloc(), 1);
12787        assert_eq!(trailing.ploc(), 1);
12788    }
12789
12790    /// A doc comment at EOF must count exactly like a plain line comment at
12791    /// EOF. The `DocComment` adjustment exists only to discount the newline
12792    /// the scanner consumes; at EOF there is none to discount, so the two
12793    /// shapes are indistinguishable for LOC purposes.
12794    #[cfg(feature = "rust")]
12795    #[test]
12796    fn rust_doc_comment_at_eof_matches_plain_comment() {
12797        let plain = rust_loc(b"// x");
12798        // Pin the baseline absolutely too: parity alone would still hold if
12799        // both sides moved together, and would then read as a regression
12800        // when the un-newline-terminated `sloc` accounting is corrected.
12801        assert_eq!(plain.cloc(), 1);
12802        assert_eq!(plain.ploc(), 0);
12803
12804        for doc in [&b"/// x"[..], &b"//! x"[..]] {
12805            let doc = rust_loc(doc);
12806            assert_eq!(doc.cloc(), plain.cloc());
12807            assert_eq!(doc.ploc(), plain.ploc());
12808            assert_eq!(doc.sloc(), plain.sloc());
12809            assert_eq!(doc.blank(), plain.blank());
12810        }
12811    }
12812
12813    /// The newline-terminated path must stay unchanged by the #1051 guard.
12814    /// A `DocComment` node really does span one row more than it renders
12815    /// whenever the scanner consumed a newline, and that row must still be
12816    /// excluded — otherwise the guard would silently become a no-op and
12817    /// inflate CLOC for every doc-commented Rust file.
12818    #[cfg(feature = "rust")]
12819    #[test]
12820    fn rust_doc_comment_with_trailing_newline_still_discounts_the_row() {
12821        // expected: one rendered comment row, not two.
12822        assert_eq!(rust_loc(b"/// x\n").cloc(), 1);
12823        // expected: two consecutive doc comments are two rows, not four.
12824        assert_eq!(rust_loc(b"/// a\n/// b\n").cloc(), 2);
12825
12826        // The common real-world shape: doc comment attached to an item.
12827        // expected: row 0 comment-only, row 1 code.
12828        let documented = rust_loc(b"/// doc\nfn f() {}\n");
12829        assert_eq!(documented.cloc(), 1);
12830        assert_eq!(documented.ploc(), 1);
12831    }
12832
12833    /// CRLF is the boundary the guard must *not* fire on. `\r` is ordinary
12834    /// content to `process_line_doc_content`, so it consumes the following
12835    /// `\n` and the node does span an extra row — the discount is still
12836    /// owed. A lone trailing `\r` at EOF is the opposite case. Without this,
12837    /// a future grammar bump that stops consuming the newline would leave
12838    /// every LF test passing while the discount silently became dead code.
12839    #[cfg(feature = "rust")]
12840    #[test]
12841    fn rust_doc_comment_crlf_still_discounts_the_row() {
12842        // Newline consumed despite the `\r`: discount applies.
12843        assert_eq!(rust_loc(b"/// x\r\n").cloc(), 1);
12844        // expected: row 0 code, row 1 comment-only.
12845        let after_code = rust_loc(b"fn f(){}\r\n/// x");
12846        assert_eq!(after_code.cloc(), 1);
12847        assert_eq!(after_code.ploc(), 1);
12848        // Lone `\r`, then EOF: no newline consumed, so no discount is owed.
12849        assert_eq!(rust_loc(b"/// x\r").cloc(), 1);
12850    }
12851
12852    /// One-line, deliberately un-newline-terminated source, one entry per
12853    /// language whose `Loc` implementation is not a documented no-op.
12854    ///
12855    /// `sloc` is computed once, in the shared [`Sloc`], from the unit
12856    /// span the grammar hands us — so the only per-language variable is
12857    /// where each grammar puts the root node's end position. #1067 was
12858    /// possible precisely because that position was assumed rather than
12859    /// read, so the sweep is exhaustive rather than a sample: a future
12860    /// grammar whose root does *not* run to end-of-input has to show up
12861    /// here rather than silently lose a row.
12862    ///
12863    /// `Preproc` and `Ccomment` are excluded, but not because they are
12864    /// exempt from the rule. Their `Loc` impls are no-ops
12865    /// (`implement_metric_trait!(Loc, PreprocCode, CcommentCode)`, #188),
12866    /// so the *node-accumulated* sub-metrics are 0 by design — yet
12867    /// `sloc` is not node-accumulated: the walker anchors every Unit's
12868    /// row span at finalization, their synthetic Unit root included, so
12869    /// they carry a real span and drift with #1067 exactly as the
12870    /// languages below do. What they cannot join is the second sweep, whose final
12871    /// `mi != 0` assertion is unreachable with `ploc == 0`. They get
12872    /// their own check in
12873    /// `no_op_loc_grammars_still_count_their_unterminated_row`.
12874    #[cfg(any(
12875        feature = "bash",
12876        feature = "c",
12877        feature = "c-family-helpers",
12878        feature = "cpp",
12879        feature = "csharp",
12880        feature = "elixir",
12881        feature = "go",
12882        feature = "groovy",
12883        feature = "irules",
12884        feature = "java",
12885        feature = "javascript",
12886        feature = "kotlin",
12887        feature = "lua",
12888        feature = "mozcpp",
12889        feature = "mozjs",
12890        feature = "objc",
12891        feature = "perl",
12892        feature = "php",
12893        feature = "python",
12894        feature = "ruby",
12895        feature = "rust",
12896        feature = "tcl",
12897        feature = "typescript",
12898    ))]
12899    const UNTERMINATED_ONE_LINERS: &[(crate::LANG, &[u8])] = &[
12900        (crate::LANG::Rust, b"fn main() {}"),
12901        (crate::LANG::C, b"int main(void) { return 0; }"),
12902        (crate::LANG::Cpp, b"int main() { return 0; }"),
12903        (crate::LANG::Mozcpp, b"int main() { return 0; }"),
12904        (crate::LANG::Objc, b"int main(void) { return 0; }"),
12905        (crate::LANG::Csharp, b"class C { void M() {} }"),
12906        (crate::LANG::Java, b"class C { void m() {} }"),
12907        (crate::LANG::Kotlin, b"fun main() {}"),
12908        (crate::LANG::Groovy, b"def f() {}"),
12909        (crate::LANG::Go, b"package main"),
12910        (crate::LANG::Javascript, b"function f() {}"),
12911        (crate::LANG::Mozjs, b"function f() {}"),
12912        (crate::LANG::Typescript, b"function f(): void {}"),
12913        (crate::LANG::Tsx, b"function f() {}"),
12914        (crate::LANG::Python, b"def f(): pass"),
12915        (crate::LANG::Ruby, b"def f; end"),
12916        (crate::LANG::Php, b"<?php function f() {}"),
12917        (crate::LANG::Perl, b"sub f { return 1; }"),
12918        (crate::LANG::Bash, b"f() { echo hi; }"),
12919        (crate::LANG::Lua, b"function f() end"),
12920        (crate::LANG::Tcl, b"proc f {} {}"),
12921        (crate::LANG::Irules, b"proc f {} {}"),
12922        (crate::LANG::Elixir, b"defmodule M do end"),
12923    ];
12924
12925    /// #1067: `Sloc::sloc()` derived the unit's row count as `end - start`,
12926    /// which is only right when a trailing newline pushes the root node's
12927    /// end onto a phantom extra row. Source that stops mid-line — anything
12928    /// not newline-terminated — lost its final row, so a one-line file
12929    /// reported `sloc == 0`.
12930    ///
12931    /// Uses [`metrics_verbatim`], not `check_metrics`: the latter trims and
12932    /// re-appends a trailing newline, which makes this entire input class
12933    /// unreachable and the test vacuous (the same blind spot that hid
12934    /// #1051).
12935    #[cfg(all(
12936        feature = "bash",
12937        feature = "c",
12938        feature = "cpp",
12939        feature = "csharp",
12940        feature = "elixir",
12941        feature = "go",
12942        feature = "groovy",
12943        feature = "irules",
12944        feature = "java",
12945        feature = "javascript",
12946        feature = "kotlin",
12947        feature = "lua",
12948        feature = "mozcpp",
12949        feature = "mozjs",
12950        feature = "objc",
12951        feature = "perl",
12952        feature = "php",
12953        feature = "python",
12954        feature = "ruby",
12955        feature = "rust",
12956        feature = "tcl",
12957        feature = "typescript",
12958    ))]
12959    #[test]
12960    fn unterminated_one_line_file_reports_one_source_line() {
12961        for (lang, source) in UNTERMINATED_ONE_LINERS {
12962            let text = String::from_utf8_lossy(source);
12963            let loc = metrics_verbatim(*lang, source, MetricsOptions::default()).loc;
12964            assert_eq!(loc.sloc(), 1, "{lang:?} sloc for {text:?}");
12965            // The largest space can never be bigger than the file itself.
12966            assert_eq!(loc.sloc_max(), 1, "{lang:?} sloc_max for {text:?}");
12967            // The invariant documented on `Stats::with_cloc_sloc`: every
12968            // physical line is code, comment, both, or blank, so the code
12969            // and comment-only tallies cannot together exceed the row
12970            // count. `sloc == 0` broke it for any unterminated file whose
12971            // last line carried content.
12972            assert!(
12973                loc.cloc() + loc.ploc() <= loc.sloc(),
12974                "{lang:?}: cloc {} + ploc {} exceeds sloc {} for {text:?}",
12975                loc.cloc(),
12976                loc.ploc(),
12977                loc.sloc(),
12978            );
12979        }
12980    }
12981
12982    /// The two grammars whose `Loc` impl is the macro's no-op still get a
12983    /// `sloc`, so they drift with #1067 like everything else.
12984    ///
12985    /// Their root is not a `SpaceKind::Unit`, so `metrics_inner` pushes a
12986    /// synthetic Unit, whose row span `anchor_unit_sloc_span` fills in at
12987    /// finalization — a span the no-op `compute` never touches but
12988    /// `Sloc::sloc()` still measures.
12989    /// Before #1067 an unterminated one-liner measured `0` rows here too.
12990    /// Kept apart from [`UNTERMINATED_ONE_LINERS`] only because the
12991    /// `mi != 0` half of the sweep below cannot hold with `ploc == 0`.
12992    #[cfg(any(
12993        feature = "c",
12994        feature = "c-family-helpers",
12995        feature = "cpp",
12996        feature = "mozcpp"
12997    ))]
12998    #[test]
12999    fn no_op_loc_grammars_still_count_their_unterminated_row() {
13000        for (lang, source) in [
13001            (crate::LANG::Preproc, &b"#define A 1"[..]),
13002            (crate::LANG::Ccomment, &b"/* c */"[..]),
13003        ] {
13004            let bare = metrics_verbatim(lang, source, MetricsOptions::default()).loc;
13005            assert_eq!(bare.sloc(), 1, "{lang:?} unterminated sloc");
13006            let mut terminated = source.to_vec();
13007            terminated.push(b'\n');
13008            let terminated = metrics_verbatim(lang, &terminated, MetricsOptions::default()).loc;
13009            assert_eq!(
13010                bare.sloc(),
13011                terminated.sloc(),
13012                "{lang:?} sloc must not depend on the trailing newline"
13013            );
13014            // The node-accumulated sub-metrics are the ones #188 zeroes.
13015            assert_eq!((bare.ploc(), bare.cloc(), bare.lloc()), (0, 0, 0));
13016        }
13017    }
13018
13019    /// A backslash-continued `#define` body is one `PreprocArg` node
13020    /// spanning every continuation row, so each of those rows is PLOC.
13021    ///
13022    /// The four C-family `Loc` impls carry an identical arm for this
13023    /// (`tree-sitter-cpp` does not expand macros — see the comment at
13024    /// each site), and until #1229 only C++'s copy was exercised: the
13025    /// other three were the sole uncovered lines in that PR. They are
13026    /// deliberate clones, so a fixture for one is a fixture for all
13027    /// four, and `Mozcpp` in particular owns no file extension and can
13028    /// only be reached by naming the language.
13029    ///
13030    /// Measured, and confirmed discriminating by deleting the arm from
13031    /// all four modules: `ploc` is 4 with it and 3 without, the lost row
13032    /// being the macro's last continuation line. `sloc` is 5 (three
13033    /// macro rows, one blank, one `main`) and `lloc` is 1 — the single
13034    /// `return` statement — since a `#define` declares no statement.
13035    ///
13036    /// This fixture pins how *many* rows the arm credits, not where it
13037    /// stops: its body closes on `(b))`, so the node's end column is
13038    /// above 0 and the raw end row and `Node::end_line() - 1` name the
13039    /// same last row. The bound is pinned separately by
13040    /// [`a_dangling_macro_continuation_does_not_credit_the_row_below`].
13041    #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
13042    #[test]
13043    fn a_continued_macro_body_counts_every_row_it_spans() {
13044        // Rows: 0-2 are the macro, 3 is blank, 4 is `main`.
13045        const CONTINUED_MACRO: &[u8] =
13046            b"#define SUM(a, b) \\\n    ((a) + \\\n     (b))\n\nint main(void) { return SUM(1, 2); }\n";
13047
13048        // One row per language, each gated on its own feature: an
13049        // ungated table constructs a parser for a language the build
13050        // disabled and panics `LanguageDisabled`, which reads as a
13051        // defect in whatever was being changed rather than as a
13052        // configuration that cannot run this test (#1446).
13053        const LANGS: &[crate::LANG] = &[
13054            #[cfg(feature = "c")]
13055            crate::LANG::C,
13056            #[cfg(feature = "cpp")]
13057            crate::LANG::Cpp,
13058            #[cfg(feature = "mozcpp")]
13059            crate::LANG::Mozcpp,
13060            #[cfg(feature = "objc")]
13061            crate::LANG::Objc,
13062        ];
13063        crate::test_support::assert_fixtures_present(LANGS);
13064
13065        for &lang in LANGS {
13066            let loc = metrics_verbatim(lang, CONTINUED_MACRO, MetricsOptions::default()).loc;
13067            assert_eq!(
13068                loc.ploc(),
13069                4,
13070                "{lang:?}: every continuation row of the macro body is code"
13071            );
13072            assert_eq!(loc.sloc(), 5, "{lang:?} sloc");
13073            assert_eq!(loc.lloc(), 1, "{lang:?} lloc");
13074            assert_eq!(loc.cloc(), 0, "{lang:?} cloc");
13075            assert_eq!(loc.blank(), 1, "{lang:?} blank");
13076        }
13077    }
13078
13079    /// Where the `PreprocArg` arm stops: a macro body whose last
13080    /// continuation is a *dangling* backslash produces a node ending at
13081    /// column 0 of the row below, and that row is not part of the span.
13082    ///
13083    /// `Node::end_line` is what encodes the rule, so the arm derives its
13084    /// last row from `add_string_interior_ploc` rather than from the raw
13085    /// `end_row()`, which over-reads by one on exactly this shape
13086    /// (#1423). Every other multi-row PLOC path already went through the
13087    /// helper; these four arms were the last open-coded copies.
13088    ///
13089    /// Measured with `bca dump`, not read off the source text: the
13090    /// `preproc_arg` runs from row 0 column 10 to row 2 column 0. Rows
13091    /// are 0 `#define`, 1 the continuation, 2 the blank row the trailing
13092    /// backslash runs into, 3 `int x;`. So `ploc` is {0, 1, 3} = 3 and
13093    /// `blank` is 1. The raw end row credited row 2 as well, reporting
13094    /// `ploc 4, blank 0`.
13095    ///
13096    /// Two parts of the fixture are load-bearing rather than scenery:
13097    ///
13098    /// - **`int x;` after the macro.** `clamp_line_sets_to_span` already
13099    ///   drops a PLOC row past the space's own last row, so a fixture
13100    ///   ending on the macro reports the corrected numbers under either
13101    ///   spelling and discriminates nothing.
13102    /// - **The `    2 \` continuation row.** Nothing but the arm's
13103    ///   interior insert credits row 1 — `preproc_def` contributes only
13104    ///   its own start row — so `ploc == 3` fails if that row is ever
13105    ///   trimmed out of the fixture, per the fixture-decay rule in
13106    ///   `.claude/rules/testing.md`. The `blank == 1` half has no such
13107    ///   anchor available: deleting the dangling backslash also stops
13108    ///   row 2 being credited, so a revert of the bound is the only
13109    ///   evidence that half discriminates, and it was run.
13110    #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
13111    #[test]
13112    fn a_dangling_macro_continuation_does_not_credit_the_row_below() {
13113        // Rows: 0-1 are the macro, 2 is the blank row its trailing
13114        // backslash runs into, 3 is `int x;`.
13115        const DANGLING_CONTINUATION: &[u8] = b"#define A 1 \\\n    2 \\\n\nint x;\n";
13116
13117        // Gated per row for the reason
13118        // `a_continued_macro_body_counts_every_row_it_spans` gives.
13119        const LANGS: &[crate::LANG] = &[
13120            #[cfg(feature = "c")]
13121            crate::LANG::C,
13122            #[cfg(feature = "cpp")]
13123            crate::LANG::Cpp,
13124            #[cfg(feature = "mozcpp")]
13125            crate::LANG::Mozcpp,
13126            #[cfg(feature = "objc")]
13127            crate::LANG::Objc,
13128        ];
13129        crate::test_support::assert_fixtures_present(LANGS);
13130
13131        for &lang in LANGS {
13132            let loc = metrics_verbatim(lang, DANGLING_CONTINUATION, MetricsOptions::default()).loc;
13133            assert_eq!(
13134                loc.ploc(),
13135                3,
13136                "{lang:?}: the row a dangling continuation ends *at* is not code"
13137            );
13138            assert_eq!(
13139                loc.blank(),
13140                1,
13141                "{lang:?}: that row is blank, not part of the macro body"
13142            );
13143            assert_eq!(loc.sloc(), 4, "{lang:?} sloc");
13144            assert_eq!(loc.lloc(), 1, "{lang:?} lloc");
13145            assert_eq!(loc.cloc(), 0, "{lang:?} cloc");
13146        }
13147    }
13148
13149    /// Whether the last line ends in a newline is a formatting detail, not
13150    /// a property of the code — no LOC sub-metric, and therefore no MI
13151    /// value, may depend on it. This is the invariant #1067 violated, and
13152    /// it pins the fix from both sides: the newline-terminated path (the
13153    /// one every in-tree harness exercises) must not move either.
13154    ///
13155    /// **The invariant is now unconditional.** It used to be scoped to
13156    /// source containing a token: whitespace-only input collapsed most
13157    /// grammars' roots to a zero-width node at end-of-input, leaving
13158    /// `sloc` no span to measure, so `b"  "` reported one row and
13159    /// `b"  \n"` reported none (#1087). #1247 anchored the unit's `sloc`
13160    /// span to the span the unit reports, which removed that dependence
13161    /// on where the root node happens to start. The whitespace-only class
13162    /// is swept separately — it cannot ride this test, whose closing
13163    /// `assert_ne!` requires a non-zero MI — in
13164    /// [`whitespace_only_input_is_uniform_across_grammars`].
13165    #[cfg(all(
13166        feature = "bash",
13167        feature = "c",
13168        feature = "cpp",
13169        feature = "csharp",
13170        feature = "elixir",
13171        feature = "go",
13172        feature = "groovy",
13173        feature = "irules",
13174        feature = "java",
13175        feature = "javascript",
13176        feature = "kotlin",
13177        feature = "lua",
13178        feature = "mozcpp",
13179        feature = "mozjs",
13180        feature = "objc",
13181        feature = "perl",
13182        feature = "php",
13183        feature = "python",
13184        feature = "ruby",
13185        feature = "rust",
13186        feature = "tcl",
13187        feature = "typescript",
13188    ))]
13189    #[test]
13190    fn trailing_newline_does_not_change_loc_or_mi() {
13191        for (lang, source) in UNTERMINATED_ONE_LINERS {
13192            let text = String::from_utf8_lossy(source);
13193            let bare = metrics_verbatim(*lang, source, MetricsOptions::default());
13194            let mut newline_terminated = source.to_vec();
13195            newline_terminated.push(b'\n');
13196            let terminated =
13197                metrics_verbatim(*lang, &newline_terminated, MetricsOptions::default());
13198
13199            assert_eq!(
13200                bare.loc.sloc(),
13201                terminated.loc.sloc(),
13202                "{lang:?} sloc {text:?}"
13203            );
13204            assert_eq!(
13205                bare.loc.ploc(),
13206                terminated.loc.ploc(),
13207                "{lang:?} ploc {text:?}"
13208            );
13209            assert_eq!(
13210                bare.loc.cloc(),
13211                terminated.loc.cloc(),
13212                "{lang:?} cloc {text:?}"
13213            );
13214            assert_eq!(
13215                bare.loc.lloc(),
13216                terminated.loc.lloc(),
13217                "{lang:?} lloc {text:?}"
13218            );
13219            assert_eq!(
13220                bare.loc.blank(),
13221                terminated.loc.blank(),
13222                "{lang:?} blank {text:?}"
13223            );
13224            // The MI knock-on: `mi::Stats::is_empty` short-circuits to
13225            // 0.0 on an empty size term, so before the fix every unterminated
13226            // one-liner reported MI 0 while its newline-terminated twin
13227            // reported a real score. Inputs are now identical, so the
13228            // three formulas agree bit-for-bit.
13229            assert_eq!(
13230                bare.mi.original(),
13231                terminated.mi.original(),
13232                "{lang:?} mi {text:?}"
13233            );
13234            assert_eq!(
13235                bare.mi.sei(),
13236                terminated.mi.sei(),
13237                "{lang:?} mi.sei {text:?}"
13238            );
13239            assert_eq!(
13240                bare.mi.visual_studio(),
13241                terminated.mi.visual_studio(),
13242                "{lang:?} mi.visual_studio {text:?}",
13243            );
13244            assert_ne!(bare.mi.original(), 0.0, "{lang:?} mi must not be zeroed");
13245        }
13246    }
13247
13248    /// The second #1067 symptom: `b"fn f(){}\n/// x"` reported `sloc == 1`
13249    /// with `ploc == 1` *and* `cloc == 1`, so `cloc + ploc > sloc`. The
13250    /// file has two rows; only the missing one made the sums disagree.
13251    #[cfg(feature = "rust")]
13252    #[test]
13253    fn unterminated_trailing_comment_upholds_the_cloc_ploc_invariant() {
13254        // expected: row 0 is code, row 1 is comment-only, nothing blank.
13255        let loc = rust_loc(b"fn f(){}\n/// x");
13256        assert_eq!(loc.sloc(), 2);
13257        assert_eq!(loc.ploc(), 1);
13258        assert_eq!(loc.cloc(), 1);
13259        assert_eq!(loc.blank(), 0);
13260    }
13261
13262    /// Degenerate inputs, pinned so the end-column rule in
13263    /// `Node::end_line` cannot drift into fabricating rows for files
13264    /// that have none.
13265    #[cfg(feature = "rust")]
13266    #[test]
13267    fn degenerate_inputs_report_their_real_row_count() {
13268        // No bytes, no rows.
13269        assert_eq!(rust_loc(b"").sloc(), 0);
13270        // One row of whitespace, unterminated: the root node is empty but
13271        // sits at column 3, so the row is real and counts as blank.
13272        let spaces = rust_loc(b"   ");
13273        assert_eq!(spaces.sloc(), 1);
13274        assert_eq!(spaces.blank(), 1);
13275        // Newline-terminated whitespace was the #1087 carve-out: most
13276        // grammars collapse the root to a zero-width node at end-of-input
13277        // (`(1, 0)..(1, 0)` for `"\n"`), so the measured span had no rows
13278        // left to attribute and `sloc` was 0 for a file that plainly has
13279        // one. #1247 retired that: the unit's `sloc` span is now anchored
13280        // to the span the unit *reports*, which #1195 already anchored at
13281        // line 1, so a collapsed root no longer costs the file its rows.
13282        let one = rust_loc(b"\n");
13283        assert_eq!((one.sloc(), one.ploc(), one.blank()), (1, 0, 1));
13284        let two = rust_loc(b"\n\n");
13285        assert_eq!((two.sloc(), two.ploc(), two.blank()), (2, 0, 2));
13286    }
13287
13288    /// Every grammar whose `Loc` behaviour this module owns, including the
13289    /// two whose `compute` is the `implement_metric_trait!` no-op.
13290    ///
13291    /// [`UNTERMINATED_ONE_LINERS`] carries a fixture per language because
13292    /// its sweeps need parseable code; the whitespace-only sweep needs
13293    /// only the language, and must not omit `Preproc`/`Ccomment` — their
13294    /// synthetic Unit root is anchored like any other, so they carry a
13295    /// real span and answer the #1087/#1247 question too.
13296    #[cfg(any(
13297        feature = "bash",
13298        feature = "c",
13299        feature = "c-family-helpers",
13300        feature = "cpp",
13301        feature = "csharp",
13302        feature = "elixir",
13303        feature = "go",
13304        feature = "groovy",
13305        feature = "irules",
13306        feature = "java",
13307        feature = "javascript",
13308        feature = "kotlin",
13309        feature = "lua",
13310        feature = "mozcpp",
13311        feature = "mozjs",
13312        feature = "objc",
13313        feature = "perl",
13314        feature = "php",
13315        feature = "python",
13316        feature = "ruby",
13317        feature = "rust",
13318        feature = "tcl",
13319        feature = "typescript",
13320    ))]
13321    fn all_loc_grammars() -> impl Iterator<Item = crate::LANG> {
13322        UNTERMINATED_ONE_LINERS
13323            .iter()
13324            .map(|(lang, _)| *lang)
13325            .chain([crate::LANG::Preproc, crate::LANG::Ccomment])
13326    }
13327
13328    /// #1087 accepted whitespace-only source as the one input class where
13329    /// a trailing newline moved `sloc` — twenty grammars collapse the root
13330    /// to a zero-width node at end-of-input, so `"  "` reported one row
13331    /// and `"  \n"` reported none, while five grammars (Elixir, Tcl,
13332    /// iRules, Preproc, Ccomment) kept the span and were newline-
13333    /// independent already. This sweep was written to pin both halves.
13334    ///
13335    /// #1247 removed the premise. The carve-out was a consequence of
13336    /// measuring the unit's `sloc` span from the root node's first token;
13337    /// once that span is anchored to the one the unit *reports* (#1195
13338    /// anchored the reported span at line 1), a collapsed root no longer
13339    /// erases the file's rows and every grammar answers alike. The sweep
13340    /// stays, with the split list retired: it now pins the *absence* of a
13341    /// per-grammar difference, which is the property a future grammar bump
13342    /// or walker change could still break.
13343    ///
13344    /// The unterminated side is unchanged and was always uniform: every
13345    /// grammar reports the row, as one blank line.
13346    #[cfg(all(
13347        feature = "bash",
13348        feature = "c",
13349        any(
13350            feature = "c",
13351            feature = "c-family-helpers",
13352            feature = "cpp",
13353            feature = "mozcpp"
13354        ),
13355        feature = "cpp",
13356        feature = "csharp",
13357        feature = "elixir",
13358        feature = "go",
13359        feature = "groovy",
13360        feature = "irules",
13361        feature = "java",
13362        feature = "javascript",
13363        feature = "kotlin",
13364        feature = "lua",
13365        feature = "mozcpp",
13366        feature = "mozjs",
13367        feature = "objc",
13368        feature = "perl",
13369        feature = "php",
13370        feature = "python",
13371        feature = "ruby",
13372        feature = "rust",
13373        feature = "tcl",
13374        feature = "typescript",
13375    ))]
13376    #[test]
13377    fn whitespace_only_input_is_uniform_across_grammars() {
13378        // Spaces and tabs both, so a grammar that lexes one as extra and
13379        // the other as an error token cannot hide behind the sweep.
13380        for bare in [&b"  "[..], b"\t\t"] {
13381            let mut newline_terminated = bare.to_vec();
13382            newline_terminated.push(b'\n');
13383            for lang in all_loc_grammars() {
13384                let text = String::from_utf8_lossy(bare);
13385                let unterminated = metrics_verbatim(lang, bare, MetricsOptions::default()).loc;
13386                let terminated =
13387                    metrics_verbatim(lang, &newline_terminated, MetricsOptions::default()).loc;
13388
13389                assert_eq!(
13390                    (unterminated.sloc(), unterminated.blank()),
13391                    (1, 1),
13392                    "{lang:?}: unterminated {text:?} is one blank row for every grammar"
13393                );
13394
13395                assert_eq!(
13396                    (terminated.sloc(), terminated.blank()),
13397                    (1, 1),
13398                    "{lang:?}: newline-terminated {text:?} is the same one blank \
13399                     row — whether the grammar collapses its root at \
13400                     end-of-input is no longer observable in loc (#1247)"
13401                );
13402
13403                // Whitespace is never code and never a comment, whichever
13404                // side of the carve-out the grammar sits on. This is the
13405                // assertion #1135 broke for Tcl and iRules, whose row
13406                // terminator used to land in their PLOC catch-all.
13407                for loc in [&unterminated, &terminated] {
13408                    assert_eq!(
13409                        (loc.ploc(), loc.cloc(), loc.lloc()),
13410                        (0, 0, 0),
13411                        "{lang:?}: whitespace is neither code nor comment"
13412                    );
13413                }
13414            }
13415        }
13416    }
13417
13418    /// #1247: the unit anchors its *reported* span at line 1 (#1195) but
13419    /// measured its `sloc` span from the root node's first token, so blank
13420    /// rows above that token counted in neither `sloc` nor `blank` — while
13421    /// byte-identical rows one line lower counted in both.
13422    ///
13423    /// Swept across four grammars because the per-language `Loc` impls
13424    /// mirror each other. Tcl is in the list because it was already
13425    /// *right*: its row terminator is a token child of the root, so the
13426    /// root already started at row 0 and Tcl reported `sloc 4` for the
13427    /// Rust fixture's `sloc 1`. A per-language fix would have had to know
13428    /// which grammars were which; the walker-level anchor does not, and
13429    /// Tcl is the case that catches one being applied twice.
13430    ///
13431    /// `space_verbatim`, not `check_metrics`: the shim trims leading and
13432    /// trailing newlines, which deletes this test's entire subject.
13433    #[cfg(all(feature = "c", feature = "python", feature = "rust", feature = "tcl"))]
13434    #[test]
13435    fn leading_blank_rows_count_in_the_units_sloc_and_blank() {
13436        #[cfg(any(feature = "c", feature = "python", feature = "rust", feature = "tcl"))]
13437        const LEADING_BLANKS: u64 = 3;
13438        for (lang, body, ploc) in [
13439            (crate::LANG::Rust, &b"fn a() {}\n"[..], 1),
13440            (crate::LANG::Python, b"def a():\n    pass\n", 2),
13441            (crate::LANG::C, b"int f() { return 0; }\n", 1),
13442            (crate::LANG::Tcl, b"puts hi\n", 1),
13443        ] {
13444            let mut source = vec![b'\n'; LEADING_BLANKS as usize];
13445            source.extend_from_slice(body);
13446            let space = space_verbatim(lang, &source, MetricsOptions::default());
13447            let loc = &space.metrics.loc;
13448            assert_eq!(
13449                (loc.sloc(), loc.ploc(), loc.cloc(), loc.blank()),
13450                (LEADING_BLANKS + ploc, ploc, 0, LEADING_BLANKS),
13451                "{lang:?}: the leading rows are blank, not absent"
13452            );
13453            // The disagreement the issue is named for: the unit's own
13454            // reported span and its `sloc` are two spellings of one
13455            // number, and were not before.
13456            assert_eq!(
13457                loc.sloc() as usize,
13458                space.end_line - space.start_line + 1,
13459                "{lang:?}: sloc equals the rows of the unit's reported span"
13460            );
13461        }
13462    }
13463
13464    /// The two controls from #1247's evidence table. Both were already
13465    /// correct, and both are how the inconsistency was visible at all: a
13466    /// comment on line 1 flipped a byte-identical file from `sloc 1` to
13467    /// `sloc 4`, because comments are in the tree and blank rows are not.
13468    /// A fix that reached past the unit would move one of these.
13469    #[cfg(feature = "rust")]
13470    #[test]
13471    fn interior_blanks_and_leading_comments_are_unmoved_by_the_anchor() {
13472        // expected: rows 1 and 3 are code, row 2 is blank.
13473        let interior = rust_loc(b"fn a() {}\n\nfn b() {}\n");
13474        assert_eq!(
13475            (
13476                interior.sloc(),
13477                interior.ploc(),
13478                interior.cloc(),
13479                interior.blank()
13480            ),
13481            (3, 2, 0, 1)
13482        );
13483        // expected: row 1 is comment-only, rows 2-3 blank, row 4 code.
13484        let leading_comment = rust_loc(b"// c\n\n\nfn a() {}\n");
13485        assert_eq!(
13486            (
13487                leading_comment.sloc(),
13488                leading_comment.ploc(),
13489                leading_comment.cloc(),
13490                leading_comment.blank()
13491            ),
13492            (4, 1, 1, 2)
13493        );
13494    }
13495
13496    /// The anchor is gated on `SpaceKind::Unit`, and that gate is the
13497    /// entire separation between "the file starts at line 1" and "every
13498    /// space starts at line 1". Asserted on the nested space's `sloc` as
13499    /// well as its span, because only the `sloc` half is new.
13500    #[cfg(feature = "rust")]
13501    #[test]
13502    fn the_unit_anchor_does_not_reach_nested_spaces() {
13503        let space = space_verbatim(
13504            crate::LANG::Rust,
13505            b"\n\n\nfn a() {\n    let x = 1;\n}\n",
13506            MetricsOptions::default(),
13507        );
13508        assert_eq!((space.start_line, space.end_line), (1, 6));
13509        assert_eq!(space.metrics.loc.sloc(), 6, "the file has six rows");
13510        let nested = &space.spaces[0];
13511        assert_eq!((nested.start_line, nested.end_line), (4, 6));
13512        assert_eq!(
13513            nested.metrics.loc.sloc(),
13514            3,
13515            "the function's own three rows, not the file's six"
13516        );
13517        assert_eq!(nested.metrics.loc.blank(), 0, "the function has no blanks");
13518    }
13519
13520    /// `Sloc::exclude_span` records each pruned subtree's rows and
13521    /// `sloc()` subtracts their cardinality from the enclosing span, so
13522    /// widening that span at the top could in principle desynchronise
13523    /// the two. It cannot: the rows the anchor adds are above the first
13524    /// token, and no pruned subtree can overlap them. Pinned rather than
13525    /// argued, since the failure mode is a silent `saturating_sub` clamp
13526    /// to 0 rather than a panic (#722, #1247, #1417).
13527    #[cfg(feature = "rust")]
13528    #[test]
13529    fn exclude_tests_pruning_composes_with_the_unit_anchor() {
13530        // Rows 1-3 blank, 4 `fn a`, 5 blank, 6 `#[test]`, 7-9 `fn t`.
13531        let source = b"\n\n\nfn a() {}\n\n#[test]\nfn t() {\n    assert!(true);\n}\n";
13532        let kept = metrics_verbatim(
13533            crate::LANG::Rust,
13534            source,
13535            MetricsOptions::default().with_exclude_tests(true),
13536        )
13537        .loc;
13538        // The prune takes the `fn t` item's rows 7-9 and, since #1431,
13539        // the `#[test]` sibling on row 6 that marks it. What the anchor
13540        // decides is the other end: `blank` is 4 rather than 1, because
13541        // rows 1-3 are now inside the span the pruning subtracts from.
13542        assert_eq!(
13543            (kept.sloc(), kept.ploc(), kept.cloc(), kept.blank()),
13544            (5, 1, 0, 4),
13545            "the four pruned rows leave; the three leading blanks stay"
13546        );
13547
13548        let unpruned = rust_loc(source);
13549        assert_eq!(
13550            (unpruned.sloc(), unpruned.ploc(), unpruned.blank()),
13551            (9, 5, 4),
13552            "the same file unpruned — the anchor is what makes both blank counts 4"
13553        );
13554    }
13555
13556    /// Analyses `source` as Rust with `exclude_tests` on, byte-for-byte.
13557    /// `check_metrics` cannot express this: it trims and re-appends the
13558    /// trailing newline (destroying the row structure these fixtures are
13559    /// about) and its macro hard-binds `MetricsOptions`.
13560    ///
13561    /// Gated to match its three callers below, so a build without the
13562    /// Rust grammar has neither the tests nor an unused helper (#1446).
13563    #[cfg(feature = "rust")]
13564    fn rust_loc_pruned(source: &[u8]) -> Stats {
13565        metrics_verbatim(
13566            crate::LANG::Rust,
13567            source,
13568            crate::MetricsOptions::default().with_exclude_tests(true),
13569        )
13570        .loc
13571    }
13572
13573    /// #1417: `Sloc::exclude_span` recorded each pruned subtree's whole
13574    /// span as a *count*, so a row a retained sibling also occupies was
13575    /// subtracted anyway and `sloc` fell below `ploc`. Both one-liner
13576    /// spellings from the issue reported `sloc 0, ploc 1` — one row of
13577    /// code in a zero-row file.
13578    ///
13579    /// The pruned and unpruned readings are identical here, and that is
13580    /// the clearest statement of why: the whole row survives the prune,
13581    /// because `fn a()` is on it. `sloc 1` for a file that is more than
13582    /// half test code reads as under-subtraction and is not — nothing
13583    /// on that row can be removed without removing `fn a()` with it.
13584    ///
13585    /// A second, separate cause used to add a row here and is worth not
13586    /// confusing with this one: `should_skip_subtree` matched only the
13587    /// *item*, and a `#[cfg(test)]` / `#[test]` attribute is an
13588    /// `AttributeItem` sibling of it, so the attribute was walked
13589    /// normally and Rust's catch-all credited its start row to PLOC — a
13590    /// file that was 100% test code still reported `ploc 1` (#1431, now
13591    /// fixed). These fixtures put the attribute on the shared row, so
13592    /// that fix changes neither reading: what holds row 0 here is
13593    /// `fn a()`, not the attribute.
13594    #[cfg(feature = "rust")]
13595    #[test]
13596    fn a_pruned_item_sharing_a_row_with_retained_code_keeps_the_row() {
13597        for source in [
13598            &b"fn a() {} #[cfg(test)] mod t { #[test] fn x() {} }\n"[..],
13599            &b"#[cfg(test)] mod t { #[test] fn x() {} } fn a() {}\n"[..],
13600        ] {
13601            let text = String::from_utf8_lossy(source);
13602            let pruned = rust_loc_pruned(source);
13603            assert_eq!(
13604                (pruned.sloc(), pruned.ploc(), pruned.cloc(), pruned.blank()),
13605                (1, 1, 0, 0),
13606                "pruned {text:?}"
13607            );
13608
13609            let kept = rust_loc(source);
13610            assert_eq!(
13611                (kept.sloc(), kept.ploc(), kept.cloc(), kept.blank()),
13612                (1, 1, 0, 0),
13613                "unpruned {text:?} — the prune can remove no row here"
13614            );
13615        }
13616    }
13617
13618    /// The discriminator against the alternative fix #1417 rejected:
13619    /// clamping the excluded *count* to `span - |retained rows|`.
13620    ///
13621    /// On `shared_row` the truth is `sloc 3, ploc 2, blank 1`; before
13622    /// the fix it read `sloc 2, blank 0`; and the clamp reads
13623    /// `min(1, 3 - 2) = 1`, so `sloc 2, blank 0` — no improvement at
13624    /// all. It buys `ploc <= sloc` by moving the error into `blank`,
13625    /// which is the outcome #1398's rationale warns against. Measured
13626    /// against the built binary before the set-based fix landed, not
13627    /// reasoned about. Without this fixture the fix is indistinguishable
13628    /// from that alternative.
13629    ///
13630    /// `own_rows` is the control, in the same test because the two must
13631    /// not be confusable: it genuinely excludes rows — both of the
13632    /// pruned item's own, since #1431 took the attribute with it — where
13633    /// `shared_row` excludes none, so a fix cannot buy the invariant by
13634    /// under-subtracting in the ordinary rustfmt layout.
13635    ///
13636    /// **`shared_row` has no fixture-decay anchor, and none exists.**
13637    /// The rule in `.claude/rules/testing.md` asks for a second axis
13638    /// only the excluded construct contributes to, so that trimming it
13639    /// out of the fixture fails this test rather than quietly hollowing
13640    /// it. Deleting `#[cfg(test)] mod t {}` from row 0 leaves all four
13641    /// numbers at `(3, 2, 0, 1)` — measured, 0 of 3,390 lib tests fail
13642    /// — and that is not an oversight but the claim restated: a pruned
13643    /// item whose only row a retained sibling also occupies is *by
13644    /// definition* metrically indistinguishable from not being there.
13645    /// Every candidate axis collapses the same way. The unpruned
13646    /// reading is also `(3, 2, 0, 1)`, because `mod t {}` adds no row
13647    /// of its own; the space tree matches too, because the prune
13648    /// removes the space the construct would have opened. The coverage
13649    /// here is the production perturbation, which was run: this test is
13650    /// among the failures for dropping the `ploc` subtraction, for
13651    /// widening `exclude_span`'s bound by one, and for both `subtract`
13652    /// mutations. `own_rows` is anchored in the ordinary way — trim its
13653    /// pruned `mod` and `sloc 2` fails.
13654    #[cfg(feature = "rust")]
13655    #[test]
13656    fn a_blank_row_does_not_absorb_a_phantom_exclusion() {
13657        // Row 0 `fn a` and the pruned `mod t`, row 1 blank, row 2 `fn b`.
13658        let shared_row = rust_loc_pruned(b"fn a() {} #[cfg(test)] mod t {}\n\nfn b() {}\n");
13659        assert_eq!(
13660            (
13661                shared_row.sloc(),
13662                shared_row.ploc(),
13663                shared_row.cloc(),
13664                shared_row.blank()
13665            ),
13666            (3, 2, 0, 1),
13667            "nothing is excluded: row 0 carries `fn a`"
13668        );
13669
13670        // Row 0 `fn a`, row 1 blank, row 2 `#[cfg(test)]`, row 3 the
13671        // pruned `mod t` — rows 2 and 3 are both the prune's to take,
13672        // the attribute along with the item it marks (#1431).
13673        let own_rows = rust_loc_pruned(b"fn a() {}\n\n#[cfg(test)]\nmod t { #[test] fn x() {} }\n");
13674        assert_eq!(
13675            (
13676                own_rows.sloc(),
13677                own_rows.ploc(),
13678                own_rows.cloc(),
13679                own_rows.blank()
13680            ),
13681            (2, 1, 0, 1),
13682            "two of four rows excluded; only `fn a` and the blank stay"
13683        );
13684    }
13685
13686    /// The *comment* half of the settle, which the fixtures above cannot
13687    /// reach because every one of them has `cloc 0`.
13688    ///
13689    /// `Stats::settle_excluded_rows` subtracts three sets from the
13690    /// pruned rows — `ploc.lines` and both `Cloc` sets — and only the
13691    /// first was covered: deleting **both** comment subtractions failed
13692    /// 0 of 3,388 lib tests, while deleting the `ploc` one failed 3. A
13693    /// comment sharing a row with a pruned item is retained text on that
13694    /// row exactly as code would be, so the row must survive the prune.
13695    #[cfg(feature = "rust")]
13696    #[test]
13697    fn a_comment_sharing_a_pruned_items_row_keeps_that_row() {
13698        // Row 0 `#[cfg(test)]`, row 1 the pruned `mod t` and a trailing
13699        // comment. Both rows are pruned — the attribute since #1431 —
13700        // but row 1 holds retained text, so only row 0 is the prune's to
13701        // take. `sloc 1` against `sloc 0` is exactly the comment
13702        // subtraction: without it both rows go and a file with a
13703        // surviving comment reports no lines at all.
13704        let loc = rust_loc_pruned(b"#[cfg(test)]\nmod t {} // note\n");
13705        assert_eq!(loc.sloc(), 1, "the comment row survives the prune");
13706        assert_eq!(loc.cloc(), 1, "the trailing comment is still counted");
13707        assert_eq!(loc.ploc(), 0, "no production code is left on either row");
13708        assert!(
13709            loc.ploc() <= loc.sloc(),
13710            "ploc {} must not exceed sloc {}",
13711            loc.ploc(),
13712            loc.sloc()
13713        );
13714    }
13715
13716    /// The non-unit half of the same off-by-one. tree-sitter-perl's
13717    /// `function_definition` swallows the newline after the closing brace
13718    /// of a file's **last** `sub`, so that node's span ends at column 0 of
13719    /// a row it does not occupy. The old unconditional `+ 1` credited that
13720    /// row, inflating the last sub of every Perl file by one line — here,
13721    /// reporting a 3-row `sub` as 4.
13722    #[cfg(feature = "perl")]
13723    #[test]
13724    fn perl_last_sub_does_not_absorb_the_trailing_newline() {
13725        // Two identical 3-row subs; only the second hits the quirk.
13726        let space = space_verbatim(
13727            crate::LANG::Perl,
13728            b"sub f {\n    return 1;\n}\nsub g {\n    return 2;\n}\n",
13729            MetricsOptions::default(),
13730        );
13731        assert_eq!(space.metrics.loc.sloc(), 6, "the file has six rows");
13732        let subs: Vec<u64> = space
13733            .spaces
13734            .iter()
13735            .map(|child| child.metrics.loc.sloc())
13736            .collect();
13737        assert_eq!(subs, vec![3, 3], "both subs occupy three rows");
13738    }
13739
13740    /// #1398, whose mechanism [`Stats::clamp_line_sets_to_span`]
13741    /// documents: a recovery token starting one row past end-of-input
13742    /// reached a catch-all's leaf branch and became a line of code, so
13743    /// `ploc` came out one above `sloc`.
13744    ///
13745    /// The assertion is exact equality with the *unchanged* `sloc`, in
13746    /// both directions. `ploc <= sloc` alone would also be satisfied by
13747    /// a clamp wide enough to delete real code rows, and every fixture
13748    /// here is all-code, so `ploc == sloc` is the only passing value.
13749    ///
13750    /// Each row additionally asserts the tree still holds a node past
13751    /// the file's last row. Without that anchor every fixture decays
13752    /// into an ordinary all-code file the moment someone edits the
13753    /// source string, and `ploc == sloc` is then true of any
13754    /// well-formed input — measured: replacing all fifteen fixtures
13755    /// with well-formed sources of the same row count leaves this test
13756    /// green. The phantom node *is* the subject, so it is the axis to
13757    /// anchor on (`.claude/rules/testing.md`, "Perturb the fixture as
13758    /// well as the production line").
13759    ///
13760    /// Writing that anchor as `has_error()` — the obvious choice — is
13761    /// wrong, and finding out why is worth the paragraph: Ruby's
13762    /// `x = <<~DOC\na\n` parses with **no error node at all**, and
13763    /// still gets a zero-width `heredoc_end` at `(3, 1)` of a two-row
13764    /// file. A grammar need not report failure to hand back a span it
13765    /// cannot honour, which is half of why this defect survived.
13766    ///
13767    /// `metrics_verbatim`, not `check_metrics`, for a duller reason
13768    /// than #1051's: a `LANG`-keyed table cannot dispatch the generic
13769    /// `check_metrics::<T>` shim, and that shim's `fn` callback returns
13770    /// nothing. Every fixture here is already a fixed point of the
13771    /// trailing-newline normalisation, so — measured — a
13772    /// `check_metrics` twin of any of these rows fails against unfixed
13773    /// code too. This is *not* the EOF class both harnesses hide.
13774    #[cfg(any(
13775        feature = "bash",
13776        feature = "c",
13777        feature = "cpp",
13778        feature = "elixir",
13779        feature = "groovy",
13780        feature = "lua",
13781        feature = "mozcpp",
13782        feature = "objc",
13783        feature = "perl",
13784        feature = "ruby",
13785    ))]
13786    #[test]
13787    fn a_recovery_token_past_the_span_is_not_a_code_row() {
13788        // Every fixture that reproduced the defect, one row per
13789        // language, as `(language, source, rows the file has, ploc
13790        // before #1398)`. That last column is what makes this a
13791        // regression test rather than a restatement of the fixed
13792        // behaviour: a reviewer can see which row the assertion is
13793        // about, and it is one above `sloc` in every case.
13794        //
13795        // Four unrelated recovery shapes are represented, which is the
13796        // argument for one clamp over ten per-arm skips: a heredoc
13797        // whose terminator never arrives (Bash, Perl, Ruby), an
13798        // unterminated string delimiter (Elixir, Lua, Groovy), an
13799        // unclosed bracket (Ruby's `%w[`), and a dangling line
13800        // continuation (the C family, where the `\` splices a row that
13801        // is not there).
13802        const FIXTURES: &[(crate::LANG, &[u8], u64, u64)] = &[
13803            #[cfg(feature = "bash")]
13804            (crate::LANG::Bash, b"cat <<DOC\na\n", 2, 3),
13805            #[cfg(feature = "c")]
13806            (crate::LANG::C, b"int x = \\\n", 1, 2),
13807            #[cfg(feature = "cpp")]
13808            (crate::LANG::Cpp, b"int x = \\\n", 1, 2),
13809            #[cfg(feature = "mozcpp")]
13810            (crate::LANG::Mozcpp, b"int x = \\\n", 1, 2),
13811            #[cfg(feature = "objc")]
13812            (crate::LANG::Objc, b"int x = \\\n", 1, 2),
13813            // The issue's reproduction spells this `x = "abc\n\n\n`;
13814            // the trailing blank rows never reach the parser, because
13815            // `normalize_line_endings` collapses a run of trailing
13816            // newlines to one before any file-based caller sees it.
13817            #[cfg(feature = "elixir")]
13818            (crate::LANG::Elixir, b"x = \"abc\n", 1, 2),
13819            #[cfg(feature = "elixir")]
13820            (crate::LANG::Elixir, b"x = \"\"\"\nabc\n", 2, 3),
13821            #[cfg(feature = "elixir")]
13822            (crate::LANG::Elixir, b"x = ~s(\n", 1, 2),
13823            // Not a truncation, and the only row here reachable from a
13824            // file nobody cut short: Elixir comments with `#`, so a
13825            // C-style block comment is a whole-line parse failure. The
13826            // shape reaches the same phantom row, which is the point —
13827            // "malformed" is not a synonym for "truncated".
13828            #[cfg(feature = "elixir")]
13829            (crate::LANG::Elixir, b"/* block */\n", 1, 2),
13830            #[cfg(feature = "groovy")]
13831            (crate::LANG::Groovy, b"def x = \"\"\"abc\n", 1, 2),
13832            #[cfg(feature = "lua")]
13833            (crate::LANG::Lua, b"x = \"abc\n", 1, 2),
13834            #[cfg(feature = "lua")]
13835            (crate::LANG::Lua, b"x = 'abc\n", 1, 2),
13836            #[cfg(feature = "perl")]
13837            (crate::LANG::Perl, b"my $x = <<DOC;\na\n", 2, 3),
13838            #[cfg(feature = "ruby")]
13839            (crate::LANG::Ruby, b"x = <<~DOC\na\n", 2, 3),
13840            #[cfg(feature = "ruby")]
13841            (crate::LANG::Ruby, b"x = %w[\n", 1, 2),
13842        ];
13843
13844        crate::test_support::assert_fixtures_present(FIXTURES);
13845        for &(lang, source, rows, ploc_before_1398) in FIXTURES {
13846            let text = String::from_utf8_lossy(source);
13847            // The anchor: the tree still contains a node starting past
13848            // the file's last row. That node *is* this test's subject,
13849            // so an edit that repaired the fixture — or a grammar bump
13850            // that stopped emitting the phantom — fails here rather
13851            // than leaving the metric assertions true for an unrelated
13852            // reason.
13853            //
13854            // Not `has_error()`, which would be the obvious anchor and
13855            // is wrong: `x = <<~DOC\na\n` parses **cleanly** in Ruby,
13856            // phantom `heredoc_end` at `(3, 1)` and all. A clean parse
13857            // is not evidence of an in-span tree, which is half of why
13858            // this defect went unnoticed.
13859            let ast = crate::Ast::parse(crate::Source::new(lang, source))
13860                .expect("an enabled language parses, error tree or not");
13861            let mut stack = vec![ast.root_node()];
13862            let mut phantom = None;
13863            while let Some(node) = stack.pop() {
13864                if node.start_row() as u64 >= rows {
13865                    phantom = Some(node.start_row());
13866                    break;
13867                }
13868                stack.extend(node.children());
13869            }
13870            assert!(
13871                phantom.is_some(),
13872                "{lang:?}: {text:?} no longer produces a node past row {rows}, \
13873                 so it no longer exercises #1398"
13874            );
13875
13876            let loc = metrics_verbatim(lang, source, MetricsOptions::default()).loc;
13877            assert_eq!(loc.sloc(), rows, "{lang:?}: {text:?} occupies {rows} rows");
13878            assert_eq!(
13879                loc.ploc(),
13880                rows,
13881                "{lang:?}: every row of {text:?} is code and no row past it is \
13882                 (was {ploc_before_1398} before #1398)"
13883            );
13884            // One-sided, unlike the two above: every fixture is all
13885            // code, so this cannot catch a clamp that over-reaches into
13886            // the comment sets — an over-reach can only leave 0 at 0.
13887            // What it does pin is the other direction, a future change
13888            // that files the phantom row as a *comment* instead. The
13889            // CLOC half of the clamp is covered by
13890            // `an_unterminated_pod_block_does_not_comment_a_row_past_eof`,
13891            // which is its only guard.
13892            assert_eq!((loc.cloc(), loc.blank()), (0, 0), "{lang:?}: {text:?}");
13893        }
13894    }
13895
13896    /// The `span == 0` arm of [`Stats::clamp_line_sets_to_span`] clears
13897    /// both line sets, so its precondition — that no walk arrives here
13898    /// holding a row — is the thing worth pinning. Nothing else can:
13899    /// the two assertions at the end of that function compare against
13900    /// `span`, which the clear forces to `0 <= 0` whatever it destroyed.
13901    ///
13902    /// This test used to assert the clearing itself, seeding row 0
13903    /// because it is the one row a `(0, 0)` spelling would wrongly keep.
13904    /// That is now covered a layer down by
13905    /// `line_set::tests::retain_range_inverted_empties_the_set`, which
13906    /// seeds `[0, 1, 400]` and asserts row 0 specifically is gone — so
13907    /// the seed here is free to become what it always described: the
13908    /// state the guard exists to reject.
13909    ///
13910    /// A fixture cannot reach this branch with a populated set at all —
13911    /// `span == 0` needs a space opened on a genuinely zero-width node
13912    /// at column 0, and every `is_func_space` matches compound
13913    /// productions — so seeding `Stats` directly is the only shape that
13914    /// can (`.claude/rules/testing.md`, "Pair any end-to-end test with a
13915    /// direct unit test on the function whose contract is verified").
13916    #[test]
13917    #[cfg(debug_assertions)]
13918    #[should_panic(expected = "zero-span space reached the clamp holding")]
13919    fn a_zero_span_holding_rows_trips_the_guard() {
13920        let mut stats = Stats::default();
13921        // The empty file's span: `0..0`, covering no row at all.
13922        stats.init_unit_span(0, 0);
13923        stats.ploc.lines.insert(0);
13924        assert_eq!((stats.sloc(), stats.ploc()), (0, 1));
13925
13926        stats.clamp_line_sets_to_span();
13927    }
13928
13929    /// The companion to the guard above: an *empty* zero-span space is
13930    /// the state every real walk arrives in, and it must pass through
13931    /// the clamp untouched rather than trip the assertion.
13932    #[test]
13933    fn a_zero_span_without_rows_clamps_cleanly() {
13934        let mut stats = Stats::default();
13935        stats.init_unit_span(0, 0);
13936
13937        stats.clamp_line_sets_to_span();
13938
13939        assert_eq!(
13940            (stats.sloc(), stats.ploc(), stats.cloc()),
13941            (0, 0, 0),
13942            "a span of no rows retains no row"
13943        );
13944    }
13945
13946    /// The phantom row lands on whichever space was open when the
13947    /// recovery token was visited, so the contract has to hold per
13948    /// space and not only on the file. Elixir's unterminated `do` block
13949    /// is the case that separates the two: before #1398 the `defmodule`
13950    /// space reported `ploc 2` against its own `sloc 1`, and so did the
13951    /// unit, and a unit-only clamp would have left the nested space
13952    /// wrong while the file read as fixed.
13953    ///
13954    /// `space_verbatim`, not `metrics_verbatim`: the claim is about the
13955    /// nested space, whose numbers the root aggregate unions away.
13956    #[cfg(feature = "elixir")]
13957    #[test]
13958    fn the_clamp_reaches_a_nested_space_not_only_the_unit() {
13959        let space = space_verbatim(
13960            crate::LANG::Elixir,
13961            b"defmodule M do\n",
13962            MetricsOptions::default(),
13963        );
13964        assert_eq!(
13965            (space.metrics.loc.sloc(), space.metrics.loc.ploc()),
13966            (1, 1),
13967            "the unit is one row, all code"
13968        );
13969        let module = space
13970            .spaces
13971            .first()
13972            .expect("the `defmodule` opens a space even unterminated");
13973        assert_eq!(module.name.as_deref(), Some("M"));
13974        assert_eq!(
13975            (module.metrics.loc.sloc(), module.metrics.loc.ploc()),
13976            (1, 1),
13977            "the module's own space is one row too — `ploc` was 2 before #1398"
13978        );
13979    }
13980
13981    /// The same phantom row reaches CLOC, which has its own contract:
13982    /// `cloc > sloc` pushes MI's comments percentage above 100%, the
13983    /// defect #461 fixed for co-located comments and left open for a
13984    /// comment node ending past end-of-input. An unterminated Perl POD
13985    /// block is the reproduction the #1398 sweep turned up — two rows,
13986    /// `cloc 3`.
13987    ///
13988    /// Pinned alongside the PLOC sweep because the clamp covers all
13989    /// three line sets in one pass; without this row the CLOC half of
13990    /// `Stats::clamp_line_sets_to_span` has no test.
13991    #[cfg(feature = "perl")]
13992    #[test]
13993    fn an_unterminated_pod_block_does_not_comment_a_row_past_eof() {
13994        let loc =
13995            metrics_verbatim(crate::LANG::Perl, b"=pod\nabc\n", MetricsOptions::default()).loc;
13996        // expected: both rows are POD, so both are comment-only and
13997        // neither is code or blank. `cloc` was 3 before #1398.
13998        assert_eq!((loc.sloc(), loc.cloc()), (2, 2));
13999        assert_eq!((loc.ploc(), loc.blank()), (0, 0));
14000    }
14001
14002    /// #1135: Tcl and its iRules dialect are the only grammars here that
14003    /// surface the row terminator as a token child of the root. `LF`'s
14004    /// start row is the row it *terminates*, so the `_` catch-all in
14005    /// their `Loc` impls inserted that row into PLOC — turning every
14006    /// comment-only and whitespace-only row into a line of code.
14007    ///
14008    /// A wholly empty row never showed the defect: the `LF` that starts
14009    /// on it is the one tree-sitter collapses at end-of-input. A row of
14010    /// *whitespace* does, and trailing whitespace on an otherwise blank
14011    /// line is ordinary in real source — which is what makes the two
14012    /// spellings' disagreement the sharpest assertion here.
14013    ///
14014    /// Uses [`metrics_verbatim`] so the fixtures reach the parser
14015    /// byte-for-byte; `check_metrics` rewrites the trailing newline.
14016    #[cfg(all(feature = "irules", feature = "tcl"))]
14017    #[test]
14018    fn tcl_family_does_not_count_terminator_rows_as_code() {
14019        for lang in [crate::LANG::Tcl, crate::LANG::Irules] {
14020            // Three rows: code, whitespace-only, code.
14021            let padded = metrics_verbatim(
14022                lang,
14023                b"proc f {} {}\n   \nproc g {} {}\n",
14024                MetricsOptions::default(),
14025            )
14026            .loc;
14027            assert_eq!(padded.sloc(), 3, "{lang:?} sloc");
14028            assert_eq!(padded.ploc(), 2, "{lang:?} ploc");
14029            assert_eq!(padded.blank(), 1, "{lang:?} blank");
14030
14031            // Whether a blank row carries spaces is not a property of the
14032            // code, so the empty-row spelling must agree exactly. Pre-fix
14033            // this side stayed correct while the padded side reported
14034            // `ploc 3 / blank 0`.
14035            let empty = metrics_verbatim(
14036                lang,
14037                b"proc f {} {}\n\nproc g {} {}\n",
14038                MetricsOptions::default(),
14039            )
14040            .loc;
14041            assert_eq!(
14042                (padded.sloc(), padded.ploc(), padded.blank()),
14043                (empty.sloc(), empty.ploc(), empty.blank()),
14044                "{lang:?}: trailing whitespace on a blank row must not make it code"
14045            );
14046
14047            // The comment-only half of the same defect. It escaped
14048            // `unterminated_one_line_file_reports_one_source_line`
14049            // because that sweep's fixtures are one-liners with no
14050            // comment row. The cross-language version of this case is
14051            // `a_comment_row_is_never_counted_as_code`; the two rows here
14052            // stay so the Tcl-family regression reads in one place.
14053            let commented = metrics_verbatim(
14054                lang,
14055                b"# lead-in\nproc f {} {}\n",
14056                MetricsOptions::default(),
14057            )
14058            .loc;
14059            assert_eq!(commented.sloc(), 2, "{lang:?} commented sloc");
14060            assert_eq!(commented.ploc(), 1, "{lang:?} commented ploc");
14061            assert_eq!(commented.cloc(), 1, "{lang:?} commented cloc");
14062            assert_eq!(commented.blank(), 0, "{lang:?} commented blank");
14063        }
14064    }
14065
14066    /// The comment spellings to sweep for each language: the line form
14067    /// every language has, then the block and doc forms where one exists.
14068    ///
14069    /// Nothing here needs to be exhaustive per language — the defect this
14070    /// guards against is a stray *token* inside (or terminating) a
14071    /// comment node reaching a PLOC catch-all, which any one spelling of
14072    /// a comment exposes. The block and doc entries are there because
14073    /// those nodes have child tokens the line form does not.
14074    #[cfg(any(
14075        feature = "bash",
14076        feature = "c",
14077        feature = "cpp",
14078        feature = "csharp",
14079        feature = "elixir",
14080        feature = "go",
14081        feature = "groovy",
14082        feature = "irules",
14083        feature = "java",
14084        feature = "javascript",
14085        feature = "kotlin",
14086        feature = "lua",
14087        feature = "mozcpp",
14088        feature = "mozjs",
14089        feature = "objc",
14090        feature = "perl",
14091        feature = "php",
14092        feature = "python",
14093        feature = "ruby",
14094        feature = "rust",
14095        feature = "tcl",
14096        feature = "typescript",
14097    ))]
14098    fn comment_spellings(lang: crate::LANG) -> &'static [&'static str] {
14099        use crate::LANG::*;
14100        match lang {
14101            Python | Ruby | Bash | Elixir | Tcl | Irules | Perl => &["# c"],
14102            Lua => &["-- c", "--[[ c ]]"],
14103            Php => &["# c", "/* c */", "/** c */"],
14104            Rust => &["// c", "/* c */", "/// c"],
14105            _ => &["// c", "/* c */"],
14106        }
14107    }
14108
14109    /// The entries of [`comment_spellings`] that are documentation to
14110    /// the language's own tool, and so bill a `dloc` row as well as a
14111    /// `cloc` one (#1597). Only two of the spellings above are doc
14112    /// markers anywhere; the full per-tool table lives in `dloc.rs`.
14113    #[cfg(any(
14114        feature = "bash",
14115        feature = "c",
14116        feature = "cpp",
14117        feature = "csharp",
14118        feature = "elixir",
14119        feature = "go",
14120        feature = "groovy",
14121        feature = "irules",
14122        feature = "java",
14123        feature = "javascript",
14124        feature = "kotlin",
14125        feature = "lua",
14126        feature = "mozcpp",
14127        feature = "mozjs",
14128        feature = "objc",
14129        feature = "perl",
14130        feature = "php",
14131        feature = "python",
14132        feature = "ruby",
14133        feature = "rust",
14134        feature = "tcl",
14135        feature = "typescript",
14136    ))]
14137    fn doc_comment_spellings(lang: crate::LANG) -> &'static [&'static str] {
14138        use crate::LANG::*;
14139        match lang {
14140            Php => &["/** c */"],
14141            Rust => &["/// c"],
14142            _ => &[],
14143        }
14144    }
14145
14146    /// A comment-only row is never a physical line of code — in any
14147    /// language, in any comment spelling, on either side of the code.
14148    ///
14149    /// Two separate defects broke this, both by letting a token reach the
14150    /// `_` catch-all that ends `stats.ploc.lines.insert(start)`. In Tcl
14151    /// and iRules it was the row terminator, whose start row is the row
14152    /// it terminates (#1135). In Perl it was the `#` *inside* the
14153    /// `comments` node, which additionally tripped
14154    /// `check_comment_ends_on_code_line` into reclassifying the row from
14155    /// comment-only to code-and-comment (#1137).
14156    ///
14157    /// Neither was visible to the per-language `*_cloc` tests — several
14158    /// assert `cloc` and `blank` and leave `ploc` unpinned — nor to
14159    /// `unterminated_one_line_file_reports_one_source_line`, whose
14160    /// fixtures carry no comment row at all. Since the failure mode is
14161    /// structural rather than language-specific, the sweep is per
14162    /// language rather than a sample.
14163    #[cfg(all(
14164        feature = "bash",
14165        feature = "c",
14166        feature = "cpp",
14167        feature = "csharp",
14168        feature = "elixir",
14169        feature = "go",
14170        feature = "groovy",
14171        feature = "irules",
14172        feature = "java",
14173        feature = "javascript",
14174        feature = "kotlin",
14175        feature = "lua",
14176        feature = "mozcpp",
14177        feature = "mozjs",
14178        feature = "objc",
14179        feature = "perl",
14180        feature = "php",
14181        feature = "python",
14182        feature = "ruby",
14183        feature = "rust",
14184        feature = "tcl",
14185        feature = "typescript",
14186    ))]
14187    #[test]
14188    fn a_comment_row_is_never_counted_as_code() {
14189        for (lang, code) in UNTERMINATED_ONE_LINERS {
14190            // PHP is the one language whose code fixture must open the
14191            // file: outside `<?php` every row is inline HTML, so a
14192            // comment placed before it is not a comment at all.
14193            let must_lead = *lang == crate::LANG::Php;
14194            for &comment in comment_spellings(*lang) {
14195                let orders: &[bool] = if must_lead { &[false] } else { &[true, false] };
14196                for &comment_first in orders {
14197                    let mut src = Vec::new();
14198                    let (first, second): (&[u8], &[u8]) = if comment_first {
14199                        (comment.as_bytes(), code)
14200                    } else {
14201                        (code, comment.as_bytes())
14202                    };
14203                    src.extend_from_slice(first);
14204                    src.push(b'\n');
14205                    src.extend_from_slice(second);
14206                    src.push(b'\n');
14207
14208                    let loc = metrics_verbatim(*lang, &src, MetricsOptions::default()).loc;
14209                    let text = String::from_utf8_lossy(&src);
14210                    assert_eq!(loc.sloc(), 2, "{lang:?} sloc for {text:?}");
14211                    assert_eq!(
14212                        loc.ploc(),
14213                        1,
14214                        "{lang:?} ploc for {text:?} — the comment row is not code"
14215                    );
14216                    assert_eq!(loc.cloc(), 1, "{lang:?} cloc for {text:?}");
14217                    assert_eq!(loc.blank(), 0, "{lang:?} blank for {text:?}");
14218                    // A comment row is a documentation row exactly when its
14219                    // spelling is a doc marker to the language's tool, or —
14220                    // in Go and Ruby — when it sits directly above a doc
14221                    // target (#1597). `doc_comment_spellings` says which of
14222                    // the spellings above are markers, and this sweep pins
14223                    // both answers for every grammar at once; `dloc.rs` has
14224                    // the per-tool pitfalls. Go's `package main` and Ruby's
14225                    // `def f; end` are doc targets, so a comment directly
14226                    // above either is a lead comment and documentation by
14227                    // position; after the code it is a dangling comment and
14228                    // is not. Rust and PHP bind a doc comment to the
14229                    // declaration after it (#1613), so one at the end of the
14230                    // file is not one.
14231                    let positional =
14232                        comment_first && matches!(lang, crate::LANG::Go | crate::LANG::Ruby);
14233                    let bound_dangling =
14234                        !comment_first && matches!(lang, crate::LANG::Rust | crate::LANG::Php);
14235                    let expected = u64::from(
14236                        (doc_comment_spellings(*lang).contains(&comment) && !bound_dangling)
14237                            || positional,
14238                    );
14239                    assert_eq!(
14240                        loc.dloc(),
14241                        expected,
14242                        "{lang:?} dloc for {text:?} — a doc marker, or a Go / Ruby lead comment"
14243                    );
14244                }
14245            }
14246        }
14247    }
14248
14249    /// A `var` / `let` / `const` declaration is an executable statement and
14250    /// counts one LLOC, the same as Java's `LocalVariableDeclaration` and
14251    /// Rust's `let` (#1283 — before the fix a declarations-only file
14252    /// reported `lloc 0`). The fourth row is one `variable_declaration`
14253    /// carrying two declarators, so it counts once, not twice. The fifth row
14254    /// is a `using_declaration` — the grammar's third executable declaration
14255    /// kind, which TypeScript and TSX do not have.
14256    #[cfg(feature = "javascript")]
14257    #[test]
14258    fn javascript_declaration_lloc() {
14259        check_metrics::<JavascriptParser>(
14260            "var a = 1;\nlet b = 2;\nconst c = 3;\nvar d = 4, e = 5;\nusing r = open();\n",
14261            "foo.js",
14262            |metric| {
14263                assert_eq!(metric.loc.sloc(), 5);
14264                assert_eq!(metric.loc.ploc(), 5);
14265                assert_eq!(metric.loc.lloc(), 5);
14266                assert_eq!(metric.loc.cloc(), 0);
14267                assert_eq!(metric.loc.blank(), 0);
14268            },
14269        );
14270    }
14271
14272    /// The classic `for (var i = 0; …)` header is part of the `ForStatement`,
14273    /// which already counts its own LLOC, so the header declaration must not
14274    /// add a second one. `for (const x of …)` and `for (var k in …)` need no
14275    /// carve-out at all: the grammar inlines the `const` / `var` keyword into
14276    /// `for_in_statement` and emits no declaration node — they are here so a
14277    /// carve-out wrongly widened to `ForInStatement` still has an input that
14278    /// notices. `var s = i;` in the loop *body* is a real logical line: the
14279    /// `StatementBlock` stops the ancestor walk (#1283).
14280    ///
14281    /// expected: for-statement 1 + body declaration 1 + for-of 1 + for-in 1 = 4
14282    #[cfg(feature = "javascript")]
14283    #[test]
14284    fn javascript_for_header_declaration_not_double_counted() {
14285        check_metrics::<JavascriptParser>(
14286            "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",
14287            "foo.js",
14288            |metric| {
14289                assert_eq!(metric.loc.sloc(), 7);
14290                assert_eq!(metric.loc.ploc(), 7);
14291                assert_eq!(metric.loc.lloc(), 4);
14292                assert_eq!(metric.loc.cloc(), 0);
14293                assert_eq!(metric.loc.blank(), 0);
14294            },
14295        );
14296    }
14297
14298    /// `export const a = 1;` is one logical line, not two: the declaration
14299    /// nests inside the `ExportStatement`, whose arm already counted the row.
14300    /// The declaration inside the exported function body still counts — the
14301    /// `StatementBlock` stops the ancestor walk before the `ExportStatement`
14302    /// is reached (#1283).
14303    ///
14304    /// expected: 4 export statements + the `const c = 4;` in `f`'s body = 5
14305    #[cfg(feature = "javascript")]
14306    #[test]
14307    fn javascript_exported_declaration_counts_once() {
14308        check_metrics::<JavascriptParser>(
14309            "export const a = 1;\nexport let b = 2;\nexport default 3;\nexport function f() { const c = 4; }\n",
14310            "foo.js",
14311            |metric| {
14312                assert_eq!(metric.loc.sloc(), 4);
14313                assert_eq!(metric.loc.ploc(), 4);
14314                assert_eq!(metric.loc.lloc(), 5);
14315                assert_eq!(metric.loc.cloc(), 0);
14316                assert_eq!(metric.loc.blank(), 0);
14317            },
14318        );
14319    }
14320
14321    /// A `var` / `let` / `const` declaration is an executable statement and
14322    /// counts one LLOC, the same as Java's `LocalVariableDeclaration` and
14323    /// Rust's `let` (#1283 — before the fix a declarations-only file
14324    /// reported `lloc 0`). The fourth row is one `variable_declaration`
14325    /// carrying two declarators, so it counts once, not twice. The fifth row
14326    /// is a `using_declaration` — the grammar's third executable declaration
14327    /// kind, which TypeScript and TSX do not have.
14328    #[cfg(feature = "mozjs")]
14329    #[test]
14330    fn mozjs_declaration_lloc() {
14331        check_metrics::<MozjsParser>(
14332            "var a = 1;\nlet b = 2;\nconst c = 3;\nvar d = 4, e = 5;\nusing r = open();\n",
14333            "foo.js",
14334            |metric| {
14335                assert_eq!(metric.loc.sloc(), 5);
14336                assert_eq!(metric.loc.ploc(), 5);
14337                assert_eq!(metric.loc.lloc(), 5);
14338                assert_eq!(metric.loc.cloc(), 0);
14339                assert_eq!(metric.loc.blank(), 0);
14340            },
14341        );
14342    }
14343
14344    /// The classic `for (var i = 0; …)` header is part of the `ForStatement`,
14345    /// which already counts its own LLOC, so the header declaration must not
14346    /// add a second one. `for (const x of …)` and `for (var k in …)` need no
14347    /// carve-out at all: the grammar inlines the `const` / `var` keyword into
14348    /// `for_in_statement` and emits no declaration node — they are here so a
14349    /// carve-out wrongly widened to `ForInStatement` still has an input that
14350    /// notices. `var s = i;` in the loop *body* is a real logical line: the
14351    /// `StatementBlock` stops the ancestor walk (#1283).
14352    ///
14353    /// expected: for-statement 1 + body declaration 1 + for-of 1 + for-in 1 = 4
14354    #[cfg(feature = "mozjs")]
14355    #[test]
14356    fn mozjs_for_header_declaration_not_double_counted() {
14357        check_metrics::<MozjsParser>(
14358            "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",
14359            "foo.js",
14360            |metric| {
14361                assert_eq!(metric.loc.sloc(), 7);
14362                assert_eq!(metric.loc.ploc(), 7);
14363                assert_eq!(metric.loc.lloc(), 4);
14364                assert_eq!(metric.loc.cloc(), 0);
14365                assert_eq!(metric.loc.blank(), 0);
14366            },
14367        );
14368    }
14369
14370    /// `export const a = 1;` is one logical line, not two: the declaration
14371    /// nests inside the `ExportStatement`, whose arm already counted the row.
14372    /// The declaration inside the exported function body still counts — the
14373    /// `StatementBlock` stops the ancestor walk before the `ExportStatement`
14374    /// is reached (#1283).
14375    ///
14376    /// expected: 4 export statements + the `const c = 4;` in `f`'s body = 5
14377    #[cfg(feature = "mozjs")]
14378    #[test]
14379    fn mozjs_exported_declaration_counts_once() {
14380        check_metrics::<MozjsParser>(
14381            "export const a = 1;\nexport let b = 2;\nexport default 3;\nexport function f() { const c = 4; }\n",
14382            "foo.js",
14383            |metric| {
14384                assert_eq!(metric.loc.sloc(), 4);
14385                assert_eq!(metric.loc.ploc(), 4);
14386                assert_eq!(metric.loc.lloc(), 5);
14387                assert_eq!(metric.loc.cloc(), 0);
14388                assert_eq!(metric.loc.blank(), 0);
14389            },
14390        );
14391    }
14392
14393    /// A `var` / `let` / `const` declaration is an executable statement and
14394    /// counts one LLOC, the same as Java's `LocalVariableDeclaration` and
14395    /// Rust's `let` (#1283 — before the fix a declarations-only file
14396    /// reported `lloc 0`). The fourth row is one `variable_declaration`
14397    /// carrying two declarators, so it counts once, not twice.
14398    #[cfg(feature = "typescript")]
14399    #[test]
14400    fn typescript_declaration_lloc() {
14401        check_metrics::<TypescriptParser>(
14402            "var a: number = 1;\nlet b = 2;\nconst c = 3;\nvar d = 4, e = 5;\n",
14403            "foo.ts",
14404            |metric| {
14405                assert_eq!(metric.loc.sloc(), 4);
14406                assert_eq!(metric.loc.ploc(), 4);
14407                assert_eq!(metric.loc.lloc(), 4);
14408                assert_eq!(metric.loc.cloc(), 0);
14409                assert_eq!(metric.loc.blank(), 0);
14410            },
14411        );
14412    }
14413
14414    /// The classic `for (var i = 0; …)` header is part of the `ForStatement`,
14415    /// which already counts its own LLOC, so the header declaration must not
14416    /// add a second one. `for (const x of …)` and `for (var k in …)` need no
14417    /// carve-out at all: the grammar inlines the `const` / `var` keyword into
14418    /// `for_in_statement` and emits no declaration node — they are here so a
14419    /// carve-out wrongly widened to `ForInStatement` still has an input that
14420    /// notices. `var s = i;` in the loop *body* is a real logical line: the
14421    /// `StatementBlock` stops the ancestor walk (#1283).
14422    ///
14423    /// expected: for-statement 1 + body declaration 1 + for-of 1 + for-in 1 = 4
14424    #[cfg(feature = "typescript")]
14425    #[test]
14426    fn typescript_for_header_declaration_not_double_counted() {
14427        check_metrics::<TypescriptParser>(
14428            "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",
14429            "foo.ts",
14430            |metric| {
14431                assert_eq!(metric.loc.sloc(), 7);
14432                assert_eq!(metric.loc.ploc(), 7);
14433                assert_eq!(metric.loc.lloc(), 4);
14434                assert_eq!(metric.loc.cloc(), 0);
14435                assert_eq!(metric.loc.blank(), 0);
14436            },
14437        );
14438    }
14439
14440    /// `export const a = 1;` is one logical line, not two: the declaration
14441    /// nests inside the `ExportStatement`, whose arm already counted the row.
14442    /// The declaration inside the exported function body still counts — the
14443    /// `StatementBlock` stops the ancestor walk before the `ExportStatement`
14444    /// is reached (#1283).
14445    ///
14446    /// expected: 4 export statements + the `const c = 4;` in `f`'s body = 5
14447    ///
14448    /// The TypeScript spelling also pins `export declare const …`, where an
14449    /// `ambient_declaration` sits between the export and the declaration: the
14450    /// carve-out walks the ancestor chain rather than checking the parent, so
14451    /// it still sees the enclosing `ExportStatement`.
14452    #[cfg(feature = "typescript")]
14453    #[test]
14454    fn typescript_exported_declaration_counts_once() {
14455        check_metrics::<TypescriptParser>(
14456            "export const a: number = 1;\nexport declare const b: string;\nexport default 3;\nexport function f(): void { const c = 4; }\n",
14457            "foo.ts",
14458            |metric| {
14459                assert_eq!(metric.loc.sloc(), 4);
14460                assert_eq!(metric.loc.ploc(), 4);
14461                assert_eq!(metric.loc.lloc(), 5);
14462                assert_eq!(metric.loc.cloc(), 0);
14463                assert_eq!(metric.loc.blank(), 0);
14464            },
14465        );
14466    }
14467
14468    /// A `var` / `let` / `const` declaration is an executable statement and
14469    /// counts one LLOC, the same as Java's `LocalVariableDeclaration` and
14470    /// Rust's `let` (#1283 — before the fix a declarations-only file
14471    /// reported `lloc 0`). The fourth row is one `variable_declaration`
14472    /// carrying two declarators, so it counts once, not twice.
14473    #[cfg(feature = "typescript")]
14474    #[test]
14475    fn tsx_declaration_lloc() {
14476        check_metrics::<TsxParser>(
14477            "var a: number = 1;\nlet b = 2;\nconst c = 3;\nvar d = 4, e = 5;\n",
14478            "foo.tsx",
14479            |metric| {
14480                assert_eq!(metric.loc.sloc(), 4);
14481                assert_eq!(metric.loc.ploc(), 4);
14482                assert_eq!(metric.loc.lloc(), 4);
14483                assert_eq!(metric.loc.cloc(), 0);
14484                assert_eq!(metric.loc.blank(), 0);
14485            },
14486        );
14487    }
14488
14489    /// The classic `for (var i = 0; …)` header is part of the `ForStatement`,
14490    /// which already counts its own LLOC, so the header declaration must not
14491    /// add a second one. `for (const x of …)` and `for (var k in …)` need no
14492    /// carve-out at all: the grammar inlines the `const` / `var` keyword into
14493    /// `for_in_statement` and emits no declaration node — they are here so a
14494    /// carve-out wrongly widened to `ForInStatement` still has an input that
14495    /// notices. `var s = i;` in the loop *body* is a real logical line: the
14496    /// `StatementBlock` stops the ancestor walk (#1283).
14497    ///
14498    /// expected: for-statement 1 + body declaration 1 + for-of 1 + for-in 1 = 4
14499    #[cfg(feature = "typescript")]
14500    #[test]
14501    fn tsx_for_header_declaration_not_double_counted() {
14502        check_metrics::<TsxParser>(
14503            "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",
14504            "foo.tsx",
14505            |metric| {
14506                assert_eq!(metric.loc.sloc(), 7);
14507                assert_eq!(metric.loc.ploc(), 7);
14508                assert_eq!(metric.loc.lloc(), 4);
14509                assert_eq!(metric.loc.cloc(), 0);
14510                assert_eq!(metric.loc.blank(), 0);
14511            },
14512        );
14513    }
14514
14515    /// `export const a = 1;` is one logical line, not two: the declaration
14516    /// nests inside the `ExportStatement`, whose arm already counted the row.
14517    /// The declaration inside the exported function body still counts — the
14518    /// `StatementBlock` stops the ancestor walk before the `ExportStatement`
14519    /// is reached (#1283).
14520    ///
14521    /// expected: 4 export statements + the `const c = 4;` in `f`'s body = 5
14522    ///
14523    /// The TypeScript spelling also pins `export declare const …`, where an
14524    /// `ambient_declaration` sits between the export and the declaration: the
14525    /// carve-out walks the ancestor chain rather than checking the parent, so
14526    /// it still sees the enclosing `ExportStatement`.
14527    #[cfg(feature = "typescript")]
14528    #[test]
14529    fn tsx_exported_declaration_counts_once() {
14530        check_metrics::<TsxParser>(
14531            "export const a: number = 1;\nexport declare const b: string;\nexport default 3;\nexport function f(): void { const c = 4; }\n",
14532            "foo.tsx",
14533            |metric| {
14534                assert_eq!(metric.loc.sloc(), 4);
14535                assert_eq!(metric.loc.ploc(), 4);
14536                assert_eq!(metric.loc.lloc(), 5);
14537                assert_eq!(metric.loc.cloc(), 0);
14538                assert_eq!(metric.loc.blank(), 0);
14539            },
14540        );
14541    }
14542
14543    /// A brace-less `for` body is a declaration with the *same* parent as
14544    /// the header, so a carve-out keyed on the enclosing kind alone
14545    /// dropped it (#1283 review): `for (…) var s = i;` counted one
14546    /// logical line where the braced spelling and `for (…) x++;` count
14547    /// two. The header is identified by the `initializer` field instead.
14548    /// The nested spelling — a `switch` as the brace-less body — is the
14549    /// same miss one level down.
14550    ///
14551    /// expected: for 1 + body declaration 1 = 2; for 1 + switch 1 +
14552    /// case declaration 1 = 3.
14553    #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
14554    #[test]
14555    fn js_family_braceless_for_body_declaration_counts() {
14556        #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
14557        const BODY: &str = "for (var i = 0; i < 3; i++) var s = i;\n";
14558        #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
14559        const NESTED: &str = "for (let i = 0; i < 2; i++) switch (i) { case 0: let y = 1; }\n";
14560        check_metrics::<JavascriptParser>(BODY, "foo.js", |m| assert_eq!(m.loc.lloc(), 2));
14561        check_metrics::<MozjsParser>(BODY, "foo.js", |m| assert_eq!(m.loc.lloc(), 2));
14562        check_metrics::<TypescriptParser>(BODY, "foo.ts", |m| assert_eq!(m.loc.lloc(), 2));
14563        check_metrics::<TsxParser>(BODY, "foo.tsx", |m| assert_eq!(m.loc.lloc(), 2));
14564        check_metrics::<JavascriptParser>(NESTED, "foo.js", |m| assert_eq!(m.loc.lloc(), 3));
14565        check_metrics::<MozjsParser>(NESTED, "foo.js", |m| assert_eq!(m.loc.lloc(), 3));
14566        check_metrics::<TypescriptParser>(NESTED, "foo.ts", |m| assert_eq!(m.loc.lloc(), 3));
14567        check_metrics::<TsxParser>(NESTED, "foo.tsx", |m| assert_eq!(m.loc.lloc(), 3));
14568    }
14569
14570    /// Ambient declarations execute nothing — `declare const x: T;` has
14571    /// no initializer to run — so they are no logical line, whether the
14572    /// `declare` is top-level or the declaration sits inside a
14573    /// `declare namespace` / `declare module` body (#1283 review: only
14574    /// the `export declare` spelling was carved out, so a `.d.ts` file
14575    /// reported one LLOC per `declare const`).
14576    ///
14577    /// expected: 0 — every row is ambient.
14578    #[cfg(feature = "typescript")]
14579    #[test]
14580    fn typescript_ambient_declarations_are_not_logical_lines() {
14581        const SRC: &str = "declare const VERSION: string;\ndeclare let mutable: number;\ndeclare namespace NS { const inner: number; }\ndeclare module \"m\" { let y: string; }\n";
14582        check_metrics::<TypescriptParser>(SRC, "foo.ts", |m| assert_eq!(m.loc.lloc(), 0));
14583        check_metrics::<TsxParser>(SRC, "foo.tsx", |m| assert_eq!(m.loc.lloc(), 0));
14584    }
14585}