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