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