big_code_analysis/metrics/loc.rs
1// Per-language metric and AST modules deliberately consume the macro-
2// generated tree-sitter token enums via `use crate::*` and `use Foo::*`
3// inside match expressions — explicit imports would list dozens of
4// variants per arm and obscure the per-language token sets that are the
5// point of these files. Allowed at the module level rather than per
6// function so the per-language impl blocks stay readable.
7#![allow(
8 clippy::enum_glob_use,
9 clippy::match_same_arms,
10 clippy::struct_field_names,
11 clippy::wildcard_imports
12)]
13// Metric counts (token, function, branch, argument, etc.) are stored as
14// `usize` and crossed with `f64` averages, ratios, and Halstead scores
15// across the cyclomatic / MI / Halstead computations. The `usize as f64`
16// and `f64 as usize` casts are intentional and snapshot-anchored — every
17// site is bounded by the count it came from. Allowing the lints at the
18// module level keeps the metric arithmetic legible.
19#![allow(
20 clippy::cast_precision_loss,
21 clippy::cast_possible_truncation,
22 clippy::cast_sign_loss
23)]
24// `Loc` is the one metric that computes on tree-sitter *span
25// coordinates* rather than on its own accumulators, and a row index
26// arriving from a parse is attacker-controlled through the source
27// layout. #1051 was a `usize` underflow of exactly this shape — a Rust
28// doc comment at EOF drove `end - 1` below zero, panicking in debug and
29// wrapping to `usize::MAX` in release, from an input as small as
30// `/// x`. Warning here forces every span adjustment to be explicitly
31// saturating, checked, or annotated, rather than relying on a bound
32// that holds only until a grammar changes shape.
33//
34// Deliberately scoped to `loc` rather than to `src/metrics/`: the other
35// metrics contribute 244 hits, all of them `+=` on their own counters,
36// which is not this bug class and would bury it (#1152).
37#![warn(clippy::arithmetic_side_effects)]
38
39use crate::checker::Checker;
40use crate::lang_helpers::python::python_is_block;
41use std::fmt;
42
43use crate::macros::implement_metric_trait;
44use crate::*;
45
46// Collapse the `usize::MAX` sentinel that `*_min` fields are
47// initialised to on `Default` into `0`, so a never-observed space
48// serializes to a meaningful number rather than `18446744073709551615`.
49// Mirrors `tokens::Stats::tokens_min`'s guard.
50#[inline]
51fn min_or_zero(v: usize) -> u64 {
52 if v == usize::MAX { 0 } else { v as u64 }
53}
54
55/// Number of physical source rows covered by a span running from the
56/// 0-based `start_row` to the 1-based inclusive `end_line`.
57///
58/// The subtlety is not here — it is in which row counts as the last
59/// one, and that rule lives once, in [`Node::end_line`]: a span whose
60/// end position sits at column 0 stops *before* that row contributes a
61/// single character, so the row belongs to whatever follows. Both
62/// callers take `end_line` straight from the node, so this is plain
63/// subtraction rather than a second copy of the rule.
64///
65/// Keying on the end column rather than on "is this the unit?" is what
66/// fixed issue #1067 here and #1163 in `Node`: the unit/non-unit split
67/// assumed the unit always ends at column 0 (false for un-newline-
68/// terminated input, which then lost a row) and that nothing else ever
69/// does (false for Perl, whose last `sub` gained one).
70///
71/// Requires `end_line >= start_row`, which tree-sitter guarantees for a
72/// single node's own span. The `debug_assert` pins that in tests; the
73/// `saturating_sub` decides what release does if it is ever violated
74/// anyway. Zero is the right answer there — an inverted span covers no
75/// rows. Note what this does *not* buy: `sloc()` already clamps with
76/// `saturating_sub`, so a wrapped value could not have escaped as
77/// `usize::MAX` either. It would have escaped as `sloc: 0` for a
78/// non-empty file, and on into MI's SLOC term — a wrong number surfacing
79/// far from its cause, which is how #1051 was reported.
80#[inline]
81fn span_rows(start_row: usize, end_line: usize) -> usize {
82 debug_assert!(
83 end_line >= start_row,
84 "span_rows: end_line {end_line} < start_row {start_row}"
85 );
86 end_line.saturating_sub(start_row)
87}
88
89mod line_set;
90use line_set::LineSet;
91
92/// The `SLoc` metric suite.
93#[derive(Debug, Clone, PartialEq)]
94pub struct Sloc {
95 start: usize,
96 // 1-based inclusive last line of the span, from `Node::end_line`.
97 // Storing the resolved line rather than the raw end row plus its
98 // column keeps the "does the final row count" rule in one place.
99 end_line: usize,
100 // Physical rows removed from this space's span by `exclude_tests`
101 // pruning. `sloc` is the lone loc sub-metric computed by span
102 // subtraction rather than node-by-node accumulation, so a pruned
103 // subtree (which a `continue` in the walk suppresses for every
104 // accumulated metric) leaves the span untouched. We record each
105 // pruned subtree's rows here and subtract the cardinality in
106 // `sloc()` so SLOC drops in step with `ploc`/`cloc`/`lloc`
107 // (issue #722).
108 //
109 // A set rather than the counter this was until #1417, because rows
110 // shared with retained code have to be identifiable and removed
111 // before the subtraction — see `Stats::settle_excluded_rows`. It is
112 // the single source of truth; there is no cached count to fall out
113 // of step with it, matching what `ploc`/`cloc` already gave up
114 // when they became bitsets (#1109).
115 excluded_rows: LineSet,
116 sloc_min: usize,
117 sloc_max: usize,
118}
119
120impl Default for Sloc {
121 fn default() -> Self {
122 Self {
123 start: 0,
124 end_line: 0,
125 excluded_rows: LineSet::default(),
126 sloc_min: usize::MAX,
127 sloc_max: 0,
128 }
129 }
130}
131
132impl Sloc {
133 /// The `SLOC` metric value for this space (source lines, including blanks and comments).
134 #[inline]
135 #[must_use]
136 pub fn sloc(&self) -> u64 {
137 // This metric counts the number of physical lines this space
138 // occupies, including blanks and comments.
139 let span = span_rows(self.start, self.end_line);
140 // Subtract the rows belonging to `exclude_tests`-pruned subtrees
141 // (issue #722). `saturating_sub` is defensive: the set is
142 // clamped to the span and its rows are a subset of it, but a
143 // future caller that records a row outside the span must not
144 // wrap to `u64::MAX`.
145 span.saturating_sub(self.excluded_rows.len()) as u64
146 }
147
148 /// Records the rows of a pruned (`exclude_tests`) subtree so that
149 /// `sloc()` drops in step with the node-accumulated loc sub-metrics.
150 /// The arguments are the pruned node's own start row and
151 /// `Node::end_line`, so the rows recorded follow the same rule the
152 /// enclosing span was measured with.
153 ///
154 /// A *set* rather than the running count this kept until #1417. A
155 /// pruned item can share its first or last physical row with a
156 /// retained sibling — `fn a() {} #[cfg(test)] mod t { … }` is one
157 /// row, of which nothing is removed — and a count cannot tell that
158 /// row from one the prune really took, so `sloc()` fell below
159 /// `ploc()`. The set also makes two pruned siblings on one row
160 /// subtract that row once instead of twice.
161 ///
162 /// The rows recorded here are the pruned node's *whole* span;
163 /// [`Stats::settle_excluded_rows`] is what takes the shared ones
164 /// back out, once the retained sets for this space are complete.
165 #[inline]
166 pub(crate) fn exclude_span(&mut self, start_row: usize, end_line: usize) {
167 // `end_line` is the 1-based inclusive last row, so the last
168 // 0-based row is `end_line - 1`. Guarding here rather than
169 // leaning on `insert_range`'s inverted-span return, which would
170 // need `end_line - 1` computed first: at `end_line == 0` that
171 // saturates to row 0 and records a row the node does not
172 // occupy, and an empty span is not an inverted one.
173 if end_line <= start_row {
174 debug_assert!(
175 end_line == start_row,
176 "exclude_span: end_line {end_line} < start_row {start_row}"
177 );
178 return;
179 }
180 // Exact under the guard — `end_line > start_row >= 0` gives
181 // `end_line >= 1` — and saturating for the module's
182 // `arithmetic_side_effects` warning, which cannot see that.
183 // The row count inserted is `span_rows(start_row, end_line)`,
184 // byte-identical to the count this used to add.
185 self.excluded_rows
186 .insert_range(start_row, end_line.saturating_sub(1));
187 }
188
189 /// The `Sloc` metric minimum value. See `min_or_zero` for the
190 /// `usize::MAX` sentinel guard.
191 #[inline]
192 #[must_use]
193 pub fn sloc_min(&self) -> u64 {
194 min_or_zero(self.sloc_min)
195 }
196
197 /// The `Sloc` metric maximum value.
198 #[inline]
199 #[must_use]
200 pub fn sloc_max(&self) -> u64 {
201 self.sloc_max as u64
202 }
203
204 /// Folds `other` into `self`, updating the min/max accumulators and
205 /// unioning the child's `exclude_tests`-pruned rows.
206 #[inline]
207 pub fn merge(&mut self, other: &Sloc) {
208 // Fold the child's own min/max (not its aggregate `sloc()`), so the
209 // granularity of deeply nested function spaces propagates to the
210 // root. This matches every sibling metric (cyclomatic, cognitive,
211 // exit, nargs, nom, tokens, abc) and fixed issue #437.
212 self.sloc_min = self.sloc_min.min(other.sloc_min);
213 self.sloc_max = self.sloc_max.max(other.sloc_max);
214
215 // Propagate the child's pruned rows upward so an ancestor's
216 // span-based `sloc()` drops by the same rows, mirroring how `Ploc`
217 // unions its line-set upward (`Ploc::merge`). The prune hook records
218 // each pruned subtree's span only on its innermost enclosing
219 // func-space; without this fold a `#[test] fn` inside a retained
220 // `impl`/`trait`/closure would shrink only that space's `sloc`,
221 // leaving every enclosing space (including the unit, which feeds
222 // MI's SLOC term) inflated (issue #741, #722 follow-up). Each
223 // ancestor's span already includes the pruned rows exactly once, so
224 // subtracting them once per level cannot double-count: pruned
225 // subtrees never descend, so a nested pruned item is recorded on a
226 // single space and folded up one altitude at a time.
227 //
228 // Union rather than the `saturating_add` this was until #1417.
229 // Addition double-counted two pruned siblings sharing one
230 // physical row, and — the reason the fold is safe to leave
231 // exactly as it is — the child arrives here already settled
232 // against its own retained rows, which the parent's later
233 // `settle_excluded_rows` can only narrow further:
234 // `child_retained ⊆ parent_retained`, so
235 // `(X \ child_retained) \ parent_retained == X \ parent_retained`.
236 // The order the two settle in therefore does not matter.
237 self.excluded_rows.union_with(&other.excluded_rows);
238 }
239
240 #[inline]
241 pub(crate) fn compute_minmax(&mut self) {
242 // Fold this space's own span unconditionally so containers (Unit,
243 // classes) participate in min/max, matching the sibling metrics'
244 // convention. Each space runs this before being merged upward, so
245 // the guarded form previously here dropped nested leaves (#437).
246 self.sloc_min = self.sloc_min.min(self.sloc() as usize);
247 self.sloc_max = self.sloc_max.max(self.sloc() as usize);
248 }
249}
250
251/// The `PLoc` metric suite.
252#[derive(Debug, Clone, PartialEq)]
253pub struct Ploc {
254 lines: LineSet,
255 ploc_min: usize,
256 ploc_max: usize,
257}
258
259impl Default for Ploc {
260 fn default() -> Self {
261 Self {
262 lines: LineSet::default(),
263 ploc_min: usize::MAX,
264 ploc_max: 0,
265 }
266 }
267}
268
269impl Ploc {
270 /// The `PLOC` metric value for this space (physical lines of code, excluding blanks and comments).
271 #[inline]
272 #[must_use]
273 pub fn ploc(&self) -> u64 {
274 // This metric counts the number of instruction lines in a code
275 // https://en.wikipedia.org/wiki/Source_lines_of_code
276 self.lines.len() as u64
277 }
278
279 /// The `Ploc` metric minimum value. See `min_or_zero` for the
280 /// `usize::MAX` sentinel guard.
281 #[inline]
282 #[must_use]
283 pub fn ploc_min(&self) -> u64 {
284 min_or_zero(self.ploc_min)
285 }
286
287 /// The `Ploc` metric maximum value.
288 #[inline]
289 #[must_use]
290 pub fn ploc_max(&self) -> u64 {
291 self.ploc_max as u64
292 }
293
294 /// Folds `other` into `self`, unioning the line set and updating min/max.
295 #[inline]
296 pub fn merge(&mut self, other: &Ploc) {
297 // Union the child's physical lines in, so a line shared with a
298 // sibling space is counted once. A word-wise OR rather than an
299 // insert per row: a line inside D nested spaces is folded upward
300 // D times (#1109).
301 self.lines.union_with(&other.lines);
302
303 // Fold the child's own min/max so nested spaces propagate (#437).
304 self.ploc_min = self.ploc_min.min(other.ploc_min);
305 self.ploc_max = self.ploc_max.max(other.ploc_max);
306 }
307
308 #[inline]
309 pub(crate) fn compute_minmax(&mut self) {
310 // Fold this space's own value unconditionally so containers
311 // participate, matching the sibling metrics' convention (#437).
312 // Bound once: `ploc()` is a popcount over the whole word array
313 // since #1109, not the O(1) `HashSet::len` it used to be.
314 let ploc = self.ploc() as usize;
315 self.ploc_min = self.ploc_min.min(ploc);
316 self.ploc_max = self.ploc_max.max(ploc);
317 }
318}
319
320/// The `CLoc` metric suite.
321#[derive(Debug, Clone, PartialEq)]
322pub struct Cloc {
323 // Physical lines that are comment-only (no code). Feeds both
324 // `cloc()` and the `blank` metric (`sloc - ploc - only.len()`).
325 // A set rather than a counter so two standalone block comments on
326 // one physical line (`/*a*/ /*b*/`) contribute a single comment
327 // line, not one per node (issue #461 follow-up). Each spanned row
328 // of a genuine multi-line block comment is a distinct key, so it
329 // still counts once per line.
330 only_comment_line_starts: LineSet,
331 // Physical lines carrying both code and comment (`int x; /*c*/`).
332 // A line with several inline block comments (`f(int /*a*/, int
333 // /*b*/)`) must contribute a single comment line, not one per
334 // node, otherwise cloc can exceed sloc/ploc and push the MI
335 // comments_percentage above 100% (issue #461). Mirrors `Ploc`'s
336 // per-line de-dup via `Ploc::lines`; disjoint from
337 // `only_comment_line_starts` by construction.
338 code_comment_line_starts: LineSet,
339 comment_line_end: Option<usize>,
340 cloc_min: usize,
341 cloc_max: usize,
342}
343
344impl Default for Cloc {
345 fn default() -> Self {
346 Self {
347 only_comment_line_starts: LineSet::default(),
348 code_comment_line_starts: LineSet::default(),
349 comment_line_end: Option::default(),
350 cloc_min: usize::MAX,
351 cloc_max: 0,
352 }
353 }
354}
355
356impl Cloc {
357 /// The `CLOC` metric value for this space (comment lines, standalone + trailing).
358 #[inline]
359 #[must_use]
360 pub fn cloc(&self) -> u64 {
361 // Comments are counted regardless of their placement
362 // https://en.wikipedia.org/wiki/Source_lines_of_code
363 //
364 // Derive from the per-physical-line sets rather than summed
365 // counters so co-located comments (standalone or inline) count
366 // their shared line once and a comment line shared across
367 // merged spaces is not double-counted (issue #461). The two
368 // sets are disjoint by construction, but a union is used
369 // defensively so a stray overlap cannot inflate the count.
370 self.only_comment_line_starts
371 .union_len(&self.code_comment_line_starts) as u64
372 }
373
374 /// The `Cloc` metric minimum value. See `min_or_zero` for the
375 /// `usize::MAX` sentinel guard.
376 #[inline]
377 #[must_use]
378 pub fn cloc_min(&self) -> u64 {
379 min_or_zero(self.cloc_min)
380 }
381
382 /// The `Cloc` metric maximum value.
383 #[inline]
384 #[must_use]
385 pub fn cloc_max(&self) -> u64 {
386 self.cloc_max as u64
387 }
388
389 /// Folds `other` into `self`, summing comment counts and updating min/max.
390 #[inline]
391 pub fn merge(&mut self, other: &Cloc) {
392 // Union both per-line sets so a comment line shared across
393 // merged spaces is counted once (mirrors `Ploc`'s line union).
394 self.only_comment_line_starts
395 .union_with(&other.only_comment_line_starts);
396 self.code_comment_line_starts
397 .union_with(&other.code_comment_line_starts);
398
399 // Fold the child's own min/max so nested spaces propagate (#437).
400 self.cloc_min = self.cloc_min.min(other.cloc_min);
401 self.cloc_max = self.cloc_max.max(other.cloc_max);
402 }
403
404 #[inline]
405 pub(crate) fn compute_minmax(&mut self) {
406 // Fold this space's own value unconditionally so containers
407 // participate, matching the sibling metrics' convention (#437).
408 // Bound once: `cloc()` is a `union_len` over both word arrays
409 // since #1109, so calling it twice is six array scans per space.
410 let cloc = self.cloc() as usize;
411 self.cloc_min = self.cloc_min.min(cloc);
412 self.cloc_max = self.cloc_max.max(cloc);
413 }
414}
415
416/// The `LLoc` metric suite.
417#[derive(Debug, Clone, PartialEq)]
418pub struct Lloc {
419 logical_lines: usize,
420 lloc_min: usize,
421 lloc_max: usize,
422}
423
424impl Default for Lloc {
425 fn default() -> Self {
426 Self {
427 logical_lines: 0,
428 lloc_min: usize::MAX,
429 lloc_max: 0,
430 }
431 }
432}
433
434impl Lloc {
435 /// The `LLOC` metric value for this space (logical statements).
436 #[inline]
437 #[must_use]
438 pub fn lloc(&self) -> u64 {
439 // This metric counts the number of statements in a code
440 // https://en.wikipedia.org/wiki/Source_lines_of_code
441 self.logical_lines as u64
442 }
443
444 /// The `Lloc` metric minimum value. See `min_or_zero` for the
445 /// `usize::MAX` sentinel guard.
446 #[inline]
447 #[must_use]
448 pub fn lloc_min(&self) -> u64 {
449 min_or_zero(self.lloc_min)
450 }
451
452 /// The `Lloc` metric maximum value.
453 #[inline]
454 #[must_use]
455 pub fn lloc_max(&self) -> u64 {
456 self.lloc_max as u64
457 }
458
459 /// Records one logical statement.
460 ///
461 /// Exists so the 23 per-language `Loc` impls name the operation
462 /// instead of each reaching into a private field, which also keeps
463 /// the module's `arithmetic_side_effects` carve-out to this one
464 /// line rather than 36 of them (#1152). Saturating is unreachable —
465 /// the count is bounded by the AST's node count — and is the right
466 /// answer if it ever were: a pinned `usize::MAX` is a visibly broken
467 /// LLOC, where a wrap to 0 reads as a legitimately empty space.
468 #[inline]
469 fn count_logical_line(&mut self) {
470 self.logical_lines = self.logical_lines.saturating_add(1);
471 }
472
473 /// Folds `other` into `self`, summing statement counts and updating min/max.
474 #[inline]
475 pub fn merge(&mut self, other: &Lloc) {
476 // Merge lloc lines
477 self.logical_lines = self.logical_lines.saturating_add(other.logical_lines);
478 // Fold the child's own min/max so nested spaces propagate (#437).
479 self.lloc_min = self.lloc_min.min(other.lloc_min);
480 self.lloc_max = self.lloc_max.max(other.lloc_max);
481 }
482
483 #[inline]
484 pub(crate) fn compute_minmax(&mut self) {
485 // Fold this space's own value unconditionally so containers
486 // participate, matching the sibling metrics' convention (#437).
487 self.lloc_min = self.lloc_min.min(self.lloc() as usize);
488 self.lloc_max = self.lloc_max.max(self.lloc() as usize);
489 }
490}
491
492/// The `Loc` metric suite.
493#[derive(Debug, Clone, PartialEq)]
494#[non_exhaustive]
495pub struct Stats {
496 sloc: Sloc,
497 ploc: Ploc,
498 cloc: Cloc,
499 lloc: Lloc,
500 space_count: usize,
501 blank_min: usize,
502 blank_max: usize,
503}
504
505impl Default for Stats {
506 fn default() -> Self {
507 Self {
508 sloc: Sloc::default(),
509 ploc: Ploc::default(),
510 cloc: Cloc::default(),
511 lloc: Lloc::default(),
512 space_count: 1,
513 blank_min: usize::MAX,
514 blank_max: 0,
515 }
516 }
517}
518
519impl fmt::Display for Stats {
520 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
521 write!(
522 f,
523 "sloc: {}, ploc: {}, lloc: {}, cloc: {}, blank: {}, sloc_average: {}, ploc_average: {}, lloc_average: {}, cloc_average: {}, blank_average: {}, sloc_min: {}, sloc_max: {}, cloc_min: {}, cloc_max: {}, ploc_min: {}, ploc_max: {}, lloc_min: {}, lloc_max: {}, blank_min: {}, blank_max: {}",
524 self.sloc(),
525 self.ploc(),
526 self.lloc(),
527 self.cloc(),
528 self.blank(),
529 self.sloc_average(),
530 self.ploc_average(),
531 self.lloc_average(),
532 self.cloc_average(),
533 self.blank_average(),
534 self.sloc_min(),
535 self.sloc_max(),
536 self.cloc_min(),
537 self.cloc_max(),
538 self.ploc_min(),
539 self.ploc_max(),
540 self.lloc_min(),
541 self.lloc_max(),
542 self.blank_min(),
543 self.blank_max(),
544 )
545 }
546}
547
548impl Stats {
549 /// Test-only constructor that forces a degenerate `cloc`/`sloc`
550 /// pair (here `cloc > sloc`) so callers in other metric modules can
551 /// exercise downstream clamps without depending on the parsing
552 /// pipeline. `cloc <= sloc` always holds for parsed input after
553 /// issue #461, so this state is unreachable through normal use.
554 #[cfg(test)]
555 pub(crate) fn with_cloc_sloc(code_comment_lines: usize, sloc_end_row: usize) -> Self {
556 let mut stats = Stats::default();
557 stats.sloc.start = 0;
558 // `end_row + 1`: the synthetic span models a real one ending
559 // mid-line, so the final row counts and `sloc == sloc_end_row + 1`.
560 stats.sloc.end_line = sloc_end_row.saturating_add(1);
561 // Inject `code_comment_lines` distinct synthetic code-comment
562 // rows. An offset past `sloc_end_row` keeps them disjoint from
563 // any real span row, so `cloc()` (the set's cardinality) equals
564 // the requested count without colliding with sloc attribution.
565 if code_comment_lines > 0 {
566 let synthetic_base = sloc_end_row.saturating_add(1);
567 // Explicit rather than leaning on `insert_range`'s inverted-span
568 // guard: that guard exists to survive a bug, not to serve as a
569 // caller's empty case. The `- 1` is exact under the `> 0` test
570 // above, which is also what makes the inclusive end well-formed.
571 let synthetic_end = synthetic_base
572 .saturating_add(code_comment_lines)
573 .saturating_sub(1);
574 stats
575 .cloc
576 .code_comment_line_starts
577 .insert_range(synthetic_base, synthetic_end);
578 }
579 stats
580 }
581
582 /// Merges a second `Loc` metric suite into the first one
583 pub fn merge(&mut self, other: &Stats) {
584 self.sloc.merge(&other.sloc);
585 self.ploc.merge(&other.ploc);
586 self.cloc.merge(&other.cloc);
587 self.lloc.merge(&other.lloc);
588
589 // Count spaces
590 self.space_count = self.space_count.saturating_add(other.space_count);
591
592 // Fold the child's own min/max so nested spaces propagate (#437).
593 self.blank_min = self.blank_min.min(other.blank_min);
594 self.blank_max = self.blank_max.max(other.blank_max);
595 }
596
597 /// Records an `exclude_tests`-pruned subtree's span so this space's
598 /// `sloc()` excludes those physical lines, matching the
599 /// node-accumulated loc sub-metrics that the pruning already drops
600 /// (issue #722). Called from the walker for the space enclosing each
601 /// skipped subtree.
602 #[inline]
603 pub(crate) fn exclude_test_span(&mut self, start_row: usize, end_line: usize) {
604 self.sloc.exclude_span(start_row, end_line);
605 }
606
607 /// The `Sloc` metric.
608 ///
609 /// Counts the number of lines in a scope
610 #[inline]
611 #[must_use]
612 pub fn sloc(&self) -> u64 {
613 self.sloc.sloc()
614 }
615
616 /// The `Ploc` metric.
617 ///
618 /// Counts the number of instruction lines in a scope
619 #[inline]
620 #[must_use]
621 pub fn ploc(&self) -> u64 {
622 self.ploc.ploc()
623 }
624
625 /// The `Lloc` metric.
626 ///
627 /// Counts the number of statements in a scope
628 #[inline]
629 #[must_use]
630 pub fn lloc(&self) -> u64 {
631 self.lloc.lloc()
632 }
633
634 /// The `Cloc` metric.
635 ///
636 /// Counts the number of comments in a scope
637 #[inline]
638 #[must_use]
639 pub fn cloc(&self) -> u64 {
640 self.cloc.cloc()
641 }
642
643 /// The `Blank` metric.
644 ///
645 /// Counts the number of blank lines in a scope
646 #[inline]
647 #[must_use]
648 pub fn blank(&self) -> u64 {
649 // `sloc - ploc - only_comment_lines` can go negative when a space's
650 // physical and comment line attribution overlaps the span row count
651 // (e.g. single-line bodies). `saturating_sub` clamps at 0 (matching
652 // the prior `.max(0.0)` on the f64 form) so the serialized value is
653 // never negative (#437).
654 self.sloc()
655 .saturating_sub(self.ploc())
656 .saturating_sub(self.cloc.only_comment_line_starts.len() as u64)
657 }
658
659 /// The `Sloc` metric average value.
660 ///
661 /// This value is computed dividing the `Sloc` value for the number of spaces
662 #[inline]
663 #[must_use]
664 pub fn sloc_average(&self) -> f64 {
665 crate::metrics::average(self.sloc() as f64, self.space_count)
666 }
667
668 /// The `Ploc` metric average value.
669 ///
670 /// This value is computed dividing the `Ploc` value for the number of spaces
671 #[inline]
672 #[must_use]
673 pub fn ploc_average(&self) -> f64 {
674 crate::metrics::average(self.ploc() as f64, self.space_count)
675 }
676
677 /// The `Lloc` metric average value.
678 ///
679 /// This value is computed dividing the `Lloc` value for the number of spaces
680 #[inline]
681 #[must_use]
682 pub fn lloc_average(&self) -> f64 {
683 crate::metrics::average(self.lloc() as f64, self.space_count)
684 }
685
686 /// The `Cloc` metric average value.
687 ///
688 /// This value is computed dividing the `Cloc` value for the number of spaces
689 #[inline]
690 #[must_use]
691 pub fn cloc_average(&self) -> f64 {
692 crate::metrics::average(self.cloc() as f64, self.space_count)
693 }
694
695 /// The `Blank` metric average value.
696 ///
697 /// This value is computed dividing the `Blank` value for the number of spaces
698 #[inline]
699 #[must_use]
700 pub fn blank_average(&self) -> f64 {
701 crate::metrics::average(self.blank() as f64, self.space_count)
702 }
703
704 /// The `Sloc` metric minimum value.
705 #[inline]
706 #[must_use]
707 pub fn sloc_min(&self) -> u64 {
708 self.sloc.sloc_min()
709 }
710
711 /// The `Sloc` metric maximum value.
712 #[inline]
713 #[must_use]
714 pub fn sloc_max(&self) -> u64 {
715 self.sloc.sloc_max()
716 }
717
718 /// The `Cloc` metric minimum value.
719 #[inline]
720 #[must_use]
721 pub fn cloc_min(&self) -> u64 {
722 self.cloc.cloc_min()
723 }
724
725 /// The `Cloc` metric maximum value.
726 #[inline]
727 #[must_use]
728 pub fn cloc_max(&self) -> u64 {
729 self.cloc.cloc_max()
730 }
731
732 /// The `Ploc` metric minimum value.
733 #[inline]
734 #[must_use]
735 pub fn ploc_min(&self) -> u64 {
736 self.ploc.ploc_min()
737 }
738
739 /// The `Ploc` metric maximum value.
740 #[inline]
741 #[must_use]
742 pub fn ploc_max(&self) -> u64 {
743 self.ploc.ploc_max()
744 }
745
746 /// The `Lloc` metric minimum value.
747 #[inline]
748 #[must_use]
749 pub fn lloc_min(&self) -> u64 {
750 self.lloc.lloc_min()
751 }
752
753 /// The `Lloc` metric maximum value.
754 #[inline]
755 #[must_use]
756 pub fn lloc_max(&self) -> u64 {
757 self.lloc.lloc_max()
758 }
759
760 /// The `Blank` metric minimum value. See `min_or_zero` for the
761 /// `usize::MAX` sentinel guard.
762 #[inline]
763 #[must_use]
764 pub fn blank_min(&self) -> u64 {
765 min_or_zero(self.blank_min)
766 }
767
768 /// The `Blank` metric maximum value.
769 #[inline]
770 #[must_use]
771 pub fn blank_max(&self) -> u64 {
772 self.blank_max as u64
773 }
774
775 #[inline]
776 pub(crate) fn compute_minmax(&mut self) {
777 self.sloc.compute_minmax();
778 self.ploc.compute_minmax();
779 self.cloc.compute_minmax();
780 self.lloc.compute_minmax();
781
782 // Fold this space's own blank value unconditionally so containers
783 // participate, matching the sibling metrics' convention (#437).
784 // `blank()` returns a `u64` already clamped at 0 by `saturating_sub`,
785 // so the widening `as usize` cast is lossless (64-bit) and cannot
786 // introduce a spurious value here. Bound once: `blank()` popcounts
787 // two word arrays since #1109.
788 let blank = self.blank() as usize;
789 self.blank_min = self.blank_min.min(blank);
790 self.blank_max = self.blank_max.max(blank);
791 }
792
793 pub(crate) fn init_unit_span(&mut self, start: usize, end_line: usize) {
794 self.sloc.start = start;
795 self.sloc.end_line = end_line;
796 }
797
798 /// Drops every recorded PLOC / CLOC row that lies outside this
799 /// space's own `sloc` row span.
800 ///
801 /// That bounds `ploc` and `cloc` by the span, which is what the
802 /// `debug_assert!` below states. It is *not* the same as pinning
803 /// the public `ploc() <= sloc()`: `sloc()` subtracts
804 /// `exclude_tests`-pruned rows, and can still come out below `ploc`
805 /// — see the assertion's comment and #1417.
806 ///
807 /// A grammar's error recovery synthesises **zero-width tokens one
808 /// row past the last row the file has**: an unterminated Bash
809 /// heredoc gets a `heredoc_end` at `(3, 1)..(3, 1)` in a two-row
810 /// file, an unterminated Elixir / Lua / Groovy string gets its
811 /// closing quote the same way. Those tokens are childless, so they
812 /// reach the leaf branch of a language's catch-all arm, which
813 /// inserts a raw `start` row — and here that `start` *is* the
814 /// phantom row. [`Node::end_line`] already encodes "a node whose
815 /// end column is 0 does not occupy the row it ends on"; nothing
816 /// encoded the same rule for a node that *begins* past the span, nor
817 /// applied it to the raw end row `init` hands the comment arms — so a
818 /// comment ending at column 0 of the row past the last one (Perl POD
819 /// running to end-of-file without `=cut`) counted that row as a
820 /// comment. The clamp removes both.
821 ///
822 /// `ploc > sloc` is a contract violation rather than a rounding
823 /// artifact: [`Stats::blank`] saturates at 0, so the clamp there
824 /// hides it while a consumer computing `ploc / sloc` gets a ratio
825 /// above 1. Ten of the twenty-odd languages reproduced it on
826 /// truncated input before #1398 — a per-arm skip would have been
827 /// ten edits that still could not cover a grammar whose recovery
828 /// shape nobody sampled, so the rule lives once, here, keyed on the
829 /// span every language already reports.
830 ///
831 /// **Input whose tree stays inside the file's rows is untouched, and
832 /// a clean parse does not guarantee that**: Perl POD at end of file
833 /// and Ruby's unterminated `<<~` heredoc both parse without an error
834 /// node and still move. Over the `pdf.js`, `DeepSpeech` and
835 /// `serde` corpora exactly one of 1,876 files moves, and it is a
836 /// real bug fixed rather than a real row lost:
837 /// `DeepSpeech/parse_valgrind_suppressions.sh` leaves a MISSING `}`
838 /// at `(58, 1)` of a 57-row file, so its `ploc` drops 36 → 35 and
839 /// `blank` rises 6 → 7. No snapshot covers it — `snapshots/` holds
840 /// only the C-family files — so the integration suite is silent
841 /// about the one case that proves the fix reaches ordinary trees.
842 /// Do not read a green snapshot run as evidence of no effect.
843 ///
844 /// The clamp's one latent hazard is the mirror image: a unit whose
845 /// span is *shorter* than the file. That happens today for a file
846 /// ending in a blank row (`"package m\n\n"` reports `unit 1..1`),
847 /// and is harmless only because a trailing blank row reaches
848 /// neither line set. Nothing pins `unit.end_line >= line_count` —
849 /// `tests/parity/space_span_containment.rs` asserts
850 /// `unit == (1, line_count)`, but none of its fixtures ends in a
851 /// blank row — so a grammar that ever credited such a row to PLOC
852 /// would have it deleted here silently.
853 pub(crate) fn clamp_line_sets_to_span(&mut self) {
854 let start = self.sloc.start;
855 let span = span_rows(start, self.sloc.end_line);
856 // `end_line` is the 1-based inclusive last row of the span, so
857 // the last 0-based row it covers is `end_line - 1` — exact
858 // wherever the span is non-empty, which is what `span == 0`
859 // selects against. `(1, 0)` is the inverted range
860 // `LineSet::retain_range` reads as "keep nothing", and it is
861 // what the empty file's `0..0` needs: retaining `0..=0` there
862 // would keep a row the file does not have.
863 //
864 // That last branch is defensive rather than live. The three
865 // walks that reach `span == 0` (an empty file, and the two
866 // whitespace-only root contracts) all arrive with both line
867 // sets already empty, so `(0, 0)` here is byte-identical over
868 // every corpus input — measured. `a_zero_span_keeps_no_row`
869 // below is therefore a direct `Stats` test rather than a
870 // fixture, per `.claude/rules/testing.md`: it is the only shape
871 // that can tell the two spellings apart.
872 //
873 // The corpus measurement is the weaker half of that claim, and
874 // `line_set`'s module header retracts one of exactly this shape
875 // as having been wrong *and* load-bearing (#1398). The stronger
876 // half is structural: `span == 0` needs a space opened on a
877 // genuinely zero-width node at column 0, and every language's
878 // `is_func_space` matches compound productions — never a raw
879 // token, never `is_error` / `is_missing` — while a MISSING node
880 // is always a single terminal.
881 //
882 // Either way the absence is not what should hold the branch,
883 // because `retain_range(1, 0)` *clears* both line sets: whatever
884 // arrived is gone, and the parent never recovers it, since a
885 // child is clamped before `Ploc::merge` lifts it. That clear is
886 // also what makes the two assertions at the end of this function
887 // vacuous here — they compare against `span`, so both read
888 // `0 <= 0` no matter what was destroyed. Assert before the
889 // clear, where the question can still be answered.
890 let (first, last) = if span == 0 {
891 debug_assert!(
892 self.ploc() == 0 && self.cloc() == 0,
893 "zero-span space reached the clamp holding {} ploc / {} cloc \
894 row(s); clearing them here would lose them silently",
895 self.ploc(),
896 self.cloc()
897 );
898 (1, 0)
899 } else {
900 (start, self.sloc.end_line.saturating_sub(1))
901 };
902 self.ploc.lines.retain_range(first, last);
903 self.cloc.only_comment_line_starts.retain_range(first, last);
904 self.cloc.code_comment_line_starts.retain_range(first, last);
905 // The fourth set is the `exclude_tests`-pruned rows, and it is
906 // clamped for the same reason as the other three rather than
907 // because anything is known to put a row outside the span
908 // there: a pruned node lies inside the space that recorded it,
909 // so this is expected to be a no-op. What it buys is the
910 // premise `X ⊆ span` that makes `settle_excluded_rows`'s
911 // `ploc() <= sloc()` a theorem rather than an observation —
912 // without it a stray row would turn `sloc()` negative-by-
913 // saturation and fire an assertion in the wrong pass (#1417).
914 self.sloc.excluded_rows.retain_range(first, last);
915
916 // The invariant the clamp establishes, asserted on the path
917 // every walk takes for every space rather than only on the
918 // fixtures the regression tests name — 821 of the workspace's
919 // tests reach it.
920 //
921 // Against the span, deliberately, and kept that way now that
922 // [`Stats::settle_excluded_rows`] asserts the stronger
923 // `ploc() <= sloc()` form (#1417). The two say different
924 // things: this pair localises a failure to the clamp itself,
925 // where `sloc()`'s subtrahend is not yet settled and could not
926 // be blamed.
927 //
928 // `ploc()` and `cloc()` popcount their word arrays (#1109):
929 // O(words) per space, the same order as the `compute_minmax` that
930 // follows, where per node it would be the quadratic shape #1122
931 // removed. The message repeats the call, but an assertion's
932 // message arguments are evaluated only on the failing branch.
933 debug_assert!(
934 self.ploc() <= span as u64,
935 "ploc {} exceeds the {span} row span it was clamped to",
936 self.ploc()
937 );
938 debug_assert!(
939 self.cloc() <= span as u64,
940 "cloc {} exceeds the {span} row span it was clamped to",
941 self.cloc()
942 );
943 }
944
945 /// Removes from the `exclude_tests`-pruned row set every row that
946 /// retained code or comments also occupy, so `sloc()` subtracts only
947 /// the rows the prune genuinely took (#1417).
948 ///
949 /// `Sloc::exclude_span` records a pruned subtree's whole span, and
950 /// its first or last physical row can carry a retained sibling —
951 /// `fn a() {} #[cfg(test)] mod t { … }` is one row that stays. The
952 /// walk cannot know that when it prunes, because the retained rows
953 /// of the enclosing space are still arriving; this pass runs once
954 /// per space at finalization, when they are all in.
955 ///
956 /// All three retained sets are subtracted, but only two of them can
957 /// move the answer, and that is worth stating rather than hedging:
958 /// `code_comment_line_starts` is a subset of `ploc.lines` by
959 /// construction. Its only writers are the two `add_cloc_lines` arms,
960 /// both gated on `ploc.lines.contains(start)` already being true,
961 /// and `check_comment_ends_on_code_line`, whose per-language call
962 /// sites every one insert that same row into `ploc.lines` on the
963 /// next statement. `Stats::merge` unions both sets and
964 /// [`Stats::clamp_line_sets_to_span`] retains both over one range,
965 /// so no later pass can separate them. Deleting the third
966 /// subtraction therefore fails no test and *cannot* — measured at 0
967 /// of 3,390 lib tests, where dropping either of the other two fails
968 /// 3 and 1. It stays as defence against a future call site that
969 /// forgets the PLOC insert, and the `debug_assert_eq!` opening the
970 /// body is what keeps that defence honest: without it the line is
971 /// unreachable weight whose loss nothing would ever notice.
972 ///
973 /// Ordered after [`Stats::clamp_line_sets_to_span`], which
974 /// establishes the premise: with `P`, `O`, `C` and the pruned set
975 /// `X` all inside the span `S`, the residual `X' = X \ (P ∪ O ∪ C)`
976 /// is disjoint from `P ∪ O ∪ C`, so `|X'| + |P ∪ O ∪ C| <= |S|` and
977 /// `sloc() = |S| - |X'| >= max(ploc(), cloc())`. That is what the
978 /// assertions below pin — on every space of every walk, so they are
979 /// the real evidence the fix holds across all twenty languages
980 /// rather than only on the fixtures the regression tests name.
981 ///
982 /// [`Stats::blank`] keeps its `saturating_sub`: `P` and `O ∪ C` may
983 /// overlap on a code-and-comment row, so no lower bound on
984 /// `sloc - ploc - |O|` follows from the above.
985 pub(crate) fn settle_excluded_rows(&mut self) {
986 // The premise that makes the third subtraction below redundant,
987 // pinned on every space of every walk because it is a property
988 // of all the `check_comment_ends_on_code_line` call sites at
989 // once and no single fixture can reach them.
990 debug_assert_eq!(
991 self.ploc
992 .lines
993 .union_len(&self.cloc.code_comment_line_starts),
994 self.ploc.lines.len(),
995 "a row carrying both code and a comment must also be a code row"
996 );
997
998 // Disjoint field paths, so the shorthand borrows cleanly.
999 let excluded = &mut self.sloc.excluded_rows;
1000 excluded.subtract(&self.ploc.lines);
1001 excluded.subtract(&self.cloc.only_comment_line_starts);
1002 excluded.subtract(&self.cloc.code_comment_line_starts);
1003
1004 debug_assert!(
1005 self.ploc() <= self.sloc(),
1006 "ploc {} exceeds sloc {} after settling",
1007 self.ploc(),
1008 self.sloc()
1009 );
1010 debug_assert!(
1011 self.cloc() <= self.sloc(),
1012 "cloc {} exceeds sloc {} after settling",
1013 self.cloc(),
1014 self.sloc()
1015 );
1016 }
1017}
1018
1019#[doc(hidden)]
1020/// Per-language computation of the lines-of-code metrics.
1021pub(crate) trait Loc
1022where
1023 Self: Checker,
1024{
1025 /// Walk `node` and update `stats` with this metric for the language
1026 /// implementing the trait.
1027 /// `ancestors` is the chain the walker descended through: the
1028 /// C-family and JVM-family arms read it to tell a declaration in a
1029 /// loop header from one in the loop body (#1084).
1030 fn compute(node: &Node, ancestors: Ancestors<'_, '_>, stats: &mut Stats, is_func_space: bool);
1031}
1032
1033mod shared;
1034pub(crate) use shared::*;
1035
1036// Real defaults — Loc counts on these "languages" would conflate
1037// comments / preproc directives with executable code; treating them
1038// as 0 is the documented behaviour. Audited in #188.
1039implement_metric_trait!(Loc, PreprocCode, CcommentCode);
1040
1041mod bash;
1042mod c;
1043mod cpp;
1044mod csharp;
1045mod elixir;
1046mod go;
1047mod groovy;
1048mod irules;
1049mod java;
1050mod javascript;
1051mod kotlin;
1052mod lua;
1053mod mozcpp;
1054mod mozjs;
1055mod objc;
1056mod perl;
1057mod php;
1058mod python;
1059mod ruby;
1060mod rust;
1061mod tcl;
1062mod tsx;
1063mod typescript;
1064
1065#[cfg(test)]
1066#[allow(
1067 clippy::float_cmp,
1068 clippy::cast_precision_loss,
1069 clippy::cast_possible_truncation,
1070 clippy::cast_sign_loss,
1071 clippy::similar_names,
1072 clippy::doc_markdown,
1073 clippy::needless_raw_string_hashes,
1074 clippy::too_many_lines
1075)]
1076mod tests {
1077 use crate::test_support::{check_metrics_only_shim, metrics_verbatim, space_verbatim};
1078
1079 use super::*;
1080
1081 check_metrics_only_shim!(check_metrics, Loc);
1082
1083 /// A `Stats::default()` that never sees an observation must not leak
1084 /// the `usize::MAX` sentinel for any of the LOC `_min` accumulators
1085 /// (`sloc_min`, `ploc_min`, `lloc_min`, `cloc_min`, `blank_min`).
1086 /// The getters collapse the sentinel to `0.0` so JSON never emits
1087 /// `1.8446744e19`.
1088 #[test]
1089 fn loc_empty_file_min_is_zero() {
1090 let stats = Stats::default();
1091 assert_eq!(stats.sloc_min(), 0);
1092 assert_eq!(stats.ploc_min(), 0);
1093 assert_eq!(stats.lloc_min(), 0);
1094 assert_eq!(stats.cloc_min(), 0);
1095 assert_eq!(stats.blank_min(), 0);
1096 }
1097
1098 /// Parses `source` with `PerlParser` and asserts the resulting tree has
1099 /// no `ERROR` nodes. Use alongside metric assertions whose expected
1100 /// values would happen to match what an error tree produces — a parse
1101 /// regression in tree-sitter-perl could otherwise leave such tests
1102 /// silently green.
1103 #[cfg(feature = "perl")]
1104 #[cfg(test)]
1105 fn assert_perl_parses_cleanly(source: &str) {
1106 use crate::traits::ParserTrait;
1107 // Mirror the trailing-newline normalisation `check_func_space` does
1108 // before handing input to the parser, so this helper sees the same
1109 // bytes the metric tests do.
1110 let path = std::path::PathBuf::from("foo.pl");
1111 let mut bytes = source.trim_end_matches('\n').as_bytes().to_vec();
1112 bytes.push(b'\n');
1113 let parser = PerlParser::new(bytes, &path, None);
1114 assert!(
1115 !parser.root().has_error(),
1116 "tree-sitter-perl returned an error tree for snippet:\n{source}"
1117 );
1118 }
1119
1120 #[cfg(feature = "python")]
1121 #[test]
1122 fn python_sloc() {
1123 check_metrics::<PythonParser>(
1124 "
1125
1126 a = 42
1127
1128 ",
1129 "foo.py",
1130 |metric| {
1131 // Spaces: 1
1132 insta::assert_json_snapshot!(
1133 metric.loc,
1134 @r#"
1135 {
1136 "sloc": 1,
1137 "ploc": 1,
1138 "lloc": 1,
1139 "cloc": 0,
1140 "blank": 0,
1141 "sloc_average": 1.0,
1142 "ploc_average": 1.0,
1143 "lloc_average": 1.0,
1144 "cloc_average": 0.0,
1145 "blank_average": 0.0,
1146 "sloc_min": 1,
1147 "sloc_max": 1,
1148 "cloc_min": 0,
1149 "cloc_max": 0,
1150 "ploc_min": 1,
1151 "ploc_max": 1,
1152 "lloc_min": 1,
1153 "lloc_max": 1,
1154 "blank_min": 0,
1155 "blank_max": 0
1156 }
1157 "#
1158 );
1159 },
1160 );
1161 }
1162
1163 #[cfg(feature = "python")]
1164 #[test]
1165 fn python_blank() {
1166 check_metrics::<PythonParser>(
1167 "
1168 a = 42
1169
1170 b = 43
1171
1172 ",
1173 "foo.py",
1174 |metric| {
1175 // Spaces: 1
1176 insta::assert_json_snapshot!(
1177 metric.loc,
1178 @r#"
1179 {
1180 "sloc": 3,
1181 "ploc": 2,
1182 "lloc": 2,
1183 "cloc": 0,
1184 "blank": 1,
1185 "sloc_average": 3.0,
1186 "ploc_average": 2.0,
1187 "lloc_average": 2.0,
1188 "cloc_average": 0.0,
1189 "blank_average": 1.0,
1190 "sloc_min": 3,
1191 "sloc_max": 3,
1192 "cloc_min": 0,
1193 "cloc_max": 0,
1194 "ploc_min": 2,
1195 "ploc_max": 2,
1196 "lloc_min": 2,
1197 "lloc_max": 2,
1198 "blank_min": 1,
1199 "blank_max": 1
1200 }
1201 "#
1202 );
1203 },
1204 );
1205 }
1206
1207 #[cfg(feature = "rust")]
1208 #[test]
1209 fn rust_blank() {
1210 check_metrics::<RustParser>(
1211 "
1212
1213 let a = 42;
1214
1215 let b = 43;
1216
1217 ",
1218 "foo.rs",
1219 |metric| {
1220 // Spaces: 1
1221 insta::assert_json_snapshot!(
1222 metric.loc,
1223 @r#"
1224 {
1225 "sloc": 3,
1226 "ploc": 2,
1227 "lloc": 2,
1228 "cloc": 0,
1229 "blank": 1,
1230 "sloc_average": 3.0,
1231 "ploc_average": 2.0,
1232 "lloc_average": 2.0,
1233 "cloc_average": 0.0,
1234 "blank_average": 1.0,
1235 "sloc_min": 3,
1236 "sloc_max": 3,
1237 "cloc_min": 0,
1238 "cloc_max": 0,
1239 "ploc_min": 2,
1240 "ploc_max": 2,
1241 "lloc_min": 2,
1242 "lloc_max": 2,
1243 "blank_min": 1,
1244 "blank_max": 1
1245 }
1246 "#
1247 );
1248 },
1249 );
1250
1251 check_metrics::<RustParser>("fn func() { /* comment */ }", "foo.rs", |metric| {
1252 // Spaces: 2
1253 insta::assert_json_snapshot!(
1254 metric.loc,
1255 @r#"
1256 {
1257 "sloc": 1,
1258 "ploc": 1,
1259 "lloc": 0,
1260 "cloc": 1,
1261 "blank": 0,
1262 "sloc_average": 0.5,
1263 "ploc_average": 0.5,
1264 "lloc_average": 0.0,
1265 "cloc_average": 0.5,
1266 "blank_average": 0.0,
1267 "sloc_min": 1,
1268 "sloc_max": 1,
1269 "cloc_min": 1,
1270 "cloc_max": 1,
1271 "ploc_min": 1,
1272 "ploc_max": 1,
1273 "lloc_min": 0,
1274 "lloc_max": 0,
1275 "blank_min": 0,
1276 "blank_max": 0
1277 }
1278 "#
1279 );
1280 });
1281 }
1282
1283 #[cfg(feature = "c")]
1284 #[test]
1285 fn c_blank() {
1286 check_metrics::<CParser>(
1287 "
1288
1289 int a = 42;
1290
1291 int b = 43;
1292
1293 ",
1294 "foo.c",
1295 |metric| {
1296 // Spaces: 1
1297 insta::assert_json_snapshot!(
1298 metric.loc,
1299 @r#"
1300 {
1301 "sloc": 3,
1302 "ploc": 2,
1303 "lloc": 2,
1304 "cloc": 0,
1305 "blank": 1,
1306 "sloc_average": 3.0,
1307 "ploc_average": 2.0,
1308 "lloc_average": 2.0,
1309 "cloc_average": 0.0,
1310 "blank_average": 1.0,
1311 "sloc_min": 3,
1312 "sloc_max": 3,
1313 "cloc_min": 0,
1314 "cloc_max": 0,
1315 "ploc_min": 2,
1316 "ploc_max": 2,
1317 "lloc_min": 2,
1318 "lloc_max": 2,
1319 "blank_min": 1,
1320 "blank_max": 1
1321 }
1322 "#
1323 );
1324 },
1325 );
1326 }
1327
1328 #[cfg(feature = "python")]
1329 #[test]
1330 fn python_no_zero_blank() {
1331 // Checks that the blank metric is not equal to 0 when there are some
1332 // comments next to code lines.
1333 check_metrics::<PythonParser>(
1334 "def ConnectToUpdateServer():
1335 pool = 4
1336
1337 updateServer = -42
1338 isConnected = False
1339 currTry = 0
1340 numRetries = 10 # Number of IPC connection retries before
1341 # giving up.
1342 numTries = 20 # Number of IPC connection tries before
1343 # giving up.",
1344 "foo.py",
1345 |metric| {
1346 // Spaces: 2
1347 insta::assert_json_snapshot!(
1348 metric.loc,
1349 @r#"
1350 {
1351 "sloc": 10,
1352 "ploc": 7,
1353 "lloc": 6,
1354 "cloc": 4,
1355 "blank": 1,
1356 "sloc_average": 5.0,
1357 "ploc_average": 3.5,
1358 "lloc_average": 3.0,
1359 "cloc_average": 2.0,
1360 "blank_average": 0.5,
1361 "sloc_min": 10,
1362 "sloc_max": 10,
1363 "cloc_min": 4,
1364 "cloc_max": 4,
1365 "ploc_min": 7,
1366 "ploc_max": 7,
1367 "lloc_min": 6,
1368 "lloc_max": 6,
1369 "blank_min": 1,
1370 "blank_max": 1
1371 }
1372 "#
1373 );
1374 },
1375 );
1376 }
1377
1378 #[cfg(feature = "python")]
1379 #[test]
1380 fn python_no_blank() {
1381 // Checks that the blank metric is equal to 0 when there are no blank
1382 // lines and there are comments next to code lines.
1383 check_metrics::<PythonParser>(
1384 "def ConnectToUpdateServer():
1385 pool = 4
1386 updateServer = -42
1387 isConnected = False
1388 currTry = 0
1389 numRetries = 10 # Number of IPC connection retries before
1390 # giving up.
1391 numTries = 20 # Number of IPC connection tries before
1392 # giving up.",
1393 "foo.py",
1394 |metric| {
1395 // Spaces: 2
1396 insta::assert_json_snapshot!(
1397 metric.loc,
1398 @r#"
1399 {
1400 "sloc": 9,
1401 "ploc": 7,
1402 "lloc": 6,
1403 "cloc": 4,
1404 "blank": 0,
1405 "sloc_average": 4.5,
1406 "ploc_average": 3.5,
1407 "lloc_average": 3.0,
1408 "cloc_average": 2.0,
1409 "blank_average": 0.0,
1410 "sloc_min": 9,
1411 "sloc_max": 9,
1412 "cloc_min": 4,
1413 "cloc_max": 4,
1414 "ploc_min": 7,
1415 "ploc_max": 7,
1416 "lloc_min": 6,
1417 "lloc_max": 6,
1418 "blank_min": 0,
1419 "blank_max": 0
1420 }
1421 "#
1422 );
1423 },
1424 );
1425 }
1426
1427 #[cfg(feature = "python")]
1428 #[test]
1429 fn python_no_zero_blank_more_comments() {
1430 // Checks that the blank metric is not equal to 0 when there are more
1431 // comments next to code lines compared to the previous tests.
1432 check_metrics::<PythonParser>(
1433 "def ConnectToUpdateServer():
1434 pool = 4
1435
1436 updateServer = -42
1437 isConnected = False
1438 currTry = 0 # Set this variable to 0
1439 numRetries = 10 # Number of IPC connection retries before
1440 # giving up.
1441 numTries = 20 # Number of IPC connection tries before
1442 # giving up.",
1443 "foo.py",
1444 |metric| {
1445 // Spaces: 2
1446 insta::assert_json_snapshot!(
1447 metric.loc,
1448 @r#"
1449 {
1450 "sloc": 10,
1451 "ploc": 7,
1452 "lloc": 6,
1453 "cloc": 5,
1454 "blank": 1,
1455 "sloc_average": 5.0,
1456 "ploc_average": 3.5,
1457 "lloc_average": 3.0,
1458 "cloc_average": 2.5,
1459 "blank_average": 0.5,
1460 "sloc_min": 10,
1461 "sloc_max": 10,
1462 "cloc_min": 5,
1463 "cloc_max": 5,
1464 "ploc_min": 7,
1465 "ploc_max": 7,
1466 "lloc_min": 6,
1467 "lloc_max": 6,
1468 "blank_min": 1,
1469 "blank_max": 1
1470 }
1471 "#
1472 );
1473 },
1474 );
1475 }
1476
1477 #[cfg(feature = "rust")]
1478 #[test]
1479 fn rust_no_zero_blank() {
1480 // Checks that the blank metric is not equal to 0 when there are some
1481 // comments next to code lines.
1482 check_metrics::<RustParser>(
1483 "fn ConnectToUpdateServer() {
1484 let pool = 0;
1485
1486 let updateServer = -42;
1487 let isConnected = false;
1488 let currTry = 0;
1489 let numRetries = 10; // Number of IPC connection retries before
1490 // giving up.
1491 let numTries = 20; // Number of IPC connection tries before
1492 // giving up.
1493 }",
1494 "foo.rs",
1495 |metric| {
1496 // Spaces: 2
1497 insta::assert_json_snapshot!(
1498 metric.loc,
1499 @r#"
1500 {
1501 "sloc": 11,
1502 "ploc": 8,
1503 "lloc": 6,
1504 "cloc": 4,
1505 "blank": 1,
1506 "sloc_average": 5.5,
1507 "ploc_average": 4.0,
1508 "lloc_average": 3.0,
1509 "cloc_average": 2.0,
1510 "blank_average": 0.5,
1511 "sloc_min": 11,
1512 "sloc_max": 11,
1513 "cloc_min": 4,
1514 "cloc_max": 4,
1515 "ploc_min": 8,
1516 "ploc_max": 8,
1517 "lloc_min": 6,
1518 "lloc_max": 6,
1519 "blank_min": 1,
1520 "blank_max": 1
1521 }
1522 "#
1523 );
1524 },
1525 );
1526 }
1527
1528 #[cfg(feature = "javascript")]
1529 #[test]
1530 fn javascript_no_zero_blank() {
1531 // Checks that the blank metric is not equal to 0 when there are some
1532 // comments next to code lines.
1533 check_metrics::<JavascriptParser>(
1534 "function ConnectToUpdateServer() {
1535 var pool = 0;
1536
1537 var updateServer = -42;
1538 var isConnected = false;
1539 var currTry = 0;
1540 var numRetries = 10; // Number of IPC connection retries before
1541 // giving up.
1542 var numTries = 20; // Number of IPC connection tries before
1543 // giving up.
1544 }",
1545 "foo.js",
1546 |metric| {
1547 // Spaces: 2
1548 insta::assert_json_snapshot!(
1549 metric.loc,
1550 @r#"
1551 {
1552 "sloc": 11,
1553 "ploc": 8,
1554 "lloc": 6,
1555 "cloc": 4,
1556 "blank": 1,
1557 "sloc_average": 5.5,
1558 "ploc_average": 4.0,
1559 "lloc_average": 3.0,
1560 "cloc_average": 2.0,
1561 "blank_average": 0.5,
1562 "sloc_min": 11,
1563 "sloc_max": 11,
1564 "cloc_min": 4,
1565 "cloc_max": 4,
1566 "ploc_min": 8,
1567 "ploc_max": 8,
1568 "lloc_min": 6,
1569 "lloc_max": 6,
1570 "blank_min": 1,
1571 "blank_max": 1
1572 }
1573 "#
1574 );
1575 },
1576 );
1577 }
1578
1579 #[cfg(feature = "cpp")]
1580 #[test]
1581 fn cpp_no_zero_blank() {
1582 // Checks that the blank metric is not equal to 0 when there are some
1583 // comments next to code lines.
1584 check_metrics::<CppParser>(
1585 "void ConnectToUpdateServer() {
1586 int pool;
1587
1588 int updateServer = -42;
1589 bool isConnected = false;
1590 int currTry = 0;
1591 const int numRetries = 10; // Number of IPC connection retries before
1592 // giving up.
1593 const int numTries = 20; // Number of IPC connection tries before
1594 // giving up.
1595 }",
1596 "foo.cpp",
1597 |metric| {
1598 // Spaces: 2
1599 insta::assert_json_snapshot!(
1600 metric.loc,
1601 @r#"
1602 {
1603 "sloc": 11,
1604 "ploc": 8,
1605 "lloc": 6,
1606 "cloc": 4,
1607 "blank": 1,
1608 "sloc_average": 5.5,
1609 "ploc_average": 4.0,
1610 "lloc_average": 3.0,
1611 "cloc_average": 2.0,
1612 "blank_average": 0.5,
1613 "sloc_min": 11,
1614 "sloc_max": 11,
1615 "cloc_min": 4,
1616 "cloc_max": 4,
1617 "ploc_min": 8,
1618 "ploc_max": 8,
1619 "lloc_min": 6,
1620 "lloc_max": 6,
1621 "blank_min": 1,
1622 "blank_max": 1
1623 }
1624 "#
1625 );
1626 },
1627 );
1628 }
1629
1630 #[cfg(feature = "cpp")]
1631 #[test]
1632 fn cpp_code_line_start_block_blank() {
1633 // Checks that the blank metric is equal to 1 when there are
1634 // block comments starting next to code lines.
1635 check_metrics::<CppParser>(
1636 "void ConnectToUpdateServer() {
1637 int pool;
1638
1639 int updateServer = -42;
1640 bool isConnected = false;
1641 int currTry = 0;
1642 const int numRetries = 10; /* Number of IPC connection retries
1643 before
1644 giving up. */
1645 const int numTries = 20; // Number of IPC connection tries before
1646 // giving up.
1647 }",
1648 "foo.cpp",
1649 |metric| {
1650 // Spaces: 2
1651 insta::assert_json_snapshot!(
1652 metric.loc,
1653 @r#"
1654 {
1655 "sloc": 12,
1656 "ploc": 8,
1657 "lloc": 6,
1658 "cloc": 5,
1659 "blank": 1,
1660 "sloc_average": 6.0,
1661 "ploc_average": 4.0,
1662 "lloc_average": 3.0,
1663 "cloc_average": 2.5,
1664 "blank_average": 0.5,
1665 "sloc_min": 12,
1666 "sloc_max": 12,
1667 "cloc_min": 5,
1668 "cloc_max": 5,
1669 "ploc_min": 8,
1670 "ploc_max": 8,
1671 "lloc_min": 6,
1672 "lloc_max": 6,
1673 "blank_min": 1,
1674 "blank_max": 1
1675 }
1676 "#
1677 );
1678 },
1679 );
1680 }
1681
1682 #[cfg(feature = "cpp")]
1683 #[test]
1684 fn cpp_block_comment_blank() {
1685 // Checks that the blank metric is equal to 1 when there are
1686 // block comments on independent lines.
1687 check_metrics::<CppParser>(
1688 "void ConnectToUpdateServer() {
1689 int pool;
1690
1691 int updateServer = -42;
1692 bool isConnected = false;
1693 int currTry = 0;
1694 /* Number of IPC connection retries
1695 before
1696 giving up. */
1697 const int numRetries = 10;
1698 const int numTries = 20; // Number of IPC connection tries before
1699 // giving up.
1700 }",
1701 "foo.cpp",
1702 |metric| {
1703 // Spaces: 2
1704 insta::assert_json_snapshot!(
1705 metric.loc,
1706 @r#"
1707 {
1708 "sloc": 13,
1709 "ploc": 8,
1710 "lloc": 6,
1711 "cloc": 5,
1712 "blank": 1,
1713 "sloc_average": 6.5,
1714 "ploc_average": 4.0,
1715 "lloc_average": 3.0,
1716 "cloc_average": 2.5,
1717 "blank_average": 0.5,
1718 "sloc_min": 13,
1719 "sloc_max": 13,
1720 "cloc_min": 5,
1721 "cloc_max": 5,
1722 "ploc_min": 8,
1723 "ploc_max": 8,
1724 "lloc_min": 6,
1725 "lloc_max": 6,
1726 "blank_min": 1,
1727 "blank_max": 1
1728 }
1729 "#
1730 );
1731 },
1732 );
1733 }
1734
1735 #[cfg(feature = "cpp")]
1736 #[test]
1737 fn cpp_code_line_block_one_line_blank() {
1738 // Checks that the blank metric is equal to 1 when there are
1739 // block comments before the same code line.
1740 check_metrics::<CppParser>(
1741 "void ConnectToUpdateServer() {
1742 int pool;
1743
1744 int updateServer = -42;
1745 bool isConnected = false;
1746 int currTry = 0;
1747 /* Number of IPC connection retries before giving up. */ const int numRetries = 10;
1748 const int numTries = 20; // Number of IPC connection tries before
1749 // giving up.
1750 }",
1751 "foo.cpp",
1752 |metric| {
1753 // Spaces: 2
1754 insta::assert_json_snapshot!(
1755 metric.loc,
1756 @r#"
1757 {
1758 "sloc": 10,
1759 "ploc": 8,
1760 "lloc": 6,
1761 "cloc": 3,
1762 "blank": 1,
1763 "sloc_average": 5.0,
1764 "ploc_average": 4.0,
1765 "lloc_average": 3.0,
1766 "cloc_average": 1.5,
1767 "blank_average": 0.5,
1768 "sloc_min": 10,
1769 "sloc_max": 10,
1770 "cloc_min": 3,
1771 "cloc_max": 3,
1772 "ploc_min": 8,
1773 "ploc_max": 8,
1774 "lloc_min": 6,
1775 "lloc_max": 6,
1776 "blank_min": 1,
1777 "blank_max": 1
1778 }
1779 "#
1780 );
1781 },
1782 );
1783 }
1784
1785 #[cfg(feature = "cpp")]
1786 #[test]
1787 fn cpp_code_line_end_block_blank() {
1788 // Checks that the blank metric is equal to 1 when there are
1789 // block comments ending next to code lines.
1790 check_metrics::<CppParser>(
1791 "void ConnectToUpdateServer() {
1792 int pool;
1793
1794 int updateServer = -42;
1795 bool isConnected = false;
1796 int currTry = 0;
1797 /* Number of IPC connection retries
1798 before
1799 giving up. */ const int numRetries = 10;
1800 const int numTries = 20; // Number of IPC connection tries before
1801 // giving up.
1802 }",
1803 "foo.cpp",
1804 |metric| {
1805 // Spaces: 2
1806 insta::assert_json_snapshot!(
1807 metric.loc,
1808 @r#"
1809 {
1810 "sloc": 12,
1811 "ploc": 8,
1812 "lloc": 6,
1813 "cloc": 5,
1814 "blank": 1,
1815 "sloc_average": 6.0,
1816 "ploc_average": 4.0,
1817 "lloc_average": 3.0,
1818 "cloc_average": 2.5,
1819 "blank_average": 0.5,
1820 "sloc_min": 12,
1821 "sloc_max": 12,
1822 "cloc_min": 5,
1823 "cloc_max": 5,
1824 "ploc_min": 8,
1825 "ploc_max": 8,
1826 "lloc_min": 6,
1827 "lloc_max": 6,
1828 "blank_min": 1,
1829 "blank_max": 1
1830 }
1831 "#
1832 );
1833 },
1834 );
1835 }
1836
1837 #[cfg(feature = "python")]
1838 #[test]
1839 fn python_cloc() {
1840 check_metrics::<PythonParser>(
1841 "\"\"\"Block comment
1842 Block comment
1843 \"\"\"
1844 # Line Comment
1845 a = 42 # Line Comment",
1846 "foo.py",
1847 |metric| {
1848 // Spaces: 1
1849 insta::assert_json_snapshot!(
1850 metric.loc,
1851 @r#"
1852 {
1853 "sloc": 5,
1854 "ploc": 1,
1855 "lloc": 2,
1856 "cloc": 5,
1857 "blank": 0,
1858 "sloc_average": 5.0,
1859 "ploc_average": 1.0,
1860 "lloc_average": 2.0,
1861 "cloc_average": 5.0,
1862 "blank_average": 0.0,
1863 "sloc_min": 5,
1864 "sloc_max": 5,
1865 "cloc_min": 5,
1866 "cloc_max": 5,
1867 "ploc_min": 1,
1868 "ploc_max": 1,
1869 "lloc_min": 2,
1870 "lloc_max": 2,
1871 "blank_min": 0,
1872 "blank_max": 0
1873 }
1874 "#
1875 );
1876 },
1877 );
1878 }
1879
1880 #[cfg(feature = "rust")]
1881 #[test]
1882 fn rust_cloc() {
1883 check_metrics::<RustParser>(
1884 "/*Block comment
1885 Block Comment*/
1886 //Line Comment
1887 /*Block Comment*/ let a = 42; // Line Comment",
1888 "foo.rs",
1889 |metric| {
1890 // Spaces: 1
1891 // expected: cloc = 4 — the 2-line block (lines 1-2) and
1892 // the standalone `//Line Comment` (line 3) give 3
1893 // only-comment lines; line 4 carries a leading block
1894 // comment AND a trailing line comment but is one
1895 // physical code line, so it adds a single code-comment
1896 // line, not two (issue #461). Pre-fix this reported 5,
1897 // violating cloc <= sloc (sloc = 4).
1898 insta::assert_json_snapshot!(
1899 metric.loc,
1900 @r#"
1901 {
1902 "sloc": 4,
1903 "ploc": 1,
1904 "lloc": 1,
1905 "cloc": 4,
1906 "blank": 0,
1907 "sloc_average": 4.0,
1908 "ploc_average": 1.0,
1909 "lloc_average": 1.0,
1910 "cloc_average": 4.0,
1911 "blank_average": 0.0,
1912 "sloc_min": 4,
1913 "sloc_max": 4,
1914 "cloc_min": 4,
1915 "cloc_max": 4,
1916 "ploc_min": 1,
1917 "ploc_max": 1,
1918 "lloc_min": 1,
1919 "lloc_max": 1,
1920 "blank_min": 0,
1921 "blank_max": 0
1922 }
1923 "#
1924 );
1925 },
1926 );
1927 }
1928
1929 #[cfg(feature = "c")]
1930 #[test]
1931 fn c_cloc() {
1932 check_metrics::<CParser>(
1933 "/*Block comment
1934 Block Comment*/
1935 //Line Comment
1936 /*Block Comment*/ int a = 42; // Line Comment",
1937 "foo.c",
1938 |metric| {
1939 // Spaces: 1
1940 // expected: cloc = 4 — see `rust_cloc`; line 4's leading
1941 // block comment and trailing line comment share one
1942 // physical code line and add a single code-comment line
1943 // (issue #461). Pre-fix reported 5 (cloc > sloc = 4).
1944 insta::assert_json_snapshot!(
1945 metric.loc,
1946 @r#"
1947 {
1948 "sloc": 4,
1949 "ploc": 1,
1950 "lloc": 1,
1951 "cloc": 4,
1952 "blank": 0,
1953 "sloc_average": 4.0,
1954 "ploc_average": 1.0,
1955 "lloc_average": 1.0,
1956 "cloc_average": 4.0,
1957 "blank_average": 0.0,
1958 "sloc_min": 4,
1959 "sloc_max": 4,
1960 "cloc_min": 4,
1961 "cloc_max": 4,
1962 "ploc_min": 1,
1963 "ploc_max": 1,
1964 "lloc_min": 1,
1965 "lloc_max": 1,
1966 "blank_min": 0,
1967 "blank_max": 0
1968 }
1969 "#
1970 );
1971 },
1972 );
1973 }
1974
1975 #[cfg(feature = "python")]
1976 #[test]
1977 fn python_lloc() {
1978 check_metrics::<PythonParser>(
1979 "for x in range(0,42):
1980 if x % 2 == 0:
1981 print(x)",
1982 "foo.py",
1983 |metric| {
1984 // Spaces: 1
1985 insta::assert_json_snapshot!(
1986 metric.loc,
1987 @r#"
1988 {
1989 "sloc": 3,
1990 "ploc": 3,
1991 "lloc": 3,
1992 "cloc": 0,
1993 "blank": 0,
1994 "sloc_average": 3.0,
1995 "ploc_average": 3.0,
1996 "lloc_average": 3.0,
1997 "cloc_average": 0.0,
1998 "blank_average": 0.0,
1999 "sloc_min": 3,
2000 "sloc_max": 3,
2001 "cloc_min": 0,
2002 "cloc_max": 0,
2003 "ploc_min": 3,
2004 "ploc_max": 3,
2005 "lloc_min": 3,
2006 "lloc_max": 3,
2007 "blank_min": 0,
2008 "blank_max": 0
2009 }
2010 "#
2011 );
2012 },
2013 );
2014 }
2015
2016 #[cfg(feature = "rust")]
2017 #[test]
2018 fn rust_lloc() {
2019 check_metrics::<RustParser>(
2020 "for x in 0..42 {
2021 if x % 2 == 0 {
2022 println!(\"{}\", x);
2023 }
2024 }",
2025 "foo.rs",
2026 |metric| {
2027 // Spaces: 1
2028 insta::assert_json_snapshot!(
2029 metric.loc,
2030 @r#"
2031 {
2032 "sloc": 5,
2033 "ploc": 5,
2034 "lloc": 3,
2035 "cloc": 0,
2036 "blank": 0,
2037 "sloc_average": 5.0,
2038 "ploc_average": 5.0,
2039 "lloc_average": 3.0,
2040 "cloc_average": 0.0,
2041 "blank_average": 0.0,
2042 "sloc_min": 5,
2043 "sloc_max": 5,
2044 "cloc_min": 0,
2045 "cloc_max": 0,
2046 "ploc_min": 5,
2047 "ploc_max": 5,
2048 "lloc_min": 3,
2049 "lloc_max": 3,
2050 "blank_min": 0,
2051 "blank_max": 0
2052 }
2053 "#
2054 );
2055 },
2056 );
2057
2058 // LLOC returns three because there is an empty Rust statement
2059 check_metrics::<RustParser>(
2060 "let a = 42;
2061 if true {
2062 42
2063 } else {
2064 43
2065 };",
2066 "foo.rs",
2067 |metric| {
2068 // Spaces: 1
2069 insta::assert_json_snapshot!(
2070 metric.loc,
2071 @r#"
2072 {
2073 "sloc": 6,
2074 "ploc": 6,
2075 "lloc": 3,
2076 "cloc": 0,
2077 "blank": 0,
2078 "sloc_average": 6.0,
2079 "ploc_average": 6.0,
2080 "lloc_average": 3.0,
2081 "cloc_average": 0.0,
2082 "blank_average": 0.0,
2083 "sloc_min": 6,
2084 "sloc_max": 6,
2085 "cloc_min": 0,
2086 "cloc_max": 0,
2087 "ploc_min": 6,
2088 "ploc_max": 6,
2089 "lloc_min": 3,
2090 "lloc_max": 3,
2091 "blank_min": 0,
2092 "blank_max": 0
2093 }
2094 "#
2095 );
2096 },
2097 );
2098 }
2099
2100 #[cfg(feature = "c")]
2101 #[test]
2102 fn c_lloc() {
2103 check_metrics::<CParser>(
2104 "for (;;)
2105 break;",
2106 "foo.c",
2107 |metric| {
2108 // Spaces: 1
2109 insta::assert_json_snapshot!(
2110 metric.loc,
2111 @r#"
2112 {
2113 "sloc": 2,
2114 "ploc": 2,
2115 "lloc": 2,
2116 "cloc": 0,
2117 "blank": 0,
2118 "sloc_average": 2.0,
2119 "ploc_average": 2.0,
2120 "lloc_average": 2.0,
2121 "cloc_average": 0.0,
2122 "blank_average": 0.0,
2123 "sloc_min": 2,
2124 "sloc_max": 2,
2125 "cloc_min": 0,
2126 "cloc_max": 0,
2127 "ploc_min": 2,
2128 "ploc_max": 2,
2129 "lloc_min": 2,
2130 "lloc_max": 2,
2131 "blank_min": 0,
2132 "blank_max": 0
2133 }
2134 "#
2135 );
2136 },
2137 );
2138 }
2139
2140 #[cfg(feature = "cpp")]
2141 #[test]
2142 fn cpp_lloc() {
2143 check_metrics::<CppParser>(
2144 "nsTArray<xpcGCCallback> callbacks(extraGCCallbacks.Clone());
2145 for (uint32_t i = 0; i < callbacks.Length(); ++i) {
2146 callbacks[i](status);
2147 }",
2148 "foo.cpp",
2149 |metric| {
2150 // Spaces: 1
2151 // lloc: nsTArray, for, callbacks
2152 insta::assert_json_snapshot!(
2153 metric.loc,
2154 @r#"
2155 {
2156 "sloc": 4,
2157 "ploc": 4,
2158 "lloc": 3,
2159 "cloc": 0,
2160 "blank": 0,
2161 "sloc_average": 4.0,
2162 "ploc_average": 4.0,
2163 "lloc_average": 3.0,
2164 "cloc_average": 0.0,
2165 "blank_average": 0.0,
2166 "sloc_min": 4,
2167 "sloc_max": 4,
2168 "cloc_min": 0,
2169 "cloc_max": 0,
2170 "ploc_min": 4,
2171 "ploc_max": 4,
2172 "lloc_min": 3,
2173 "lloc_max": 3,
2174 "blank_min": 0,
2175 "blank_max": 0
2176 }
2177 "#
2178 );
2179 },
2180 );
2181 }
2182
2183 #[cfg(feature = "cpp")]
2184 #[test]
2185 fn cpp_return_lloc() {
2186 check_metrics::<CppParser>(
2187 "uint8_t* pixel_data = frame.GetFrameDataAtPos(DesktopVector(x, y));
2188 return RgbaColor(pixel_data) == blank_pixel_;",
2189 "foo.cpp",
2190 |metric| {
2191 // Spaces: 1
2192 // lloc: pixel_data, return
2193 insta::assert_json_snapshot!(
2194 metric.loc,
2195 @r#"
2196 {
2197 "sloc": 2,
2198 "ploc": 2,
2199 "lloc": 2,
2200 "cloc": 0,
2201 "blank": 0,
2202 "sloc_average": 2.0,
2203 "ploc_average": 2.0,
2204 "lloc_average": 2.0,
2205 "cloc_average": 0.0,
2206 "blank_average": 0.0,
2207 "sloc_min": 2,
2208 "sloc_max": 2,
2209 "cloc_min": 0,
2210 "cloc_max": 0,
2211 "ploc_min": 2,
2212 "ploc_max": 2,
2213 "lloc_min": 2,
2214 "lloc_max": 2,
2215 "blank_min": 0,
2216 "blank_max": 0
2217 }
2218 "#
2219 );
2220 },
2221 );
2222 }
2223
2224 #[cfg(feature = "cpp")]
2225 #[test]
2226 fn cpp_for_lloc() {
2227 check_metrics::<CppParser>(
2228 "for (; start != end; ++start) {
2229 const unsigned char idx = *start;
2230 if (idx > 127 || !kValidTokenMap[idx]) return false;
2231 }",
2232 "foo.cpp",
2233 |metric| {
2234 // Spaces: 1
2235 // lloc: for, idx, if, return
2236 insta::assert_json_snapshot!(
2237 metric.loc,
2238 @r#"
2239 {
2240 "sloc": 4,
2241 "ploc": 4,
2242 "lloc": 4,
2243 "cloc": 0,
2244 "blank": 0,
2245 "sloc_average": 4.0,
2246 "ploc_average": 4.0,
2247 "lloc_average": 4.0,
2248 "cloc_average": 0.0,
2249 "blank_average": 0.0,
2250 "sloc_min": 4,
2251 "sloc_max": 4,
2252 "cloc_min": 0,
2253 "cloc_max": 0,
2254 "ploc_min": 4,
2255 "ploc_max": 4,
2256 "lloc_min": 4,
2257 "lloc_max": 4,
2258 "blank_min": 0,
2259 "blank_max": 0
2260 }
2261 "#
2262 );
2263 },
2264 );
2265 }
2266
2267 #[cfg(feature = "cpp")]
2268 #[test]
2269 fn cpp_while_lloc() {
2270 check_metrics::<CppParser>(
2271 "while (sHeapAtoms) {
2272 HttpHeapAtom* next = sHeapAtoms->next;
2273 free(sHeapAtoms);
2274 }",
2275 "foo.cpp",
2276 |metric| {
2277 // Spaces: 1
2278 // lloc: while, next, free
2279 insta::assert_json_snapshot!(
2280 metric.loc,
2281 @r#"
2282 {
2283 "sloc": 4,
2284 "ploc": 4,
2285 "lloc": 3,
2286 "cloc": 0,
2287 "blank": 0,
2288 "sloc_average": 4.0,
2289 "ploc_average": 4.0,
2290 "lloc_average": 3.0,
2291 "cloc_average": 0.0,
2292 "blank_average": 0.0,
2293 "sloc_min": 4,
2294 "sloc_max": 4,
2295 "cloc_min": 0,
2296 "cloc_max": 0,
2297 "ploc_min": 4,
2298 "ploc_max": 4,
2299 "lloc_min": 3,
2300 "lloc_max": 3,
2301 "blank_min": 0,
2302 "blank_max": 0
2303 }
2304 "#
2305 );
2306 },
2307 );
2308 }
2309
2310 #[cfg(feature = "python")]
2311 #[test]
2312 fn python_string_on_new_line() {
2313 // More lines of the same instruction were counted as blank lines
2314 check_metrics::<PythonParser>(
2315 "capabilities[\"goog:chromeOptions\"][\"androidPackage\"] = \\
2316 \"org.chromium.weblayer.shell\"",
2317 "foo.py",
2318 |metric| {
2319 // Spaces: 1
2320 insta::assert_json_snapshot!(
2321 metric.loc,
2322 @r#"
2323 {
2324 "sloc": 2,
2325 "ploc": 2,
2326 "lloc": 1,
2327 "cloc": 0,
2328 "blank": 0,
2329 "sloc_average": 2.0,
2330 "ploc_average": 2.0,
2331 "lloc_average": 1.0,
2332 "cloc_average": 0.0,
2333 "blank_average": 0.0,
2334 "sloc_min": 2,
2335 "sloc_max": 2,
2336 "cloc_min": 0,
2337 "cloc_max": 0,
2338 "ploc_min": 2,
2339 "ploc_max": 2,
2340 "lloc_min": 1,
2341 "lloc_max": 1,
2342 "blank_min": 0,
2343 "blank_max": 0
2344 }
2345 "#
2346 );
2347 },
2348 );
2349 }
2350
2351 #[cfg(feature = "python")]
2352 #[test]
2353 fn python_multiline_string_assignment_ploc() {
2354 // Regression test for issue #415: interior rows of a multi-line,
2355 // non-docstring string literal were mis-counted as blank lines.
2356 check_metrics::<PythonParser>(
2357 "QUERY = \"\"\"
2358SELECT id, name
2359FROM users
2360WHERE active = 1
2361ORDER BY name
2362\"\"\"",
2363 "foo.py",
2364 |metric| {
2365 // Spaces: 1. Six physical lines, all code, no blanks.
2366 assert_eq!(metric.loc.sloc(), 6);
2367 assert_eq!(metric.loc.ploc(), 6);
2368 assert_eq!(metric.loc.cloc(), 0);
2369 assert_eq!(metric.loc.blank(), 0);
2370 insta::assert_json_snapshot!(
2371 metric.loc,
2372 @r#"
2373 {
2374 "sloc": 6,
2375 "ploc": 6,
2376 "lloc": 1,
2377 "cloc": 0,
2378 "blank": 0,
2379 "sloc_average": 6.0,
2380 "ploc_average": 6.0,
2381 "lloc_average": 1.0,
2382 "cloc_average": 0.0,
2383 "blank_average": 0.0,
2384 "sloc_min": 6,
2385 "sloc_max": 6,
2386 "cloc_min": 0,
2387 "cloc_max": 0,
2388 "ploc_min": 6,
2389 "ploc_max": 6,
2390 "lloc_min": 1,
2391 "lloc_max": 1,
2392 "blank_min": 0,
2393 "blank_max": 0
2394 }
2395 "#
2396 );
2397 },
2398 );
2399 }
2400
2401 #[cfg(feature = "python")]
2402 #[test]
2403 fn python_multiline_string_argument_ploc() {
2404 // Regression test for issue #415: a multi-line string passed as a call
2405 // argument must have all its rows counted as code, not blank.
2406 check_metrics::<PythonParser>(
2407 "print(\"\"\"
2408line one
2409line two
2410\"\"\")",
2411 "foo.py",
2412 |metric| {
2413 // Spaces: 1. Four physical lines, all code, no blanks.
2414 assert_eq!(metric.loc.sloc(), 4);
2415 assert_eq!(metric.loc.ploc(), 4);
2416 assert_eq!(metric.loc.cloc(), 0);
2417 assert_eq!(metric.loc.blank(), 0);
2418 },
2419 );
2420 }
2421
2422 #[cfg(feature = "python")]
2423 #[test]
2424 fn python_single_line_string_assignment_ploc() {
2425 // Single-line, non-docstring string: behaviour must be unchanged by
2426 // the issue #415 fix (start == end means no extra rows are inserted).
2427 check_metrics::<PythonParser>("QUERY = \"SELECT 1\"", "foo.py", |metric| {
2428 // Spaces: 1.
2429 assert_eq!(metric.loc.sloc(), 1);
2430 assert_eq!(metric.loc.ploc(), 1);
2431 assert_eq!(metric.loc.cloc(), 0);
2432 assert_eq!(metric.loc.blank(), 0);
2433 });
2434 }
2435
2436 #[cfg(feature = "python")]
2437 #[test]
2438 fn python_multiline_docstring_still_cloc() {
2439 // The fix for issue #415 must leave docstring classification unchanged:
2440 // a bare triple-quoted string statement is still counted as comments.
2441 check_metrics::<PythonParser>(
2442 "def f():
2443 \"\"\"Docstring line one
2444 Docstring line two
2445 \"\"\"
2446 return 1",
2447 "foo.py",
2448 |metric| {
2449 // Spaces: 2 (module + function). The three docstring rows are
2450 // comment lines, not code or blank lines.
2451 assert_eq!(metric.loc.cloc(), 3);
2452 assert_eq!(metric.loc.blank(), 0);
2453 },
2454 );
2455 }
2456
2457 #[cfg(feature = "rust")]
2458 #[test]
2459 fn rust_no_field_expression_lloc() {
2460 check_metrics::<RustParser>(
2461 "struct Foo {
2462 field: usize,
2463 }
2464 let foo = Foo { 42 };
2465 foo.field;",
2466 "foo.rs",
2467 |metric| {
2468 // Spaces: 1
2469 insta::assert_json_snapshot!(
2470 metric.loc,
2471 @r#"
2472 {
2473 "sloc": 5,
2474 "ploc": 5,
2475 "lloc": 2,
2476 "cloc": 0,
2477 "blank": 0,
2478 "sloc_average": 5.0,
2479 "ploc_average": 5.0,
2480 "lloc_average": 2.0,
2481 "cloc_average": 0.0,
2482 "blank_average": 0.0,
2483 "sloc_min": 5,
2484 "sloc_max": 5,
2485 "cloc_min": 0,
2486 "cloc_max": 0,
2487 "ploc_min": 5,
2488 "ploc_max": 5,
2489 "lloc_min": 2,
2490 "lloc_max": 2,
2491 "blank_min": 0,
2492 "blank_max": 0
2493 }
2494 "#
2495 );
2496 },
2497 );
2498 }
2499
2500 #[cfg(feature = "rust")]
2501 #[test]
2502 fn rust_no_parenthesized_expression_lloc() {
2503 check_metrics::<RustParser>("let a = (42 + 0);", "foo.rs", |metric| {
2504 // Spaces: 1
2505 insta::assert_json_snapshot!(
2506 metric.loc,
2507 @r#"
2508 {
2509 "sloc": 1,
2510 "ploc": 1,
2511 "lloc": 1,
2512 "cloc": 0,
2513 "blank": 0,
2514 "sloc_average": 1.0,
2515 "ploc_average": 1.0,
2516 "lloc_average": 1.0,
2517 "cloc_average": 0.0,
2518 "blank_average": 0.0,
2519 "sloc_min": 1,
2520 "sloc_max": 1,
2521 "cloc_min": 0,
2522 "cloc_max": 0,
2523 "ploc_min": 1,
2524 "ploc_max": 1,
2525 "lloc_min": 1,
2526 "lloc_max": 1,
2527 "blank_min": 0,
2528 "blank_max": 0
2529 }
2530 "#
2531 );
2532 });
2533 }
2534
2535 #[cfg(feature = "rust")]
2536 #[test]
2537 fn rust_no_array_expression_lloc() {
2538 check_metrics::<RustParser>("let a = [0; 42];", "foo.rs", |metric| {
2539 // Spaces: 1
2540 insta::assert_json_snapshot!(
2541 metric.loc,
2542 @r#"
2543 {
2544 "sloc": 1,
2545 "ploc": 1,
2546 "lloc": 1,
2547 "cloc": 0,
2548 "blank": 0,
2549 "sloc_average": 1.0,
2550 "ploc_average": 1.0,
2551 "lloc_average": 1.0,
2552 "cloc_average": 0.0,
2553 "blank_average": 0.0,
2554 "sloc_min": 1,
2555 "sloc_max": 1,
2556 "cloc_min": 0,
2557 "cloc_max": 0,
2558 "ploc_min": 1,
2559 "ploc_max": 1,
2560 "lloc_min": 1,
2561 "lloc_max": 1,
2562 "blank_min": 0,
2563 "blank_max": 0
2564 }
2565 "#
2566 );
2567 });
2568 }
2569
2570 #[cfg(feature = "rust")]
2571 #[test]
2572 fn rust_no_tuple_expression_lloc() {
2573 check_metrics::<RustParser>("let a = (0, 42);", "foo.rs", |metric| {
2574 // Spaces: 1
2575 insta::assert_json_snapshot!(
2576 metric.loc,
2577 @r#"
2578 {
2579 "sloc": 1,
2580 "ploc": 1,
2581 "lloc": 1,
2582 "cloc": 0,
2583 "blank": 0,
2584 "sloc_average": 1.0,
2585 "ploc_average": 1.0,
2586 "lloc_average": 1.0,
2587 "cloc_average": 0.0,
2588 "blank_average": 0.0,
2589 "sloc_min": 1,
2590 "sloc_max": 1,
2591 "cloc_min": 0,
2592 "cloc_max": 0,
2593 "ploc_min": 1,
2594 "ploc_max": 1,
2595 "lloc_min": 1,
2596 "lloc_max": 1,
2597 "blank_min": 0,
2598 "blank_max": 0
2599 }
2600 "#
2601 );
2602 });
2603 }
2604
2605 #[cfg(feature = "rust")]
2606 #[test]
2607 fn rust_no_unit_expression_lloc() {
2608 check_metrics::<RustParser>("let a = ();", "foo.rs", |metric| {
2609 // Spaces: 1
2610 insta::assert_json_snapshot!(
2611 metric.loc,
2612 @r#"
2613 {
2614 "sloc": 1,
2615 "ploc": 1,
2616 "lloc": 1,
2617 "cloc": 0,
2618 "blank": 0,
2619 "sloc_average": 1.0,
2620 "ploc_average": 1.0,
2621 "lloc_average": 1.0,
2622 "cloc_average": 0.0,
2623 "blank_average": 0.0,
2624 "sloc_min": 1,
2625 "sloc_max": 1,
2626 "cloc_min": 0,
2627 "cloc_max": 0,
2628 "ploc_min": 1,
2629 "ploc_max": 1,
2630 "lloc_min": 1,
2631 "lloc_max": 1,
2632 "blank_min": 0,
2633 "blank_max": 0
2634 }
2635 "#
2636 );
2637 });
2638 }
2639
2640 #[cfg(feature = "rust")]
2641 #[test]
2642 fn rust_call_function_lloc() {
2643 check_metrics::<RustParser>(
2644 "let a = foo(); // +1
2645 foo(); // +1
2646 k!(foo()); // +1",
2647 "foo.rs",
2648 |metric| {
2649 // Spaces: 1
2650 insta::assert_json_snapshot!(
2651 metric.loc,
2652 @r#"
2653 {
2654 "sloc": 3,
2655 "ploc": 3,
2656 "lloc": 3,
2657 "cloc": 3,
2658 "blank": 0,
2659 "sloc_average": 3.0,
2660 "ploc_average": 3.0,
2661 "lloc_average": 3.0,
2662 "cloc_average": 3.0,
2663 "blank_average": 0.0,
2664 "sloc_min": 3,
2665 "sloc_max": 3,
2666 "cloc_min": 3,
2667 "cloc_max": 3,
2668 "ploc_min": 3,
2669 "ploc_max": 3,
2670 "lloc_min": 3,
2671 "lloc_max": 3,
2672 "blank_min": 0,
2673 "blank_max": 0
2674 }
2675 "#
2676 );
2677 },
2678 );
2679 }
2680
2681 #[cfg(feature = "rust")]
2682 #[test]
2683 fn rust_macro_invocation_lloc() {
2684 check_metrics::<RustParser>(
2685 "let a = foo!(); // +1
2686 foo!(); // +1
2687 k(foo!()); // +1",
2688 "foo.rs",
2689 |metric| {
2690 // Spaces: 1
2691 insta::assert_json_snapshot!(
2692 metric.loc,
2693 @r#"
2694 {
2695 "sloc": 3,
2696 "ploc": 3,
2697 "lloc": 3,
2698 "cloc": 3,
2699 "blank": 0,
2700 "sloc_average": 3.0,
2701 "ploc_average": 3.0,
2702 "lloc_average": 3.0,
2703 "cloc_average": 3.0,
2704 "blank_average": 0.0,
2705 "sloc_min": 3,
2706 "sloc_max": 3,
2707 "cloc_min": 3,
2708 "cloc_max": 3,
2709 "ploc_min": 3,
2710 "ploc_max": 3,
2711 "lloc_min": 3,
2712 "lloc_max": 3,
2713 "blank_min": 0,
2714 "blank_max": 0
2715 }
2716 "#
2717 );
2718 },
2719 );
2720 }
2721
2722 #[cfg(feature = "rust")]
2723 #[test]
2724 fn rust_function_in_loop_lloc() {
2725 check_metrics::<RustParser>(
2726 "for (a, b) in c.iter().enumerate() {} // +1
2727 while (a, b) in c.iter().enumerate() {} // +1
2728 while let Some(a) = c.strip_prefix(\"hi\") {} // +1",
2729 "foo.rs",
2730 |metric| {
2731 // Spaces: 1
2732 insta::assert_json_snapshot!(
2733 metric.loc,
2734 @r#"
2735 {
2736 "sloc": 3,
2737 "ploc": 3,
2738 "lloc": 3,
2739 "cloc": 3,
2740 "blank": 0,
2741 "sloc_average": 3.0,
2742 "ploc_average": 3.0,
2743 "lloc_average": 3.0,
2744 "cloc_average": 3.0,
2745 "blank_average": 0.0,
2746 "sloc_min": 3,
2747 "sloc_max": 3,
2748 "cloc_min": 3,
2749 "cloc_max": 3,
2750 "ploc_min": 3,
2751 "ploc_max": 3,
2752 "lloc_min": 3,
2753 "lloc_max": 3,
2754 "blank_min": 0,
2755 "blank_max": 0
2756 }
2757 "#
2758 );
2759 },
2760 );
2761 }
2762
2763 #[cfg(feature = "rust")]
2764 #[test]
2765 fn rust_function_in_if_lloc() {
2766 check_metrics::<RustParser>(
2767 "if foo() {} // +1
2768 if let Some(a) = foo() {} // +1",
2769 "foo.rs",
2770 |metric| {
2771 // Spaces: 1
2772 insta::assert_json_snapshot!(
2773 metric.loc,
2774 @r#"
2775 {
2776 "sloc": 2,
2777 "ploc": 2,
2778 "lloc": 2,
2779 "cloc": 2,
2780 "blank": 0,
2781 "sloc_average": 2.0,
2782 "ploc_average": 2.0,
2783 "lloc_average": 2.0,
2784 "cloc_average": 2.0,
2785 "blank_average": 0.0,
2786 "sloc_min": 2,
2787 "sloc_max": 2,
2788 "cloc_min": 2,
2789 "cloc_max": 2,
2790 "ploc_min": 2,
2791 "ploc_max": 2,
2792 "lloc_min": 2,
2793 "lloc_max": 2,
2794 "blank_min": 0,
2795 "blank_max": 0
2796 }
2797 "#
2798 );
2799 },
2800 );
2801 }
2802
2803 #[cfg(feature = "rust")]
2804 #[test]
2805 fn rust_function_in_return_lloc() {
2806 check_metrics::<RustParser>(
2807 "return foo();
2808 await foo();",
2809 "foo.rs",
2810 |metric| {
2811 // Spaces: 1
2812 insta::assert_json_snapshot!(
2813 metric.loc,
2814 @r#"
2815 {
2816 "sloc": 2,
2817 "ploc": 2,
2818 "lloc": 2,
2819 "cloc": 0,
2820 "blank": 0,
2821 "sloc_average": 2.0,
2822 "ploc_average": 2.0,
2823 "lloc_average": 2.0,
2824 "cloc_average": 0.0,
2825 "blank_average": 0.0,
2826 "sloc_min": 2,
2827 "sloc_max": 2,
2828 "cloc_min": 0,
2829 "cloc_max": 0,
2830 "ploc_min": 2,
2831 "ploc_max": 2,
2832 "lloc_min": 2,
2833 "lloc_max": 2,
2834 "blank_min": 0,
2835 "blank_max": 0
2836 }
2837 "#
2838 );
2839 },
2840 );
2841 }
2842
2843 #[cfg(feature = "rust")]
2844 #[test]
2845 fn rust_closure_expression_lloc() {
2846 check_metrics::<RustParser>(
2847 "let a = |i: i32| -> i32 { i + 1 }; // +1
2848 a(42); // +1
2849 k(b.iter().map(|n| n.parse.ok().unwrap_or(42))); // +1",
2850 "foo.rs",
2851 |metric| {
2852 // Spaces: 3
2853 insta::assert_json_snapshot!(
2854 metric.loc,
2855 @r#"
2856 {
2857 "sloc": 3,
2858 "ploc": 3,
2859 "lloc": 3,
2860 "cloc": 3,
2861 "blank": 0,
2862 "sloc_average": 1.0,
2863 "ploc_average": 1.0,
2864 "lloc_average": 1.0,
2865 "cloc_average": 1.0,
2866 "blank_average": 0.0,
2867 "sloc_min": 1,
2868 "sloc_max": 3,
2869 "cloc_min": 0,
2870 "cloc_max": 3,
2871 "ploc_min": 1,
2872 "ploc_max": 3,
2873 "lloc_min": 0,
2874 "lloc_max": 3,
2875 "blank_min": 0,
2876 "blank_max": 0
2877 }
2878 "#
2879 );
2880 },
2881 );
2882 }
2883
2884 #[cfg(feature = "python")]
2885 #[test]
2886 fn python_general_loc() {
2887 check_metrics::<PythonParser>(
2888 "def func(a,
2889 b,
2890 c):
2891 print(a)
2892 print(b)
2893 print(c)",
2894 "foo.py",
2895 |metric| {
2896 // Spaces: 2
2897 insta::assert_json_snapshot!(
2898 metric.loc,
2899 @r#"
2900 {
2901 "sloc": 6,
2902 "ploc": 6,
2903 "lloc": 3,
2904 "cloc": 0,
2905 "blank": 0,
2906 "sloc_average": 3.0,
2907 "ploc_average": 3.0,
2908 "lloc_average": 1.5,
2909 "cloc_average": 0.0,
2910 "blank_average": 0.0,
2911 "sloc_min": 6,
2912 "sloc_max": 6,
2913 "cloc_min": 0,
2914 "cloc_max": 0,
2915 "ploc_min": 6,
2916 "ploc_max": 6,
2917 "lloc_min": 3,
2918 "lloc_max": 3,
2919 "blank_min": 0,
2920 "blank_max": 0
2921 }
2922 "#
2923 );
2924 },
2925 );
2926 }
2927
2928 #[cfg(feature = "python")]
2929 #[test]
2930 fn python_real_loc() {
2931 check_metrics::<PythonParser>(
2932 "def web_socket_transfer_data(request):
2933 while True:
2934 line = request.ws_stream.receive_message()
2935 if line is None:
2936 return
2937 code, reason = line.split(' ', 1)
2938 if code is None or reason is None:
2939 return
2940 request.ws_stream.close_connection(int(code), reason)
2941 # close_connection() initiates closing handshake. It validates code
2942 # and reason. If you want to send a broken close frame for a test,
2943 # following code will be useful.
2944 # > data = struct.pack('!H', int(code)) + reason.encode('UTF-8')
2945 # > request.connection.write(stream.create_close_frame(data))
2946 # > # Suppress to re-respond client responding close frame.
2947 # > raise Exception(\"customized server initiated closing handshake\")",
2948 "foo.py",
2949 |metric| {
2950 // Spaces: 2
2951 insta::assert_json_snapshot!(
2952 metric.loc,
2953 @r#"
2954 {
2955 "sloc": 16,
2956 "ploc": 9,
2957 "lloc": 8,
2958 "cloc": 7,
2959 "blank": 0,
2960 "sloc_average": 8.0,
2961 "ploc_average": 4.5,
2962 "lloc_average": 4.0,
2963 "cloc_average": 3.5,
2964 "blank_average": 0.0,
2965 "sloc_min": 16,
2966 "sloc_max": 16,
2967 "cloc_min": 7,
2968 "cloc_max": 7,
2969 "ploc_min": 9,
2970 "ploc_max": 9,
2971 "lloc_min": 8,
2972 "lloc_max": 8,
2973 "blank_min": 0,
2974 "blank_max": 0
2975 }
2976 "#
2977 );
2978 },
2979 );
2980 }
2981
2982 #[cfg(feature = "javascript")]
2983 #[test]
2984 fn javascript_real_loc() {
2985 check_metrics::<JavascriptParser>(
2986 "assert.throws(Test262Error, function() {
2987 for (let { poisoned: x = ++initEvalCount } = poisonedProperty; ; ) {
2988 return;
2989 }
2990 });",
2991 "foo.js",
2992 |metric| {
2993 // Spaces: 2
2994 insta::assert_json_snapshot!(
2995 metric.loc,
2996 @r#"
2997 {
2998 "sloc": 5,
2999 "ploc": 5,
3000 "lloc": 4,
3001 "cloc": 0,
3002 "blank": 0,
3003 "sloc_average": 2.5,
3004 "ploc_average": 2.5,
3005 "lloc_average": 2.0,
3006 "cloc_average": 0.0,
3007 "blank_average": 0.0,
3008 "sloc_min": 5,
3009 "sloc_max": 5,
3010 "cloc_min": 0,
3011 "cloc_max": 0,
3012 "ploc_min": 5,
3013 "ploc_max": 5,
3014 "lloc_min": 3,
3015 "lloc_max": 4,
3016 "blank_min": 0,
3017 "blank_max": 0
3018 }
3019 "#
3020 );
3021 },
3022 );
3023 }
3024
3025 #[cfg(feature = "mozjs")]
3026 #[test]
3027 fn mozjs_real_loc() {
3028 check_metrics::<MozjsParser>(
3029 "assert.throws(Test262Error, function() {
3030 for (let { poisoned: x = ++initEvalCount } = poisonedProperty; ; ) {
3031 return;
3032 }
3033 });",
3034 "foo.js",
3035 |metric| {
3036 // Spaces: 2
3037 insta::assert_json_snapshot!(
3038 metric.loc,
3039 @r#"
3040 {
3041 "sloc": 5,
3042 "ploc": 5,
3043 "lloc": 4,
3044 "cloc": 0,
3045 "blank": 0,
3046 "sloc_average": 2.5,
3047 "ploc_average": 2.5,
3048 "lloc_average": 2.0,
3049 "cloc_average": 0.0,
3050 "blank_average": 0.0,
3051 "sloc_min": 5,
3052 "sloc_max": 5,
3053 "cloc_min": 0,
3054 "cloc_max": 0,
3055 "ploc_min": 5,
3056 "ploc_max": 5,
3057 "lloc_min": 3,
3058 "lloc_max": 4,
3059 "blank_min": 0,
3060 "blank_max": 0
3061 }
3062 "#
3063 );
3064 },
3065 );
3066 }
3067
3068 #[cfg(feature = "mozjs")]
3069 #[test]
3070 fn mozjs_blank_and_comment_loc() {
3071 check_metrics::<MozjsParser>(
3072 "// a comment
3073 function f() {
3074
3075 var x = 1;
3076
3077 }",
3078 "foo.js",
3079 |metric| {
3080 insta::assert_json_snapshot!(
3081 metric.loc,
3082 @r#"
3083 {
3084 "sloc": 6,
3085 "ploc": 3,
3086 "lloc": 1,
3087 "cloc": 1,
3088 "blank": 2,
3089 "sloc_average": 3.0,
3090 "ploc_average": 1.5,
3091 "lloc_average": 0.5,
3092 "cloc_average": 0.5,
3093 "blank_average": 1.0,
3094 "sloc_min": 5,
3095 "sloc_max": 6,
3096 "cloc_min": 0,
3097 "cloc_max": 1,
3098 "ploc_min": 3,
3099 "ploc_max": 3,
3100 "lloc_min": 1,
3101 "lloc_max": 1,
3102 "blank_min": 2,
3103 "blank_max": 2
3104 }
3105 "#
3106 );
3107 },
3108 );
3109 }
3110
3111 #[cfg(feature = "cpp")]
3112 #[test]
3113 fn cpp_namespace_loc() {
3114 check_metrics::<CppParser>(
3115 "namespace mozilla::dom::quota {} // namespace mozilla::dom::quota",
3116 "foo.cpp",
3117 |metric| {
3118 // Spaces: 2
3119 insta::assert_json_snapshot!(
3120 metric.loc,
3121 @r#"
3122 {
3123 "sloc": 1,
3124 "ploc": 1,
3125 "lloc": 0,
3126 "cloc": 1,
3127 "blank": 0,
3128 "sloc_average": 0.5,
3129 "ploc_average": 0.5,
3130 "lloc_average": 0.0,
3131 "cloc_average": 0.5,
3132 "blank_average": 0.0,
3133 "sloc_min": 1,
3134 "sloc_max": 1,
3135 "cloc_min": 0,
3136 "cloc_max": 1,
3137 "ploc_min": 1,
3138 "ploc_max": 1,
3139 "lloc_min": 0,
3140 "lloc_max": 0,
3141 "blank_min": 0,
3142 "blank_max": 0
3143 }
3144 "#
3145 );
3146 },
3147 );
3148 }
3149
3150 #[cfg(feature = "java")]
3151 #[test]
3152 fn java_comments() {
3153 check_metrics::<JavaParser>(
3154 "for (int i = 0; i < 100; i++) { \
3155 // Print hello
3156 System.out.println(\"hello\"); \
3157 // Print world
3158 System.out.println(\"hello\"); \
3159 }",
3160 "foo.java",
3161 |metric| {
3162 // Spaces: 1
3163 insta::assert_json_snapshot!(
3164 metric.loc,
3165 @r#"
3166 {
3167 "sloc": 3,
3168 "ploc": 3,
3169 "lloc": 3,
3170 "cloc": 2,
3171 "blank": 0,
3172 "sloc_average": 3.0,
3173 "ploc_average": 3.0,
3174 "lloc_average": 3.0,
3175 "cloc_average": 2.0,
3176 "blank_average": 0.0,
3177 "sloc_min": 3,
3178 "sloc_max": 3,
3179 "cloc_min": 2,
3180 "cloc_max": 2,
3181 "ploc_min": 3,
3182 "ploc_max": 3,
3183 "lloc_min": 3,
3184 "lloc_max": 3,
3185 "blank_min": 0,
3186 "blank_max": 0
3187 }
3188 "#
3189 );
3190 },
3191 );
3192 }
3193
3194 #[cfg(feature = "java")]
3195 #[test]
3196 fn java_blank() {
3197 check_metrics::<JavaParser>(
3198 "int x = 1;
3199
3200
3201 int y = 2;",
3202 "foo.java",
3203 |metric| {
3204 // Spaces: 1
3205 insta::assert_json_snapshot!(
3206 metric.loc,
3207 @r#"
3208 {
3209 "sloc": 4,
3210 "ploc": 2,
3211 "lloc": 2,
3212 "cloc": 0,
3213 "blank": 2,
3214 "sloc_average": 4.0,
3215 "ploc_average": 2.0,
3216 "lloc_average": 2.0,
3217 "cloc_average": 0.0,
3218 "blank_average": 2.0,
3219 "sloc_min": 4,
3220 "sloc_max": 4,
3221 "cloc_min": 0,
3222 "cloc_max": 0,
3223 "ploc_min": 2,
3224 "ploc_max": 2,
3225 "lloc_min": 2,
3226 "lloc_max": 2,
3227 "blank_min": 2,
3228 "blank_max": 2
3229 }
3230 "#
3231 );
3232 },
3233 );
3234 }
3235
3236 #[cfg(feature = "java")]
3237 #[test]
3238 fn java_sloc() {
3239 check_metrics::<JavaParser>(
3240 "for (int i = 0; i < 100; i++) {
3241 System.out.println(i);
3242 }",
3243 "foo.java",
3244 |metric| {
3245 // Spaces: 1
3246 insta::assert_json_snapshot!(
3247 metric.loc,
3248 @r#"
3249 {
3250 "sloc": 3,
3251 "ploc": 3,
3252 "lloc": 2,
3253 "cloc": 0,
3254 "blank": 0,
3255 "sloc_average": 3.0,
3256 "ploc_average": 3.0,
3257 "lloc_average": 2.0,
3258 "cloc_average": 0.0,
3259 "blank_average": 0.0,
3260 "sloc_min": 3,
3261 "sloc_max": 3,
3262 "cloc_min": 0,
3263 "cloc_max": 0,
3264 "ploc_min": 3,
3265 "ploc_max": 3,
3266 "lloc_min": 2,
3267 "lloc_max": 2,
3268 "blank_min": 0,
3269 "blank_max": 0
3270 }
3271 "#
3272 );
3273 },
3274 );
3275 }
3276
3277 #[cfg(feature = "java")]
3278 #[test]
3279 fn java_module_sloc() {
3280 check_metrics::<JavaParser>(
3281 "module helloworld{
3282 exports com.test;
3283 }",
3284 "foo.java",
3285 |metric| {
3286 // Spaces: 1
3287 insta::assert_json_snapshot!(
3288 metric.loc,
3289 @r#"
3290 {
3291 "sloc": 3,
3292 "ploc": 3,
3293 "lloc": 0,
3294 "cloc": 0,
3295 "blank": 0,
3296 "sloc_average": 3.0,
3297 "ploc_average": 3.0,
3298 "lloc_average": 0.0,
3299 "cloc_average": 0.0,
3300 "blank_average": 0.0,
3301 "sloc_min": 3,
3302 "sloc_max": 3,
3303 "cloc_min": 0,
3304 "cloc_max": 0,
3305 "ploc_min": 3,
3306 "ploc_max": 3,
3307 "lloc_min": 0,
3308 "lloc_max": 0,
3309 "blank_min": 0,
3310 "blank_max": 0
3311 }
3312 "#
3313 );
3314 },
3315 );
3316 }
3317
3318 #[cfg(feature = "java")]
3319 #[test]
3320 fn java_single_ploc() {
3321 check_metrics::<JavaParser>("int x = 1;", "foo.java", |metric| {
3322 // Spaces: 1
3323 insta::assert_json_snapshot!(
3324 metric.loc,
3325 @r#"
3326 {
3327 "sloc": 1,
3328 "ploc": 1,
3329 "lloc": 1,
3330 "cloc": 0,
3331 "blank": 0,
3332 "sloc_average": 1.0,
3333 "ploc_average": 1.0,
3334 "lloc_average": 1.0,
3335 "cloc_average": 0.0,
3336 "blank_average": 0.0,
3337 "sloc_min": 1,
3338 "sloc_max": 1,
3339 "cloc_min": 0,
3340 "cloc_max": 0,
3341 "ploc_min": 1,
3342 "ploc_max": 1,
3343 "lloc_min": 1,
3344 "lloc_max": 1,
3345 "blank_min": 0,
3346 "blank_max": 0
3347 }
3348 "#
3349 );
3350 });
3351 }
3352
3353 #[cfg(feature = "java")]
3354 #[test]
3355 fn java_simple_ploc() {
3356 check_metrics::<JavaParser>(
3357 "for (int i = 0; i < 100; i = i++) {
3358 System.out.println(i);
3359 }",
3360 "foo.java",
3361 |metric| {
3362 // Spaces: 1
3363 insta::assert_json_snapshot!(
3364 metric.loc,
3365 @r#"
3366 {
3367 "sloc": 3,
3368 "ploc": 3,
3369 "lloc": 2,
3370 "cloc": 0,
3371 "blank": 0,
3372 "sloc_average": 3.0,
3373 "ploc_average": 3.0,
3374 "lloc_average": 2.0,
3375 "cloc_average": 0.0,
3376 "blank_average": 0.0,
3377 "sloc_min": 3,
3378 "sloc_max": 3,
3379 "cloc_min": 0,
3380 "cloc_max": 0,
3381 "ploc_min": 3,
3382 "ploc_max": 3,
3383 "lloc_min": 2,
3384 "lloc_max": 2,
3385 "blank_min": 0,
3386 "blank_max": 0
3387 }
3388 "#
3389 );
3390 },
3391 );
3392 }
3393
3394 #[cfg(feature = "java")]
3395 #[test]
3396 fn java_multi_ploc() {
3397 check_metrics::<JavaParser>(
3398 "int x = 1;
3399 for (int i = 0; i < 100; i++) {
3400 System.out.println(i);
3401 }",
3402 "foo.java",
3403 |metric| {
3404 // Spaces: 1
3405 insta::assert_json_snapshot!(
3406 metric.loc,
3407 @r#"
3408 {
3409 "sloc": 4,
3410 "ploc": 4,
3411 "lloc": 3,
3412 "cloc": 0,
3413 "blank": 0,
3414 "sloc_average": 4.0,
3415 "ploc_average": 4.0,
3416 "lloc_average": 3.0,
3417 "cloc_average": 0.0,
3418 "blank_average": 0.0,
3419 "sloc_min": 4,
3420 "sloc_max": 4,
3421 "cloc_min": 0,
3422 "cloc_max": 0,
3423 "ploc_min": 4,
3424 "ploc_max": 4,
3425 "lloc_min": 3,
3426 "lloc_max": 3,
3427 "blank_min": 0,
3428 "blank_max": 0
3429 }
3430 "#
3431 );
3432 },
3433 );
3434 }
3435
3436 #[cfg(feature = "java")]
3437 #[test]
3438 fn java_single_statement_lloc() {
3439 check_metrics::<JavaParser>("int max = 10;", "foo.java", |metric| {
3440 // Spaces: 1
3441 insta::assert_json_snapshot!(
3442 metric.loc,
3443 @r#"
3444 {
3445 "sloc": 1,
3446 "ploc": 1,
3447 "lloc": 1,
3448 "cloc": 0,
3449 "blank": 0,
3450 "sloc_average": 1.0,
3451 "ploc_average": 1.0,
3452 "lloc_average": 1.0,
3453 "cloc_average": 0.0,
3454 "blank_average": 0.0,
3455 "sloc_min": 1,
3456 "sloc_max": 1,
3457 "cloc_min": 0,
3458 "cloc_max": 0,
3459 "ploc_min": 1,
3460 "ploc_max": 1,
3461 "lloc_min": 1,
3462 "lloc_max": 1,
3463 "blank_min": 0,
3464 "blank_max": 0
3465 }
3466 "#
3467 );
3468 });
3469 }
3470
3471 #[cfg(feature = "java")]
3472 #[test]
3473 fn java_for_lloc() {
3474 check_metrics::<JavaParser>(
3475 "for (int i = 0; i < 100; i++) { // + 1
3476 System.out.println(i); // + 1
3477 }",
3478 "foo.java",
3479 |metric| {
3480 // Spaces: 1
3481 insta::assert_json_snapshot!(
3482 metric.loc,
3483 @r#"
3484 {
3485 "sloc": 3,
3486 "ploc": 3,
3487 "lloc": 2,
3488 "cloc": 2,
3489 "blank": 0,
3490 "sloc_average": 3.0,
3491 "ploc_average": 3.0,
3492 "lloc_average": 2.0,
3493 "cloc_average": 2.0,
3494 "blank_average": 0.0,
3495 "sloc_min": 3,
3496 "sloc_max": 3,
3497 "cloc_min": 2,
3498 "cloc_max": 2,
3499 "ploc_min": 3,
3500 "ploc_max": 3,
3501 "lloc_min": 2,
3502 "lloc_max": 2,
3503 "blank_min": 0,
3504 "blank_max": 0
3505 }
3506 "#
3507 );
3508 },
3509 );
3510 }
3511
3512 #[cfg(feature = "java")]
3513 #[test]
3514 fn java_foreach_lloc() {
3515 check_metrics::<JavaParser>(
3516 "
3517 int arr[]={12,13,14,44}; // +1
3518 for (int i:arr) { // +1
3519 System.out.println(i); // +1
3520 }",
3521 "foo.java",
3522 |metric| {
3523 // Spaces: 1
3524 insta::assert_json_snapshot!(
3525 metric.loc,
3526 @r#"
3527 {
3528 "sloc": 4,
3529 "ploc": 4,
3530 "lloc": 3,
3531 "cloc": 3,
3532 "blank": 0,
3533 "sloc_average": 4.0,
3534 "ploc_average": 4.0,
3535 "lloc_average": 3.0,
3536 "cloc_average": 3.0,
3537 "blank_average": 0.0,
3538 "sloc_min": 4,
3539 "sloc_max": 4,
3540 "cloc_min": 3,
3541 "cloc_max": 3,
3542 "ploc_min": 4,
3543 "ploc_max": 4,
3544 "lloc_min": 3,
3545 "lloc_max": 3,
3546 "blank_min": 0,
3547 "blank_max": 0
3548 }
3549 "#
3550 );
3551 },
3552 );
3553 }
3554
3555 #[cfg(feature = "java")]
3556 #[test]
3557 fn java_while_lloc() {
3558 check_metrics::<JavaParser>(
3559 "
3560 int i=0; // +1
3561 while(i < 10) { // +1
3562 i++; // +1
3563 System.out.println(i); // +1
3564 }",
3565 "foo.java",
3566 |metric| {
3567 // Spaces: 1
3568 insta::assert_json_snapshot!(
3569 metric.loc,
3570 @r#"
3571 {
3572 "sloc": 5,
3573 "ploc": 5,
3574 "lloc": 4,
3575 "cloc": 4,
3576 "blank": 0,
3577 "sloc_average": 5.0,
3578 "ploc_average": 5.0,
3579 "lloc_average": 4.0,
3580 "cloc_average": 4.0,
3581 "blank_average": 0.0,
3582 "sloc_min": 5,
3583 "sloc_max": 5,
3584 "cloc_min": 4,
3585 "cloc_max": 4,
3586 "ploc_min": 5,
3587 "ploc_max": 5,
3588 "lloc_min": 4,
3589 "lloc_max": 4,
3590 "blank_min": 0,
3591 "blank_max": 0
3592 }
3593 "#
3594 );
3595 },
3596 );
3597 }
3598
3599 #[cfg(feature = "java")]
3600 #[test]
3601 fn java_do_while_lloc() {
3602 check_metrics::<JavaParser>(
3603 "
3604 int i=0; // +1
3605 do { // +1
3606 i++; // +1
3607 System.out.println(i); // +1
3608 } while(i < 10)",
3609 "foo.java",
3610 |metric| {
3611 // Spaces: 1
3612 insta::assert_json_snapshot!(
3613 metric.loc,
3614 @r#"
3615 {
3616 "sloc": 5,
3617 "ploc": 5,
3618 "lloc": 4,
3619 "cloc": 4,
3620 "blank": 0,
3621 "sloc_average": 5.0,
3622 "ploc_average": 5.0,
3623 "lloc_average": 4.0,
3624 "cloc_average": 4.0,
3625 "blank_average": 0.0,
3626 "sloc_min": 5,
3627 "sloc_max": 5,
3628 "cloc_min": 4,
3629 "cloc_max": 4,
3630 "ploc_min": 5,
3631 "ploc_max": 5,
3632 "lloc_min": 4,
3633 "lloc_max": 4,
3634 "blank_min": 0,
3635 "blank_max": 0
3636 }
3637 "#
3638 );
3639 },
3640 );
3641 }
3642
3643 #[cfg(feature = "java")]
3644 #[test]
3645 fn java_switch_lloc() {
3646 check_metrics::<JavaParser>(
3647 "switch(grade) { // +1
3648 case 'A' :
3649 System.out.println(\"Pass with distinction\"); // +1
3650 break; // +1
3651 case 'B' :
3652 case 'C' :
3653 System.out.println(\"Pass\"); // +1
3654 break; // +1
3655 case 'D' :
3656 System.out.println(\"At risk\"); // +1
3657 case 'F' :
3658 System.out.println(\"Fail\"); // +1
3659 break; // +1
3660 default :
3661 System.out.println(\"Invalid grade\"); // +1
3662 }",
3663 "foo.java",
3664 |metric| {
3665 // Spaces: 1
3666 insta::assert_json_snapshot!(
3667 metric.loc,
3668 @r#"
3669 {
3670 "sloc": 16,
3671 "ploc": 16,
3672 "lloc": 9,
3673 "cloc": 9,
3674 "blank": 0,
3675 "sloc_average": 16.0,
3676 "ploc_average": 16.0,
3677 "lloc_average": 9.0,
3678 "cloc_average": 9.0,
3679 "blank_average": 0.0,
3680 "sloc_min": 16,
3681 "sloc_max": 16,
3682 "cloc_min": 9,
3683 "cloc_max": 9,
3684 "ploc_min": 16,
3685 "ploc_max": 16,
3686 "lloc_min": 9,
3687 "lloc_max": 9,
3688 "blank_min": 0,
3689 "blank_max": 0
3690 }
3691 "#
3692 );
3693 },
3694 );
3695 }
3696
3697 #[cfg(feature = "java")]
3698 #[test]
3699 fn java_continue_lloc() {
3700 check_metrics::<JavaParser>(
3701 "int max = 10; // +1
3702
3703 for (int i = 0; i < max; i++) { // +1
3704 if(i % 2 == 0) { continue;} + 2
3705 System.out.println(i); // +1
3706 }",
3707 "foo.java",
3708 |metric| {
3709 // Spaces: 1
3710 insta::assert_json_snapshot!(
3711 metric.loc,
3712 @r#"
3713 {
3714 "sloc": 6,
3715 "ploc": 5,
3716 "lloc": 5,
3717 "cloc": 3,
3718 "blank": 1,
3719 "sloc_average": 6.0,
3720 "ploc_average": 5.0,
3721 "lloc_average": 5.0,
3722 "cloc_average": 3.0,
3723 "blank_average": 1.0,
3724 "sloc_min": 6,
3725 "sloc_max": 6,
3726 "cloc_min": 3,
3727 "cloc_max": 3,
3728 "ploc_min": 5,
3729 "ploc_max": 5,
3730 "lloc_min": 5,
3731 "lloc_max": 5,
3732 "blank_min": 1,
3733 "blank_max": 1
3734 }
3735 "#
3736 );
3737 },
3738 );
3739 }
3740
3741 #[cfg(feature = "java")]
3742 #[test]
3743 fn java_try_lloc() {
3744 check_metrics::<JavaParser>(
3745 "try { // +1
3746 int[] myNumbers = {1, 2, 3}; // +1
3747 System.out.println(myNumbers[10]); // +1
3748 } catch (Exception e) {
3749 System.out.println(e.getMessage()); // +1
3750 throw e; // +1
3751 }",
3752 "foo.java",
3753 |metric| {
3754 // Spaces: 1
3755 insta::assert_json_snapshot!(
3756 metric.loc,
3757 @r#"
3758 {
3759 "sloc": 7,
3760 "ploc": 7,
3761 "lloc": 5,
3762 "cloc": 5,
3763 "blank": 0,
3764 "sloc_average": 7.0,
3765 "ploc_average": 7.0,
3766 "lloc_average": 5.0,
3767 "cloc_average": 5.0,
3768 "blank_average": 0.0,
3769 "sloc_min": 7,
3770 "sloc_max": 7,
3771 "cloc_min": 5,
3772 "cloc_max": 5,
3773 "ploc_min": 7,
3774 "ploc_max": 7,
3775 "lloc_min": 5,
3776 "lloc_max": 5,
3777 "blank_min": 0,
3778 "blank_max": 0
3779 }
3780 "#
3781 );
3782 },
3783 );
3784 }
3785
3786 #[cfg(feature = "java")]
3787 #[test]
3788 fn java_class_loc() {
3789 check_metrics::<JavaParser>(
3790 "
3791 public class Person {
3792 private String name;
3793 public Person(String name){
3794 this.name = name; // +1
3795 }
3796 public String getName() {
3797 return name; // +1
3798 }
3799 }",
3800 "foo.java",
3801 |metric| {
3802 // Spaces: 4
3803 insta::assert_json_snapshot!(
3804 metric.loc,
3805 @r#"
3806 {
3807 "sloc": 9,
3808 "ploc": 9,
3809 "lloc": 2,
3810 "cloc": 2,
3811 "blank": 0,
3812 "sloc_average": 2.25,
3813 "ploc_average": 2.25,
3814 "lloc_average": 0.5,
3815 "cloc_average": 0.5,
3816 "blank_average": 0.0,
3817 "sloc_min": 3,
3818 "sloc_max": 9,
3819 "cloc_min": 1,
3820 "cloc_max": 2,
3821 "ploc_min": 3,
3822 "ploc_max": 9,
3823 "lloc_min": 1,
3824 "lloc_max": 2,
3825 "blank_min": 0,
3826 "blank_max": 0
3827 }
3828 "#
3829 );
3830 },
3831 );
3832 }
3833
3834 #[cfg(feature = "java")]
3835 #[test]
3836 fn java_expressions_lloc() {
3837 check_metrics::<JavaParser>(
3838 "int x = 10; // +1 local var declaration
3839 x=+89; // +1 expression statement
3840 int y = x * 2; // +1 local var declaration
3841 IntFunction double = (n) -> n*2; // +1 local var declaration
3842 int y2 = double(x); // +1 local var declaration
3843 System.out.println(\"double \" + x + \" = \" + y2); // +1 expression statement
3844 String message = (x % 2) == 0 ? \"Evenly done.\" : \"Oddly done.\"; // +1 local var declaration
3845 Object done = (Runnable) () -> { System.out.println(\"Done!\"); }; // +2 local var declaration + expression statement
3846 String s = \"string\"; // +1 local var declaration
3847 boolean isS = (s instanceof String); // +1 local var declaration
3848 done.run(); // +1 expression statement
3849 ",
3850 "foo.java",
3851 |metric| {
3852 // Spaces: 1
3853 insta::assert_json_snapshot!(
3854 metric.loc,
3855 @r#"
3856 {
3857 "sloc": 11,
3858 "ploc": 11,
3859 "lloc": 12,
3860 "cloc": 11,
3861 "blank": 0,
3862 "sloc_average": 11.0,
3863 "ploc_average": 11.0,
3864 "lloc_average": 12.0,
3865 "cloc_average": 11.0,
3866 "blank_average": 0.0,
3867 "sloc_min": 11,
3868 "sloc_max": 11,
3869 "cloc_min": 11,
3870 "cloc_max": 11,
3871 "ploc_min": 11,
3872 "ploc_max": 11,
3873 "lloc_min": 12,
3874 "lloc_max": 12,
3875 "blank_min": 0,
3876 "blank_max": 0
3877 }
3878 "#
3879 );
3880 },
3881 );
3882 }
3883
3884 #[cfg(feature = "java")]
3885 #[test]
3886 fn java_statement_inline_loc() {
3887 check_metrics::<JavaParser>(
3888 "for (int i = 0; i < 100; i++) { System.out.println(\"hello\"); }",
3889 "foo.java",
3890 |metric| {
3891 // Spaces: 1
3892 insta::assert_json_snapshot!(
3893 metric.loc,
3894 @r#"
3895 {
3896 "sloc": 1,
3897 "ploc": 1,
3898 "lloc": 2,
3899 "cloc": 0,
3900 "blank": 0,
3901 "sloc_average": 1.0,
3902 "ploc_average": 1.0,
3903 "lloc_average": 2.0,
3904 "cloc_average": 0.0,
3905 "blank_average": 0.0,
3906 "sloc_min": 1,
3907 "sloc_max": 1,
3908 "cloc_min": 0,
3909 "cloc_max": 0,
3910 "ploc_min": 1,
3911 "ploc_max": 1,
3912 "lloc_min": 2,
3913 "lloc_max": 2,
3914 "blank_min": 0,
3915 "blank_max": 0
3916 }
3917 "#
3918 );
3919 },
3920 );
3921 }
3922
3923 #[cfg(feature = "java")]
3924 #[test]
3925 fn java_general_loc() {
3926 check_metrics::<JavaParser>(
3927 "int max = 100;
3928
3929 /*
3930 Loop through and print
3931 from: 0
3932 to: max
3933 */
3934 for (int i = 0; i < max; i++) {
3935 // Print the value
3936 System.out.println(i);
3937 }",
3938 "foo.java",
3939 |metric| {
3940 // Spaces: 1
3941 insta::assert_json_snapshot!(
3942 metric.loc,
3943 @r#"
3944 {
3945 "sloc": 11,
3946 "ploc": 4,
3947 "lloc": 3,
3948 "cloc": 6,
3949 "blank": 1,
3950 "sloc_average": 11.0,
3951 "ploc_average": 4.0,
3952 "lloc_average": 3.0,
3953 "cloc_average": 6.0,
3954 "blank_average": 1.0,
3955 "sloc_min": 11,
3956 "sloc_max": 11,
3957 "cloc_min": 6,
3958 "cloc_max": 6,
3959 "ploc_min": 4,
3960 "ploc_max": 4,
3961 "lloc_min": 3,
3962 "lloc_max": 3,
3963 "blank_min": 1,
3964 "blank_max": 1
3965 }
3966 "#
3967 );
3968 },
3969 );
3970 }
3971
3972 #[cfg(feature = "java")]
3973 #[test]
3974 fn java_main_class_loc() {
3975 check_metrics::<JavaParser>(
3976 "package com.company;
3977 /**
3978 * The HelloWorldApp class implements an application that
3979 * simply prints \"Hello World!\" to standard output.
3980 */
3981
3982 class HelloWorldApp {
3983 public void main(String[] args) {
3984 String message = args.length == 0 ? \"Hello empty world\" : \"Hello world\"; // +1 lloc : 1 var assignment
3985 System.out.println(message); // Display the string. +1 lloc
3986 }
3987 }",
3988 "foo.java",
3989 |metric| {
3990 // Spaces: 3
3991 insta::assert_json_snapshot!(
3992 metric.loc,
3993 @r#"
3994 {
3995 "sloc": 12,
3996 "ploc": 7,
3997 "lloc": 2,
3998 "cloc": 6,
3999 "blank": 1,
4000 "sloc_average": 4.0,
4001 "ploc_average": 2.3333333333333335,
4002 "lloc_average": 0.6666666666666666,
4003 "cloc_average": 2.0,
4004 "blank_average": 0.3333333333333333,
4005 "sloc_min": 4,
4006 "sloc_max": 12,
4007 "cloc_min": 2,
4008 "cloc_max": 6,
4009 "ploc_min": 4,
4010 "ploc_max": 7,
4011 "lloc_min": 2,
4012 "lloc_max": 2,
4013 "blank_min": 0,
4014 "blank_max": 1
4015 }
4016 "#
4017 );
4018 },
4019 );
4020 }
4021
4022 #[cfg(feature = "go")]
4023 #[test]
4024 fn go_general_loc() {
4025 check_metrics::<GoParser>(
4026 "package main
4027
4028 // entrypoint
4029 func main() {
4030 /* loop body */
4031 for i := 0; i < 10; i++ {
4032 fmt.Println(i)
4033 }
4034 }",
4035 "foo.go",
4036 |metric| {
4037 // Spaces: 2 (unit + main).
4038 // lloc: for_statement (+1), fmt.Println expression (+1).
4039 // `i := 0` and `i++` inside the for-clause are gated.
4040 // cloc: 2 comments (line + block).
4041 insta::assert_json_snapshot!(
4042 metric.loc,
4043 @r#"
4044 {
4045 "sloc": 9,
4046 "ploc": 6,
4047 "lloc": 2,
4048 "cloc": 2,
4049 "blank": 1,
4050 "sloc_average": 4.5,
4051 "ploc_average": 3.0,
4052 "lloc_average": 1.0,
4053 "cloc_average": 1.0,
4054 "blank_average": 0.5,
4055 "sloc_min": 6,
4056 "sloc_max": 9,
4057 "cloc_min": 1,
4058 "cloc_max": 2,
4059 "ploc_min": 5,
4060 "ploc_max": 6,
4061 "lloc_min": 2,
4062 "lloc_max": 2,
4063 "blank_min": 0,
4064 "blank_max": 1
4065 }
4066 "#
4067 );
4068 },
4069 );
4070 }
4071
4072 #[cfg(feature = "go")]
4073 #[test]
4074 fn go_for_clause_does_not_double_count_lloc() {
4075 // Bare `for` body has only a return; the `for_statement` itself is the
4076 // single logical line. Confirms ShortVarDeclaration in a for-clause
4077 // does not add an extra lloc.
4078 check_metrics::<GoParser>(
4079 "package main
4080 func f(n int) int {
4081 for i := 0; i < n; i++ {
4082 return i
4083 }
4084 return 0
4085 }",
4086 "foo.go",
4087 |metric| {
4088 // Expected lloc: for (+1), return (+1), return (+1) = 3.
4089 // Without the gate, ShortVarDeclaration would add an extra (+1).
4090 assert_eq!(metric.loc.lloc(), 3);
4091 },
4092 );
4093 }
4094
4095 #[cfg(feature = "go")]
4096 #[test]
4097 fn go_blank() {
4098 check_metrics::<GoParser>(
4099 "package main
4100
4101 func foo() {
4102 x := 1
4103
4104 y := 2
4105 }",
4106 "foo.go",
4107 |metric| {
4108 // Spaces: 2 (unit + foo).
4109 // blank: 2 (lines 2 and 5 are empty).
4110 insta::assert_json_snapshot!(
4111 metric.loc,
4112 @r#"
4113 {
4114 "sloc": 7,
4115 "ploc": 5,
4116 "lloc": 2,
4117 "cloc": 0,
4118 "blank": 2,
4119 "sloc_average": 3.5,
4120 "ploc_average": 2.5,
4121 "lloc_average": 1.0,
4122 "cloc_average": 0.0,
4123 "blank_average": 1.0,
4124 "sloc_min": 5,
4125 "sloc_max": 7,
4126 "cloc_min": 0,
4127 "cloc_max": 0,
4128 "ploc_min": 4,
4129 "ploc_max": 5,
4130 "lloc_min": 2,
4131 "lloc_max": 2,
4132 "blank_min": 1,
4133 "blank_max": 2
4134 }
4135 "#
4136 );
4137 },
4138 );
4139 }
4140
4141 #[cfg(feature = "go")]
4142 #[test]
4143 fn go_cloc_line_comments() {
4144 check_metrics::<GoParser>(
4145 "package main
4146
4147 // helper adds two numbers.
4148 // It returns their sum.
4149 func add(a, b int) int {
4150 // compute the result
4151 return a + b
4152 }",
4153 "foo.go",
4154 |metric| {
4155 // Spaces: 2 (unit + add).
4156 // cloc: 3 lines with `//` comments.
4157 insta::assert_json_snapshot!(
4158 metric.loc,
4159 @r#"
4160 {
4161 "sloc": 8,
4162 "ploc": 4,
4163 "lloc": 1,
4164 "cloc": 3,
4165 "blank": 1,
4166 "sloc_average": 4.0,
4167 "ploc_average": 2.0,
4168 "lloc_average": 0.5,
4169 "cloc_average": 1.5,
4170 "blank_average": 0.5,
4171 "sloc_min": 4,
4172 "sloc_max": 8,
4173 "cloc_min": 1,
4174 "cloc_max": 3,
4175 "ploc_min": 3,
4176 "ploc_max": 4,
4177 "lloc_min": 1,
4178 "lloc_max": 1,
4179 "blank_min": 0,
4180 "blank_max": 1
4181 }
4182 "#
4183 );
4184 },
4185 );
4186 }
4187
4188 #[cfg(feature = "go")]
4189 #[test]
4190 fn go_cloc_block_comments() {
4191 check_metrics::<GoParser>(
4192 "package main
4193
4194 /* block comment
4195 spanning two lines */
4196 func foo() {
4197 x := 1 /* inline block */
4198 }",
4199 "foo.go",
4200 |metric| {
4201 // Spaces: 2 (unit + foo).
4202 // cloc: 2-line block comment + inline block = 3 comment lines.
4203 insta::assert_json_snapshot!(
4204 metric.loc,
4205 @r#"
4206 {
4207 "sloc": 7,
4208 "ploc": 4,
4209 "lloc": 1,
4210 "cloc": 3,
4211 "blank": 1,
4212 "sloc_average": 3.5,
4213 "ploc_average": 2.0,
4214 "lloc_average": 0.5,
4215 "cloc_average": 1.5,
4216 "blank_average": 0.5,
4217 "sloc_min": 3,
4218 "sloc_max": 7,
4219 "cloc_min": 1,
4220 "cloc_max": 3,
4221 "ploc_min": 3,
4222 "ploc_max": 4,
4223 "lloc_min": 1,
4224 "lloc_max": 1,
4225 "blank_min": 0,
4226 "blank_max": 1
4227 }
4228 "#
4229 );
4230 },
4231 );
4232 }
4233
4234 #[cfg(feature = "go")]
4235 #[test]
4236 fn go_lloc_if_for_switch() {
4237 check_metrics::<GoParser>(
4238 "package main
4239
4240 func foo(n int) int {
4241 if n > 0 {
4242 for i := 0; i < n; i++ {
4243 switch i {
4244 }
4245 }
4246 }
4247 return n
4248 }",
4249 "foo.go",
4250 |metric| {
4251 // Spaces: 2 (unit + foo).
4252 // lloc: if (+1), for (+1), switch (+1), return (+1) = 4.
4253 insta::assert_json_snapshot!(
4254 metric.loc,
4255 @r#"
4256 {
4257 "sloc": 11,
4258 "ploc": 10,
4259 "lloc": 4,
4260 "cloc": 0,
4261 "blank": 1,
4262 "sloc_average": 5.5,
4263 "ploc_average": 5.0,
4264 "lloc_average": 2.0,
4265 "cloc_average": 0.0,
4266 "blank_average": 0.5,
4267 "sloc_min": 9,
4268 "sloc_max": 11,
4269 "cloc_min": 0,
4270 "cloc_max": 0,
4271 "ploc_min": 9,
4272 "ploc_max": 10,
4273 "lloc_min": 4,
4274 "lloc_max": 4,
4275 "blank_min": 0,
4276 "blank_max": 1
4277 }
4278 "#
4279 );
4280 },
4281 );
4282 }
4283
4284 #[cfg(feature = "go")]
4285 #[test]
4286 fn go_lloc_go_defer() {
4287 check_metrics::<GoParser>(
4288 "package main
4289
4290 func foo() {
4291 go run()
4292 defer cleanup()
4293 }",
4294 "foo.go",
4295 |metric| {
4296 // Spaces: 2 (unit + foo).
4297 // lloc: go (+1), defer (+1) = 2.
4298 insta::assert_json_snapshot!(
4299 metric.loc,
4300 @r#"
4301 {
4302 "sloc": 6,
4303 "ploc": 5,
4304 "lloc": 2,
4305 "cloc": 0,
4306 "blank": 1,
4307 "sloc_average": 3.0,
4308 "ploc_average": 2.5,
4309 "lloc_average": 1.0,
4310 "cloc_average": 0.0,
4311 "blank_average": 0.5,
4312 "sloc_min": 4,
4313 "sloc_max": 6,
4314 "cloc_min": 0,
4315 "cloc_max": 0,
4316 "ploc_min": 4,
4317 "ploc_max": 5,
4318 "lloc_min": 2,
4319 "lloc_max": 2,
4320 "blank_min": 0,
4321 "blank_max": 1
4322 }
4323 "#
4324 );
4325 },
4326 );
4327 }
4328
4329 #[cfg(feature = "go")]
4330 #[test]
4331 fn go_lloc_var_const_declarations() {
4332 check_metrics::<GoParser>(
4333 "package main
4334
4335 func foo() {
4336 var x int
4337 var y = 10
4338 const z = 42
4339 a := 3
4340 a = 4
4341 }",
4342 "foo.go",
4343 |metric| {
4344 // Spaces: 2 (unit + foo).
4345 // lloc: var (+1), var (+1), const (+1),
4346 // short_var_decl (+1), assignment (+1) = 5.
4347 insta::assert_json_snapshot!(
4348 metric.loc,
4349 @r#"
4350 {
4351 "sloc": 9,
4352 "ploc": 8,
4353 "lloc": 5,
4354 "cloc": 0,
4355 "blank": 1,
4356 "sloc_average": 4.5,
4357 "ploc_average": 4.0,
4358 "lloc_average": 2.5,
4359 "cloc_average": 0.0,
4360 "blank_average": 0.5,
4361 "sloc_min": 7,
4362 "sloc_max": 9,
4363 "cloc_min": 0,
4364 "cloc_max": 0,
4365 "ploc_min": 7,
4366 "ploc_max": 8,
4367 "lloc_min": 5,
4368 "lloc_max": 5,
4369 "blank_min": 0,
4370 "blank_max": 1
4371 }
4372 "#
4373 );
4374 },
4375 );
4376 }
4377
4378 #[cfg(feature = "go")]
4379 #[test]
4380 fn go_lloc_select() {
4381 check_metrics::<GoParser>(
4382 "package main
4383
4384 func foo(ch chan int) {
4385 select {
4386 case v := <-ch:
4387 _ = v
4388 }
4389 }",
4390 "foo.go",
4391 |metric| {
4392 // Spaces: 2 (unit + foo).
4393 // lloc: select (+1), assignment `_ = v` (+1) = 2.
4394 // `case v := <-ch:` is a receive_statement inside a
4395 // communication_case, not a ShortVarDeclaration.
4396 insta::assert_json_snapshot!(
4397 metric.loc,
4398 @r#"
4399 {
4400 "sloc": 8,
4401 "ploc": 7,
4402 "lloc": 2,
4403 "cloc": 0,
4404 "blank": 1,
4405 "sloc_average": 4.0,
4406 "ploc_average": 3.5,
4407 "lloc_average": 1.0,
4408 "cloc_average": 0.0,
4409 "blank_average": 0.5,
4410 "sloc_min": 6,
4411 "sloc_max": 8,
4412 "cloc_min": 0,
4413 "cloc_max": 0,
4414 "ploc_min": 6,
4415 "ploc_max": 7,
4416 "lloc_min": 2,
4417 "lloc_max": 2,
4418 "blank_min": 0,
4419 "blank_max": 1
4420 }
4421 "#
4422 );
4423 },
4424 );
4425 }
4426
4427 #[cfg(feature = "go")]
4428 #[test]
4429 fn go_sloc_multiline_function() {
4430 check_metrics::<GoParser>(
4431 "package main
4432
4433 func add(
4434 a int,
4435 b int,
4436 ) int {
4437 return a + b
4438 }",
4439 "foo.go",
4440 |metric| {
4441 // Spaces: 2 (unit + add).
4442 // The multi-line signature should count each line as sloc.
4443 insta::assert_json_snapshot!(
4444 metric.loc,
4445 @r#"
4446 {
4447 "sloc": 8,
4448 "ploc": 7,
4449 "lloc": 1,
4450 "cloc": 0,
4451 "blank": 1,
4452 "sloc_average": 4.0,
4453 "ploc_average": 3.5,
4454 "lloc_average": 0.5,
4455 "cloc_average": 0.0,
4456 "blank_average": 0.5,
4457 "sloc_min": 6,
4458 "sloc_max": 8,
4459 "cloc_min": 0,
4460 "cloc_max": 0,
4461 "ploc_min": 6,
4462 "ploc_max": 7,
4463 "lloc_min": 1,
4464 "lloc_max": 1,
4465 "blank_min": 0,
4466 "blank_max": 1
4467 }
4468 "#
4469 );
4470 },
4471 );
4472 }
4473
4474 #[cfg(feature = "go")]
4475 #[test]
4476 fn go_code_comment_same_line() {
4477 check_metrics::<GoParser>(
4478 "package main
4479
4480 func foo() {
4481 x := 1 // initialize x
4482 y := 2 // initialize y
4483 }",
4484 "foo.go",
4485 |metric| {
4486 // Spaces: 2 (unit + foo).
4487 // cloc: 2 (inline comments on code lines).
4488 // blank: 1 (line between package and func).
4489 // The code+comment lines should count for both ploc and cloc.
4490 insta::assert_json_snapshot!(
4491 metric.loc,
4492 @r#"
4493 {
4494 "sloc": 6,
4495 "ploc": 5,
4496 "lloc": 2,
4497 "cloc": 2,
4498 "blank": 1,
4499 "sloc_average": 3.0,
4500 "ploc_average": 2.5,
4501 "lloc_average": 1.0,
4502 "cloc_average": 1.0,
4503 "blank_average": 0.5,
4504 "sloc_min": 4,
4505 "sloc_max": 6,
4506 "cloc_min": 2,
4507 "cloc_max": 2,
4508 "ploc_min": 4,
4509 "ploc_max": 5,
4510 "lloc_min": 2,
4511 "lloc_max": 2,
4512 "blank_min": 0,
4513 "blank_max": 1
4514 }
4515 "#
4516 );
4517 },
4518 );
4519 }
4520
4521 #[cfg(feature = "perl")]
4522 #[test]
4523 fn perl_grammar_smoke() {
4524 // Pin the contract that tree-sitter-perl 1.1.2 cleanly parses every
4525 // Perl construct exercised by the rest of the `perl_*` test suite.
4526 // If a future grammar bump turns one of these into an error tree,
4527 // the metric assertions might still pass numerically by coincidence;
4528 // this test fails loudly instead.
4529 assert_perl_parses_cleanly(
4530 "use strict;
4531use warnings;
4532
4533# line comment
4534
4535=pod
4536multi-line POD
4537=cut
4538
4539sub factorial {
4540 my ($n) = @_;
4541 return 1 if $n <= 1;
4542 return $n * factorial($n - 1);
4543}
4544
4545my @arr = (1, 2, 3);
4546my %hash = (a => 1, b => 2);
4547my $closure = sub { return $_[0] + 1; };
4548
4549for my $i (1..3) {
4550 if ($i % 2 == 0) {
4551 print \"even\\n\";
4552 } elsif ($i == 1) {
4553 print \"one\\n\";
4554 } else {
4555 print \"odd\\n\";
4556 }
4557}
4558
4559while ($x > 0) {
4560 last if $x == 0;
4561 $x--;
4562}
4563
4564unless ($done) {
4565 next;
4566}
4567
4568my $heredoc = <<END;
4569hello
4570END
4571",
4572 );
4573 }
4574
4575 #[cfg(feature = "perl")]
4576 #[test]
4577 fn perl_blank() {
4578 check_metrics::<PerlParser>(
4579 "
4580
4581my $a = 42;
4582
4583my $b = 43;
4584
4585",
4586 "foo.pl",
4587 |metric| {
4588 insta::assert_json_snapshot!(metric.loc, @r#"
4589 {
4590 "sloc": 3,
4591 "ploc": 2,
4592 "lloc": 2,
4593 "cloc": 0,
4594 "blank": 1,
4595 "sloc_average": 3.0,
4596 "ploc_average": 2.0,
4597 "lloc_average": 2.0,
4598 "cloc_average": 0.0,
4599 "blank_average": 1.0,
4600 "sloc_min": 3,
4601 "sloc_max": 3,
4602 "cloc_min": 0,
4603 "cloc_max": 0,
4604 "ploc_min": 2,
4605 "ploc_max": 2,
4606 "lloc_min": 2,
4607 "lloc_max": 2,
4608 "blank_min": 1,
4609 "blank_max": 1
4610 }
4611 "#);
4612 },
4613 );
4614 }
4615
4616 #[cfg(feature = "perl")]
4617 #[test]
4618 fn perl_no_zero_blank() {
4619 // Blank line interleaved with code that carries trailing comments —
4620 // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
4621 check_metrics::<PerlParser>(
4622 "my $a = 1;
4623my $b = 2;
4624
4625my $c = 3; # trailing
4626my $d = 4; # trailing
4627my $e = 5;",
4628 "foo.pl",
4629 |metric| {
4630 assert_eq!(metric.loc.sloc(), 6);
4631 assert_eq!(metric.loc.ploc(), 5);
4632 assert_eq!(metric.loc.cloc(), 2);
4633 assert_eq!(metric.loc.blank(), 1);
4634 insta::assert_json_snapshot!(metric.loc);
4635 },
4636 );
4637 }
4638
4639 #[cfg(feature = "perl")]
4640 #[test]
4641 fn perl_blank_zero_sanity() {
4642 // Sanity check: blank must report 0, never go negative, when the
4643 // input has no blank lines.
4644 check_metrics::<PerlParser>(
4645 "my $a = 1;
4646my $b = 2;",
4647 "foo.pl",
4648 |metric| {
4649 assert_eq!(metric.loc.sloc(), 2);
4650 assert_eq!(metric.loc.ploc(), 2);
4651 assert_eq!(metric.loc.lloc(), 2);
4652 assert_eq!(metric.loc.cloc(), 0);
4653 assert_eq!(metric.loc.blank(), 0);
4654 },
4655 );
4656 }
4657
4658 /// expected: row 0 is comment-only, row 1 is code carrying a trailing
4659 /// comment, row 2 is code — so `ploc 2` and `cloc 2`, with row 1 in
4660 /// both tallies. This pinned `ploc 3` until #1137: the `#` token
4661 /// inside the `comments` node reached the PLOC catch-all, which also
4662 /// reclassified row 0 from comment-only to code-and-comment.
4663 #[cfg(feature = "perl")]
4664 #[test]
4665 fn perl_cloc_line_comments() {
4666 check_metrics::<PerlParser>(
4667 "# top comment
4668my $a = 1; # trailing
4669my $b = 2;",
4670 "foo.pl",
4671 |metric| {
4672 insta::assert_json_snapshot!(metric.loc, @r#"
4673 {
4674 "sloc": 3,
4675 "ploc": 2,
4676 "lloc": 2,
4677 "cloc": 2,
4678 "blank": 0,
4679 "sloc_average": 3.0,
4680 "ploc_average": 2.0,
4681 "lloc_average": 2.0,
4682 "cloc_average": 2.0,
4683 "blank_average": 0.0,
4684 "sloc_min": 3,
4685 "sloc_max": 3,
4686 "cloc_min": 2,
4687 "cloc_max": 2,
4688 "ploc_min": 2,
4689 "ploc_max": 2,
4690 "lloc_min": 2,
4691 "lloc_max": 2,
4692 "blank_min": 0,
4693 "blank_max": 0
4694 }
4695 "#);
4696 },
4697 );
4698 }
4699
4700 #[cfg(feature = "perl")]
4701 #[test]
4702 fn perl_cloc_pod_block() {
4703 check_metrics::<PerlParser>(
4704 "my $x = 1;
4705=pod
4706multi-line
4707pod block
4708=cut
4709my $y = 2;",
4710 "foo.pl",
4711 |metric| {
4712 insta::assert_json_snapshot!(metric.loc, @r#"
4713 {
4714 "sloc": 6,
4715 "ploc": 2,
4716 "lloc": 2,
4717 "cloc": 4,
4718 "blank": 0,
4719 "sloc_average": 6.0,
4720 "ploc_average": 2.0,
4721 "lloc_average": 2.0,
4722 "cloc_average": 4.0,
4723 "blank_average": 0.0,
4724 "sloc_min": 6,
4725 "sloc_max": 6,
4726 "cloc_min": 4,
4727 "cloc_max": 4,
4728 "ploc_min": 2,
4729 "ploc_max": 2,
4730 "lloc_min": 2,
4731 "lloc_max": 2,
4732 "blank_min": 0,
4733 "blank_max": 0
4734 }
4735 "#);
4736 },
4737 );
4738 }
4739
4740 #[cfg(feature = "perl")]
4741 #[test]
4742 fn perl_lloc_simple_statements() {
4743 check_metrics::<PerlParser>(
4744 "my $a = 1;
4745my $b = 2;
4746my $c = 3;",
4747 "foo.pl",
4748 |metric| {
4749 insta::assert_json_snapshot!(metric.loc, @r#"
4750 {
4751 "sloc": 3,
4752 "ploc": 3,
4753 "lloc": 3,
4754 "cloc": 0,
4755 "blank": 0,
4756 "sloc_average": 3.0,
4757 "ploc_average": 3.0,
4758 "lloc_average": 3.0,
4759 "cloc_average": 0.0,
4760 "blank_average": 0.0,
4761 "sloc_min": 3,
4762 "sloc_max": 3,
4763 "cloc_min": 0,
4764 "cloc_max": 0,
4765 "ploc_min": 3,
4766 "ploc_max": 3,
4767 "lloc_min": 3,
4768 "lloc_max": 3,
4769 "blank_min": 0,
4770 "blank_max": 0
4771 }
4772 "#);
4773 },
4774 );
4775 }
4776
4777 #[cfg(feature = "perl")]
4778 #[test]
4779 fn perl_lloc_compound_statements() {
4780 check_metrics::<PerlParser>(
4781 "if ($x) {
4782 print 'a';
4783}
4784while ($n > 0) {
4785 $n--;
4786}",
4787 "foo.pl",
4788 |metric| {
4789 insta::assert_json_snapshot!(metric.loc, @r#"
4790 {
4791 "sloc": 6,
4792 "ploc": 6,
4793 "lloc": 4,
4794 "cloc": 0,
4795 "blank": 0,
4796 "sloc_average": 6.0,
4797 "ploc_average": 6.0,
4798 "lloc_average": 4.0,
4799 "cloc_average": 0.0,
4800 "blank_average": 0.0,
4801 "sloc_min": 6,
4802 "sloc_max": 6,
4803 "cloc_min": 0,
4804 "cloc_max": 0,
4805 "ploc_min": 6,
4806 "ploc_max": 6,
4807 "lloc_min": 4,
4808 "lloc_max": 4,
4809 "blank_min": 0,
4810 "blank_max": 0
4811 }
4812 "#);
4813 },
4814 );
4815 }
4816
4817 #[cfg(feature = "perl")]
4818 #[test]
4819 fn perl_lloc_postfix_form_counts_once() {
4820 // `do_thing() if cond;` is one logical line — wrapped in
4821 // single_line_statement; the inner if_simple_statement does not
4822 // add a second LLOC.
4823 check_metrics::<PerlParser>(
4824 "sub f {
4825 return 1 if $_[0];
4826}",
4827 "foo.pl",
4828 |metric| {
4829 assert_eq!(metric.loc.lloc(), 1);
4830 },
4831 );
4832 }
4833
4834 #[cfg(feature = "perl")]
4835 #[test]
4836 fn perl_lloc_use_statement() {
4837 check_metrics::<PerlParser>(
4838 "use strict;
4839use warnings;
4840my $x = 1;",
4841 "foo.pl",
4842 |metric| {
4843 insta::assert_json_snapshot!(metric.loc, @r#"
4844 {
4845 "sloc": 3,
4846 "ploc": 3,
4847 "lloc": 3,
4848 "cloc": 0,
4849 "blank": 0,
4850 "sloc_average": 3.0,
4851 "ploc_average": 3.0,
4852 "lloc_average": 3.0,
4853 "cloc_average": 0.0,
4854 "blank_average": 0.0,
4855 "sloc_min": 3,
4856 "sloc_max": 3,
4857 "cloc_min": 0,
4858 "cloc_max": 0,
4859 "ploc_min": 3,
4860 "ploc_max": 3,
4861 "lloc_min": 3,
4862 "lloc_max": 3,
4863 "blank_min": 0,
4864 "blank_max": 0
4865 }
4866 "#);
4867 },
4868 );
4869 }
4870
4871 #[cfg(feature = "perl")]
4872 #[test]
4873 fn perl_lloc_for_loop() {
4874 check_metrics::<PerlParser>(
4875 "for my $i (1..3) {
4876 print $i;
4877}",
4878 "foo.pl",
4879 |metric| {
4880 // `for_statement_2` (+1) and `print …;` SEMI in block (+1) → 2
4881 assert_eq!(metric.loc.lloc(), 2);
4882 },
4883 );
4884 }
4885
4886 #[cfg(feature = "perl")]
4887 #[test]
4888 fn perl_lloc_loop_control_statement() {
4889 check_metrics::<PerlParser>(
4890 "while (1) {
4891 last if $done;
4892}",
4893 "foo.pl",
4894 |metric| {
4895 // while_statement (+1) + loop_control_statement (+1) = 2
4896 assert_eq!(metric.loc.lloc(), 2);
4897 },
4898 );
4899 }
4900
4901 #[cfg(feature = "perl")]
4902 #[test]
4903 fn perl_lloc_no_double_count_inside_single_line_statement() {
4904 // SEMI inside a single_line_statement (postfix form) is a child of
4905 // if_simple_statement, not Block — so it must not add a second LLOC.
4906 check_metrics::<PerlParser>(
4907 "sub f {
4908 print 'a' unless $_[0];
4909}",
4910 "foo.pl",
4911 |metric| {
4912 assert_eq!(metric.loc.lloc(), 1);
4913 },
4914 );
4915 }
4916
4917 #[cfg(feature = "perl")]
4918 #[test]
4919 fn perl_lloc_function_definition_not_counted() {
4920 // `sub f { ... }` itself is a function space, not an LLOC; only its
4921 // body statements count.
4922 check_metrics::<PerlParser>(
4923 "sub f {
4924 my $x = 1;
4925}",
4926 "foo.pl",
4927 |metric| {
4928 assert_eq!(metric.loc.lloc(), 1);
4929 },
4930 );
4931 }
4932
4933 #[cfg(feature = "perl")]
4934 #[test]
4935 fn perl_lloc_anonymous_function() {
4936 // `my $f = sub { return 1; };` — the assignment is one LLOC at the
4937 // top level (the SEMI after `};`); the `return 1;` inside the
4938 // anonymous function block is a second LLOC inside the closure.
4939 check_metrics::<PerlParser>("my $f = sub { return 1; };", "foo.pl", |metric| {
4940 assert_eq!(metric.loc.lloc(), 2);
4941 });
4942 }
4943
4944 #[cfg(feature = "perl")]
4945 #[test]
4946 fn perl_multiline_string_assignment_ploc() {
4947 // Regression test for issue #778: interior rows of a multi-line string
4948 // literal are real code, not blank lines, and must be credited to PLOC
4949 // exactly as Python does (#415). Previously Perl no-op'd its string
4950 // kinds, so row 1 reached neither PLOC nor CLOC and `blank =
4951 // sloc - ploc - cloc` mislabelled it as blank (ploc was 2, blank 1).
4952 // Row 0 holds `my $s = "line1`, row 1 `line2`, row 2 `line3";`.
4953 check_metrics::<PerlParser>(
4954 "my $s = \"line1
4955line2
4956line3\";",
4957 "foo.pl",
4958 |metric| {
4959 // Three physical rows, all code, no blanks — matching Python.
4960 assert_eq!(metric.loc.sloc(), 3);
4961 assert_eq!(metric.loc.ploc(), 3);
4962 assert_eq!(metric.loc.cloc(), 0);
4963 assert_eq!(metric.loc.blank(), 0);
4964 },
4965 );
4966 }
4967
4968 // The union of the fixture languages below, so a feature subset that
4969 // enables none of them makes this test *absent* rather than failing
4970 // in a way that reads as a defect in whatever is being changed
4971 // (`.claude/rules/testing.md`). Every call site carries its own
4972 // narrower gate, so if the test exists at least one row runs and it
4973 // cannot go vacuous.
4974 #[cfg(any(
4975 feature = "bash",
4976 feature = "c",
4977 feature = "elixir",
4978 feature = "go",
4979 feature = "irules",
4980 feature = "kotlin",
4981 feature = "mozcpp",
4982 feature = "mozjs",
4983 feature = "perl",
4984 feature = "php",
4985 feature = "python",
4986 feature = "ruby",
4987 feature = "tcl",
4988 ))]
4989 #[test]
4990 fn multiline_string_ploc_consistent_across_languages() {
4991 // Cross-language parity for issue #778: the SAME 3-line string
4992 // assignment must report identical ploc / blank in every language
4993 // that has a multi-line string literal. The canonical value is
4994 // Python's #415 decision: all three rows are code, none are blank.
4995 // `check_metrics` takes a plain `fn(CodeMetrics)`, so the shared
4996 // assertion is a named function rather than a capturing closure and
4997 // must take its argument by value to match that pointer type.
4998 //
4999 // The middle row is **empty**, and that is the point (#1412).
5000 // Until then every fixture here read `line1\nline2\nline3`, which
5001 // a language passes whether or not it credits an interior row
5002 // with no text — so the table could not see either #1396 (PHP) or
5003 // #1412 (Bash), the two defects it is the cross-language home
5004 // for. C is the one language that cannot express the shape: its
5005 // multi-row literal is a backslash-newline continuation, and an
5006 // unescaped empty row ends the string, so its row keeps `line2`.
5007 //
5008 // Gated on its own callers rather than on the test's union: Go is
5009 // in the union and is the one row with different expectations, so
5010 // an ungated helper is dead code in a Go-only build.
5011 #[cfg(any(
5012 feature = "bash",
5013 feature = "c",
5014 feature = "elixir",
5015 feature = "irules",
5016 feature = "kotlin",
5017 feature = "mozcpp",
5018 feature = "mozjs",
5019 feature = "perl",
5020 feature = "php",
5021 feature = "python",
5022 feature = "ruby",
5023 feature = "tcl",
5024 ))]
5025 #[allow(clippy::needless_pass_by_value)]
5026 fn assert_three_code_rows(metric: crate::CodeMetrics) {
5027 assert_eq!(metric.loc.sloc(), 3);
5028 assert_eq!(metric.loc.ploc(), 3);
5029 assert_eq!(metric.loc.cloc(), 0);
5030 assert_eq!(metric.loc.blank(), 0);
5031 }
5032 // The heredoc forms carry their opener and terminator on rows of
5033 // their own, so they are four rows rather than three.
5034 #[cfg(any(feature = "bash", feature = "php"))]
5035 #[allow(clippy::needless_pass_by_value)]
5036 fn assert_four_code_rows(metric: crate::CodeMetrics) {
5037 assert_eq!(metric.loc.sloc(), 4);
5038 assert_eq!(metric.loc.ploc(), 4);
5039 assert_eq!(metric.loc.cloc(), 0);
5040 assert_eq!(metric.loc.blank(), 0);
5041 }
5042 #[cfg(feature = "python")]
5043 check_metrics::<PythonParser>(
5044 "s = \"\"\"line1\n\nline3\"\"\"",
5045 "foo.py",
5046 assert_three_code_rows,
5047 );
5048 #[cfg(feature = "perl")]
5049 check_metrics::<PerlParser>(
5050 "my $s = \"line1\n\nline3\";",
5051 "foo.pl",
5052 assert_three_code_rows,
5053 );
5054 #[cfg(feature = "ruby")]
5055 check_metrics::<RubyParser>("s = \"line1\n\nline3\"", "foo.rb", assert_three_code_rows);
5056 // Go, Kotlin, and Mozilla-C++ reach the same shared
5057 // `add_multiline_string_ploc` helper through their own
5058 // raw-string kinds (`raw_string_literal`,
5059 // `multiline_string_literal`, `raw_string_literal`), and were
5060 // the three call sites of it that no test exercised. Each needs
5061 // its own syntax, so they cannot reuse the quoted form above.
5062 #[cfg(feature = "go")]
5063 check_metrics::<GoParser>(
5064 "package p\n\nvar s = `line1\n\nline3`",
5065 "foo.go",
5066 |metric| {
5067 // Two extra code rows for `package p` and the blank
5068 // between it and the declaration, which Go requires.
5069 assert_eq!(metric.loc.sloc(), 5);
5070 assert_eq!(metric.loc.ploc(), 4);
5071 assert_eq!(metric.loc.cloc(), 0);
5072 assert_eq!(metric.loc.blank(), 1);
5073 },
5074 );
5075 #[cfg(feature = "kotlin")]
5076 check_metrics::<KotlinParser>(
5077 "val s = \"\"\"line1\n\nline3\"\"\"",
5078 "foo.kt",
5079 assert_three_code_rows,
5080 );
5081 #[cfg(feature = "mozcpp")]
5082 check_metrics::<MozcppParser>(
5083 "const char* s = R\"(line1\n\nline3)\";",
5084 "foo.cpp",
5085 assert_three_code_rows,
5086 );
5087 // C has no raw string; tree-sitter-c folds a backslash-newline
5088 // continuation into one `string_literal` spanning every row, and
5089 // that continuation is why the C row below is the one that keeps
5090 // a non-empty middle line.
5091 // Mozjs carries the same `template_string` arm as its upstream
5092 // JS siblings, but only `.jsm` routes to it, so the JavaScript
5093 // and TypeScript template tests never reach this copy.
5094 #[cfg(feature = "c")]
5095 check_metrics::<CParser>(
5096 "const char* s = \"line1\\\nline2\\\nline3\";",
5097 "foo.c",
5098 assert_three_code_rows,
5099 );
5100 #[cfg(feature = "mozjs")]
5101 check_metrics::<MozjsParser>(
5102 "const s = `line1\n\nline3`;",
5103 "foo.jsm",
5104 assert_three_code_rows,
5105 );
5106 // The four languages #778 missed, added by #1260. Bash's shape is
5107 // the single-quoted `raw_string`; Tcl and its iRules dialect spell
5108 // the literal `quoted_word`; Elixir routes the `quoted_content`
5109 // every one of its string forms wraps.
5110 #[cfg(feature = "bash")]
5111 check_metrics::<BashParser>("s='line1\n\nline3'", "foo.sh", assert_three_code_rows);
5112 #[cfg(feature = "tcl")]
5113 check_metrics::<TclParser>(
5114 "set s \"line1\n\nline3\"",
5115 "foo.tcl",
5116 assert_three_code_rows,
5117 );
5118 #[cfg(feature = "irules")]
5119 check_metrics::<IrulesParser>(
5120 "set s \"line1\n\nline3\"",
5121 "foo.irule",
5122 assert_three_code_rows,
5123 );
5124 #[cfg(feature = "elixir")]
5125 check_metrics::<ElixirParser>("s = \"line1\n\nline3\"", "foo.ex", assert_three_code_rows);
5126 // PHP was absent from this table entirely — including for the
5127 // quoted forms #778 routed — which is half of why #1396 survived
5128 // #778.
5129 #[cfg(feature = "php")]
5130 check_metrics::<PhpParser>(
5131 "<?php $s = \"line1\n\nline3\";",
5132 "foo.php",
5133 assert_three_code_rows,
5134 );
5135 // The two heredocs. Neither language's quoted form above can
5136 // reach the defect its heredoc had: PHP's `heredoc` wrapper
5137 // (#1396) and Bash's `heredoc_redirect` (#1412) are separate
5138 // grammar nodes from the quoted literal, and each dropped a body
5139 // row that had no text.
5140 #[cfg(feature = "php")]
5141 check_metrics::<PhpParser>(
5142 "<?php $s = <<<EOT\n\nline3\nEOT;",
5143 "foo.php",
5144 assert_four_code_rows,
5145 );
5146 #[cfg(feature = "bash")]
5147 check_metrics::<BashParser>("cat <<EOT\n\nline3\nEOT", "foo.sh", assert_four_code_rows);
5148 }
5149
5150 #[cfg(any(
5151 feature = "bash",
5152 feature = "elixir",
5153 feature = "irules",
5154 feature = "php",
5155 feature = "tcl",
5156 ))]
5157 #[test]
5158 fn multiline_literal_ending_at_eof_credits_every_row() {
5159 // The #1260 fix lives in exactly the class both metric harnesses
5160 // normalise away: `check_metrics` and the integration
5161 // `read_file_with_eol` path each force a trailing newline, so a
5162 // node ending at EOF is unreachable from either and a test written
5163 // the ordinary way passes against unfixed code (#1051, #1067 —
5164 // `.claude/rules/testing.md`). These fixtures reach the parser
5165 // byte-for-byte through `metrics_verbatim`, with the closing
5166 // delimiter as the last byte of the file.
5167 //
5168 // The interior row is empty in every fixture, for the reason the
5169 // sibling table above records: `line1\nline2\nline3` passes
5170 // whether or not a textless interior row is credited, so the
5171 // table could not see #1396 or #1412.
5172 //
5173 // `(language, source, rows the file has)`. Every row is fully
5174 // code and none blank.
5175 const FIXTURES: &[(crate::LANG, &[u8], u64)] = &[
5176 #[cfg(feature = "bash")]
5177 (crate::LANG::Bash, b"s='line1\n\nline3'", 3),
5178 #[cfg(feature = "tcl")]
5179 (crate::LANG::Tcl, b"set s \"line1\n\nline3\"", 3),
5180 #[cfg(feature = "irules")]
5181 (crate::LANG::Irules, b"set s \"line1\n\nline3\"", 3),
5182 #[cfg(feature = "elixir")]
5183 (crate::LANG::Elixir, b"s = \"line1\n\nline3\"", 3),
5184 #[cfg(feature = "php")]
5185 (crate::LANG::Php, b"<?php $s = \"line1\n\nline3\";", 3),
5186 // The heredocs, whose terminator is the file's last row —
5187 // the one shape a heredoc can end at EOF in.
5188 #[cfg(feature = "php")]
5189 (crate::LANG::Php, b"<?php $s = <<<EOT\n\nline3\nEOT;", 4),
5190 #[cfg(feature = "bash")]
5191 (crate::LANG::Bash, b"cat <<EOT\n\nline3\nEOT", 4),
5192 // Empty throughout, which is the shape whose body node
5193 // collapses to zero width (#1412).
5194 #[cfg(feature = "bash")]
5195 (crate::LANG::Bash, b"cat <<EOT\n\nEOT", 3),
5196 ];
5197
5198 crate::test_support::assert_fixtures_present(FIXTURES);
5199 for &(lang, source, rows) in FIXTURES {
5200 let text = String::from_utf8_lossy(source);
5201 let loc = metrics_verbatim(lang, source, MetricsOptions::default()).loc;
5202 assert_eq!(loc.sloc(), rows, "{lang:?} sloc for {text:?}");
5203 assert_eq!(loc.ploc(), rows, "{lang:?} ploc for {text:?}");
5204 assert_eq!(loc.cloc(), 0, "{lang:?} cloc for {text:?}");
5205 assert_eq!(loc.blank(), 0, "{lang:?} blank for {text:?}");
5206 }
5207 }
5208
5209 #[cfg(feature = "perl")]
5210 #[test]
5211 fn perl_lloc_unless_until() {
5212 check_metrics::<PerlParser>(
5213 "unless ($x) {
5214 print 'a';
5215}
5216until ($n == 0) {
5217 $n--;
5218}",
5219 "foo.pl",
5220 |metric| {
5221 // unless_statement (+1) + print SEMI (+1) + until_statement (+1)
5222 // + $n-- SEMI (+1) = 4
5223 assert_eq!(metric.loc.lloc(), 4);
5224 },
5225 );
5226 }
5227
5228 #[cfg(feature = "perl")]
5229 #[test]
5230 fn perl_lloc_heredoc_body_not_counted() {
5231 // Heredoc body content is data, not code: the body lines should not
5232 // contribute LLOC or PLOC.
5233 check_metrics::<PerlParser>(
5234 "my $s = <<END;
5235line1
5236line2
5237END
5238my $x = 1;",
5239 "foo.pl",
5240 |metric| {
5241 // Two top-level statements: the heredoc-using `my $s = …;`
5242 // and `my $x = 1;`.
5243 assert_eq!(metric.loc.lloc(), 2);
5244 },
5245 );
5246 // Independent confirmation that the snippet is a valid heredoc and
5247 // not silently parsed as an error tree (which could otherwise yield
5248 // the same `lloc == 2.0` and mask a grammar regression).
5249 assert_perl_parses_cleanly(
5250 "my $s = <<END;
5251line1
5252line2
5253END
5254my $x = 1;",
5255 );
5256 }
5257
5258 #[cfg(feature = "perl")]
5259 #[test]
5260 fn perl_lloc_package_and_require() {
5261 check_metrics::<PerlParser>(
5262 "package Foo;
5263require 5.010;
5264my $x = 1;",
5265 "foo.pl",
5266 |metric| {
5267 insta::assert_json_snapshot!(metric.loc, @r#"
5268 {
5269 "sloc": 3,
5270 "ploc": 3,
5271 "lloc": 3,
5272 "cloc": 0,
5273 "blank": 0,
5274 "sloc_average": 3.0,
5275 "ploc_average": 3.0,
5276 "lloc_average": 3.0,
5277 "cloc_average": 0.0,
5278 "blank_average": 0.0,
5279 "sloc_min": 3,
5280 "sloc_max": 3,
5281 "cloc_min": 0,
5282 "cloc_max": 0,
5283 "ploc_min": 3,
5284 "ploc_max": 3,
5285 "lloc_min": 3,
5286 "lloc_max": 3,
5287 "blank_min": 0,
5288 "blank_max": 0
5289 }
5290 "#);
5291 },
5292 );
5293 }
5294
5295 #[cfg(feature = "lua")]
5296 #[test]
5297 fn lua_blank() {
5298 check_metrics::<LuaParser>(
5299 "local x = 1
5300
5301local y = 2",
5302 "foo.lua",
5303 |metric| {
5304 assert_eq!(metric.loc.sloc(), 3);
5305 assert_eq!(metric.loc.ploc(), 2);
5306 assert_eq!(metric.loc.lloc(), 2);
5307 assert_eq!(metric.loc.cloc(), 0);
5308 assert_eq!(metric.loc.blank(), 1);
5309 insta::assert_json_snapshot!(metric.loc);
5310 },
5311 );
5312 }
5313
5314 #[cfg(feature = "lua")]
5315 #[test]
5316 fn lua_no_zero_blank() {
5317 // Blank line interleaved with code that carries trailing comments —
5318 // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
5319 check_metrics::<LuaParser>(
5320 "local a = 1
5321local b = 2
5322
5323local c = 3 -- trailing
5324local d = 4 -- trailing
5325local e = 5",
5326 "foo.lua",
5327 |metric| {
5328 assert_eq!(metric.loc.sloc(), 6);
5329 assert_eq!(metric.loc.ploc(), 5);
5330 assert_eq!(metric.loc.cloc(), 2);
5331 assert_eq!(metric.loc.blank(), 1);
5332 insta::assert_json_snapshot!(metric.loc);
5333 },
5334 );
5335 }
5336
5337 #[cfg(feature = "lua")]
5338 #[test]
5339 fn lua_blank_zero_sanity() {
5340 // Sanity check: blank must report 0, never go negative, when the
5341 // input has no blank lines.
5342 check_metrics::<LuaParser>(
5343 "local x = 1
5344local y = 2",
5345 "foo.lua",
5346 |metric| {
5347 assert_eq!(metric.loc.sloc(), 2);
5348 assert_eq!(metric.loc.ploc(), 2);
5349 assert_eq!(metric.loc.lloc(), 2);
5350 assert_eq!(metric.loc.cloc(), 0);
5351 assert_eq!(metric.loc.blank(), 0);
5352 },
5353 );
5354 }
5355
5356 #[cfg(feature = "lua")]
5357 #[test]
5358 fn lua_cloc() {
5359 check_metrics::<LuaParser>(
5360 "-- single line comment
5361local x = 1
5362--[[
5363 block comment
5364 second line
5365]]",
5366 "foo.lua",
5367 |metric| {
5368 assert_eq!(metric.loc.sloc(), 6);
5369 assert_eq!(metric.loc.ploc(), 1);
5370 assert_eq!(metric.loc.lloc(), 1);
5371 assert_eq!(metric.loc.cloc(), 5);
5372 assert_eq!(metric.loc.blank(), 0);
5373 insta::assert_json_snapshot!(metric.loc);
5374 },
5375 );
5376 }
5377
5378 #[cfg(feature = "lua")]
5379 #[test]
5380 fn lua_lloc() {
5381 check_metrics::<LuaParser>(
5382 "local function f(x)
5383 if x > 0 then
5384 local y = x + 1
5385 return y
5386 end
5387 return 0
5388end",
5389 "foo.lua",
5390 |metric| {
5391 assert_eq!(metric.loc.sloc(), 7);
5392 assert_eq!(metric.loc.ploc(), 7);
5393 assert_eq!(metric.loc.lloc(), 5);
5394 assert_eq!(metric.loc.cloc(), 0);
5395 assert_eq!(metric.loc.blank(), 0);
5396 insta::assert_json_snapshot!(metric.loc);
5397 },
5398 );
5399 }
5400
5401 #[cfg(feature = "lua")]
5402 #[test]
5403 fn lua_no_string_lloc() {
5404 // Long strings spanning multiple lines must not inflate lloc.
5405 check_metrics::<LuaParser>(
5406 "local s = [[
5407 line one
5408 line two
5409]]",
5410 "foo.lua",
5411 |metric| {
5412 // #778: a multi-line long-bracket string credits every spanned
5413 // row to PLOC (matching Python's #415 decision), so all four
5414 // rows are code and none are blank. It still contributes a
5415 // single lloc — the assignment statement.
5416 assert_eq!(metric.loc.sloc(), 4);
5417 assert_eq!(metric.loc.ploc(), 4);
5418 assert_eq!(metric.loc.lloc(), 1);
5419 assert_eq!(metric.loc.cloc(), 0);
5420 assert_eq!(metric.loc.blank(), 0);
5421 insta::assert_json_snapshot!(metric.loc);
5422 },
5423 );
5424 }
5425
5426 #[cfg(feature = "lua")]
5427 #[test]
5428 fn lua_no_functiondefinition_lloc() {
5429 // Anonymous function definition is an expression, not a statement.
5430 // The containing variable_declaration counts as lloc; FunctionDefinition must not.
5431 check_metrics::<LuaParser>(
5432 "local f = function(x)
5433 return x + 1
5434end",
5435 "foo.lua",
5436 |metric| {
5437 assert_eq!(metric.loc.sloc(), 3);
5438 assert_eq!(metric.loc.ploc(), 3);
5439 assert_eq!(metric.loc.lloc(), 2);
5440 assert_eq!(metric.loc.cloc(), 0);
5441 assert_eq!(metric.loc.blank(), 0);
5442 insta::assert_json_snapshot!(metric.loc);
5443 },
5444 );
5445 }
5446
5447 #[cfg(feature = "lua")]
5448 #[test]
5449 fn lua_no_elseif_lloc() {
5450 // elseif_statement must not add lloc; only if_statement does.
5451 check_metrics::<LuaParser>(
5452 "local function f(x)
5453 if x > 0 then
5454 return 1
5455 elseif x < 0 then
5456 return -1
5457 else
5458 return 0
5459 end
5460end",
5461 "foo.lua",
5462 |metric| {
5463 assert_eq!(metric.loc.sloc(), 9);
5464 assert_eq!(metric.loc.ploc(), 9);
5465 assert_eq!(metric.loc.lloc(), 5);
5466 assert_eq!(metric.loc.cloc(), 0);
5467 assert_eq!(metric.loc.blank(), 0);
5468 insta::assert_json_snapshot!(metric.loc);
5469 },
5470 );
5471 }
5472
5473 #[cfg(feature = "lua")]
5474 #[test]
5475 fn lua_no_else_lloc() {
5476 // else_statement must not add lloc.
5477 check_metrics::<LuaParser>(
5478 "local function f(x)
5479 if x > 0 then
5480 return 1
5481 else
5482 return 0
5483 end
5484end",
5485 "foo.lua",
5486 |metric| {
5487 assert_eq!(metric.loc.sloc(), 7);
5488 assert_eq!(metric.loc.ploc(), 7);
5489 assert_eq!(metric.loc.lloc(), 4);
5490 assert_eq!(metric.loc.cloc(), 0);
5491 assert_eq!(metric.loc.blank(), 0);
5492 insta::assert_json_snapshot!(metric.loc);
5493 },
5494 );
5495 }
5496
5497 #[cfg(feature = "lua")]
5498 #[test]
5499 fn lua_functiondeclaration_lloc() {
5500 // Named function declaration counts as one lloc.
5501 check_metrics::<LuaParser>(
5502 "function f()
5503 return 1
5504end",
5505 "foo.lua",
5506 |metric| {
5507 assert_eq!(metric.loc.sloc(), 3);
5508 assert_eq!(metric.loc.ploc(), 3);
5509 assert_eq!(metric.loc.lloc(), 2);
5510 assert_eq!(metric.loc.cloc(), 0);
5511 assert_eq!(metric.loc.blank(), 0);
5512 insta::assert_json_snapshot!(metric.loc);
5513 },
5514 );
5515 }
5516
5517 #[cfg(feature = "lua")]
5518 #[test]
5519 fn lua_local_function_lloc() {
5520 // local function declaration is also a function_declaration node → one lloc.
5521 check_metrics::<LuaParser>(
5522 "local function g()
5523 return 2
5524end",
5525 "foo.lua",
5526 |metric| {
5527 assert_eq!(metric.loc.sloc(), 3);
5528 assert_eq!(metric.loc.ploc(), 3);
5529 assert_eq!(metric.loc.lloc(), 2);
5530 assert_eq!(metric.loc.cloc(), 0);
5531 assert_eq!(metric.loc.blank(), 0);
5532 insta::assert_json_snapshot!(metric.loc);
5533 },
5534 );
5535 }
5536
5537 #[cfg(feature = "lua")]
5538 #[test]
5539 fn lua_for_numeric_lloc() {
5540 check_metrics::<LuaParser>(
5541 "for i = 1, 10 do
5542 print(i)
5543end",
5544 "foo.lua",
5545 |metric| {
5546 assert_eq!(metric.loc.sloc(), 3);
5547 assert_eq!(metric.loc.ploc(), 3);
5548 assert_eq!(metric.loc.lloc(), 1);
5549 assert_eq!(metric.loc.cloc(), 0);
5550 assert_eq!(metric.loc.blank(), 0);
5551 insta::assert_json_snapshot!(metric.loc);
5552 },
5553 );
5554 }
5555
5556 #[cfg(feature = "lua")]
5557 #[test]
5558 fn lua_for_generic_lloc() {
5559 check_metrics::<LuaParser>(
5560 "for k, v in pairs(t) do
5561 print(k, v)
5562end",
5563 "foo.lua",
5564 |metric| {
5565 assert_eq!(metric.loc.sloc(), 3);
5566 assert_eq!(metric.loc.ploc(), 3);
5567 assert_eq!(metric.loc.lloc(), 1);
5568 assert_eq!(metric.loc.cloc(), 0);
5569 assert_eq!(metric.loc.blank(), 0);
5570 insta::assert_json_snapshot!(metric.loc);
5571 },
5572 );
5573 }
5574
5575 #[cfg(feature = "lua")]
5576 #[test]
5577 fn lua_repeat_lloc() {
5578 check_metrics::<LuaParser>(
5579 "local i = 0
5580repeat
5581 i = i + 1
5582until i >= 10",
5583 "foo.lua",
5584 |metric| {
5585 assert_eq!(metric.loc.sloc(), 4);
5586 assert_eq!(metric.loc.ploc(), 4);
5587 assert_eq!(metric.loc.lloc(), 3);
5588 assert_eq!(metric.loc.cloc(), 0);
5589 assert_eq!(metric.loc.blank(), 0);
5590 insta::assert_json_snapshot!(metric.loc);
5591 },
5592 );
5593 }
5594
5595 #[cfg(feature = "lua")]
5596 #[test]
5597 fn lua_local_decl_lloc() {
5598 check_metrics::<LuaParser>(
5599 "local x = 1
5600local y, z = 2, 3",
5601 "foo.lua",
5602 |metric| {
5603 assert_eq!(metric.loc.sloc(), 2);
5604 assert_eq!(metric.loc.ploc(), 2);
5605 assert_eq!(metric.loc.lloc(), 2);
5606 assert_eq!(metric.loc.cloc(), 0);
5607 assert_eq!(metric.loc.blank(), 0);
5608 insta::assert_json_snapshot!(metric.loc);
5609 },
5610 );
5611 }
5612
5613 #[cfg(feature = "lua")]
5614 #[test]
5615 fn lua_function_call_lloc() {
5616 // Standalone function calls have no expression_statement wrapper in Lua.
5617 // They fall to the `_` branch → counted as ploc, not lloc.
5618 check_metrics::<LuaParser>(
5619 "print(\"hello\")
5620local x = 1",
5621 "foo.lua",
5622 |metric| {
5623 assert_eq!(metric.loc.sloc(), 2);
5624 assert_eq!(metric.loc.ploc(), 2);
5625 assert_eq!(metric.loc.lloc(), 1);
5626 assert_eq!(metric.loc.cloc(), 0);
5627 assert_eq!(metric.loc.blank(), 0);
5628 insta::assert_json_snapshot!(metric.loc);
5629 },
5630 );
5631 }
5632
5633 #[cfg(feature = "lua")]
5634 #[test]
5635 fn lua_toplevel_assignment_lloc() {
5636 // Bare `x = 1` at chunk level: parent is Chunk, not VariableDeclaration,
5637 // so the parent-guard correctly counts it as 1 lloc.
5638 check_metrics::<LuaParser>(
5639 "x = 1
5640y, z = 2, 3",
5641 "foo.lua",
5642 |metric| {
5643 assert_eq!(metric.loc.sloc(), 2);
5644 assert_eq!(metric.loc.ploc(), 2);
5645 assert_eq!(metric.loc.lloc(), 2);
5646 assert_eq!(metric.loc.cloc(), 0);
5647 assert_eq!(metric.loc.blank(), 0);
5648 insta::assert_json_snapshot!(metric.loc);
5649 },
5650 );
5651 }
5652
5653 #[cfg(feature = "typescript")]
5654 #[test]
5655 fn tsx_basic_loc() {
5656 check_metrics::<TsxParser>(
5657 "// A simple utility function
5658 function add(a: number, b: number): number {
5659 /* multi-line
5660 comment */
5661 return a + b;
5662 }
5663
5664 const greet = (name: string) => {
5665 return `Hello, ${name}`;
5666 };",
5667 "foo.tsx",
5668 |metric| {
5669 insta::assert_json_snapshot!(
5670 metric.loc,
5671 @r#"
5672 {
5673 "sloc": 10,
5674 "ploc": 6,
5675 "lloc": 3,
5676 "cloc": 3,
5677 "blank": 1,
5678 "sloc_average": 3.3333333333333335,
5679 "ploc_average": 2.0,
5680 "lloc_average": 1.0,
5681 "cloc_average": 1.0,
5682 "blank_average": 0.3333333333333333,
5683 "sloc_min": 3,
5684 "sloc_max": 10,
5685 "cloc_min": 0,
5686 "cloc_max": 3,
5687 "ploc_min": 3,
5688 "ploc_max": 6,
5689 "lloc_min": 1,
5690 "lloc_max": 3,
5691 "blank_min": 0,
5692 "blank_max": 1
5693 }
5694 "#
5695 );
5696 },
5697 );
5698 }
5699
5700 #[cfg(feature = "typescript")]
5701 #[test]
5702 fn typescript_basic_loc() {
5703 check_metrics::<TypescriptParser>(
5704 "// Line comment
5705 /* Block
5706 comment */
5707 function greet(name: string): string {
5708 return `Hello, ${name}`;
5709 }
5710
5711 const add = (a: number, b: number): number => a + b;",
5712 "foo.ts",
5713 |metric| {
5714 insta::assert_json_snapshot!(
5715 metric.loc,
5716 @r#"
5717 {
5718 "sloc": 8,
5719 "ploc": 4,
5720 "lloc": 2,
5721 "cloc": 3,
5722 "blank": 1,
5723 "sloc_average": 2.6666666666666665,
5724 "ploc_average": 1.3333333333333333,
5725 "lloc_average": 0.6666666666666666,
5726 "cloc_average": 1.0,
5727 "blank_average": 0.3333333333333333,
5728 "sloc_min": 1,
5729 "sloc_max": 8,
5730 "cloc_min": 0,
5731 "cloc_max": 3,
5732 "ploc_min": 1,
5733 "ploc_max": 4,
5734 "lloc_min": 0,
5735 "lloc_max": 2,
5736 "blank_min": 0,
5737 "blank_max": 1
5738 }
5739 "#
5740 );
5741 },
5742 );
5743 }
5744
5745 #[cfg(feature = "csharp")]
5746 #[test]
5747 fn csharp_comments() {
5748 check_metrics::<CsharpParser>(
5749 "for (int i = 0; i < 100; i++) {
5750 // Print hello
5751 System.Console.WriteLine(\"hello\");
5752 /// XML doc comment
5753 System.Console.WriteLine(\"hello\");
5754 }",
5755 "foo.cs",
5756 |metric| {
5757 assert_eq!(metric.loc.sloc(), 6);
5758 assert_eq!(metric.loc.ploc(), 4);
5759 assert_eq!(metric.loc.lloc(), 3);
5760 assert_eq!(metric.loc.cloc(), 2);
5761 assert_eq!(metric.loc.blank(), 0);
5762 insta::assert_json_snapshot!(metric.loc);
5763 },
5764 );
5765 }
5766
5767 #[cfg(feature = "csharp")]
5768 #[test]
5769 fn csharp_blank() {
5770 check_metrics::<CsharpParser>(
5771 "int x = 1;
5772
5773
5774 int y = 2;",
5775 "foo.cs",
5776 |metric| {
5777 assert_eq!(metric.loc.sloc(), 4);
5778 assert_eq!(metric.loc.ploc(), 2);
5779 assert_eq!(metric.loc.lloc(), 2);
5780 assert_eq!(metric.loc.cloc(), 0);
5781 assert_eq!(metric.loc.blank(), 2);
5782 insta::assert_json_snapshot!(metric.loc);
5783 },
5784 );
5785 }
5786
5787 #[cfg(feature = "csharp")]
5788 #[test]
5789 fn csharp_sloc() {
5790 check_metrics::<CsharpParser>(
5791 "for (int i = 0; i < 100; i++) {
5792 System.Console.WriteLine(i);
5793 }",
5794 "foo.cs",
5795 |metric| {
5796 assert_eq!(metric.loc.sloc(), 3);
5797 assert_eq!(metric.loc.ploc(), 3);
5798 assert_eq!(metric.loc.lloc(), 2);
5799 assert_eq!(metric.loc.cloc(), 0);
5800 assert_eq!(metric.loc.blank(), 0);
5801 insta::assert_json_snapshot!(metric.loc);
5802 },
5803 );
5804 }
5805
5806 #[cfg(feature = "csharp")]
5807 #[test]
5808 fn csharp_module_sloc() {
5809 check_metrics::<CsharpParser>(
5810 "namespace HelloWorld {
5811 class Program { }
5812 }",
5813 "foo.cs",
5814 |metric| {
5815 assert_eq!(metric.loc.sloc(), 3);
5816 assert_eq!(metric.loc.ploc(), 3);
5817 assert_eq!(metric.loc.lloc(), 0);
5818 assert_eq!(metric.loc.cloc(), 0);
5819 assert_eq!(metric.loc.blank(), 0);
5820 insta::assert_json_snapshot!(metric.loc);
5821 },
5822 );
5823 }
5824
5825 #[cfg(feature = "csharp")]
5826 #[test]
5827 fn csharp_single_ploc() {
5828 check_metrics::<CsharpParser>("int x = 1;", "foo.cs", |metric| {
5829 assert_eq!(metric.loc.sloc(), 1);
5830 assert_eq!(metric.loc.ploc(), 1);
5831 assert_eq!(metric.loc.lloc(), 1);
5832 assert_eq!(metric.loc.cloc(), 0);
5833 assert_eq!(metric.loc.blank(), 0);
5834 insta::assert_json_snapshot!(metric.loc);
5835 });
5836 }
5837
5838 #[cfg(feature = "csharp")]
5839 #[test]
5840 fn csharp_simple_ploc() {
5841 check_metrics::<CsharpParser>(
5842 "for (int i = 0; i < 100; i++) {
5843 System.Console.WriteLine(i);
5844 }",
5845 "foo.cs",
5846 |metric| {
5847 assert_eq!(metric.loc.sloc(), 3);
5848 assert_eq!(metric.loc.ploc(), 3);
5849 assert_eq!(metric.loc.lloc(), 2);
5850 assert_eq!(metric.loc.cloc(), 0);
5851 assert_eq!(metric.loc.blank(), 0);
5852 insta::assert_json_snapshot!(metric.loc);
5853 },
5854 );
5855 }
5856
5857 #[cfg(feature = "csharp")]
5858 #[test]
5859 fn csharp_multi_ploc() {
5860 check_metrics::<CsharpParser>(
5861 "int x = 1;
5862 for (int i = 0; i < 100; i++) {
5863 System.Console.WriteLine(i);
5864 }",
5865 "foo.cs",
5866 |metric| {
5867 assert_eq!(metric.loc.sloc(), 4);
5868 assert_eq!(metric.loc.ploc(), 4);
5869 assert_eq!(metric.loc.lloc(), 3);
5870 assert_eq!(metric.loc.cloc(), 0);
5871 assert_eq!(metric.loc.blank(), 0);
5872 insta::assert_json_snapshot!(metric.loc);
5873 },
5874 );
5875 }
5876
5877 #[cfg(feature = "csharp")]
5878 #[test]
5879 fn csharp_single_statement_lloc() {
5880 check_metrics::<CsharpParser>("int max = 10;", "foo.cs", |metric| {
5881 assert_eq!(metric.loc.sloc(), 1);
5882 assert_eq!(metric.loc.ploc(), 1);
5883 assert_eq!(metric.loc.lloc(), 1);
5884 assert_eq!(metric.loc.cloc(), 0);
5885 assert_eq!(metric.loc.blank(), 0);
5886 insta::assert_json_snapshot!(metric.loc);
5887 });
5888 }
5889
5890 #[cfg(feature = "csharp")]
5891 #[test]
5892 fn csharp_for_lloc() {
5893 check_metrics::<CsharpParser>(
5894 "for (int i = 0; i < 10; i++) {
5895 System.Console.WriteLine(i);
5896 }",
5897 "foo.cs",
5898 |metric| {
5899 assert_eq!(metric.loc.sloc(), 3);
5900 assert_eq!(metric.loc.ploc(), 3);
5901 assert_eq!(metric.loc.lloc(), 2);
5902 assert_eq!(metric.loc.cloc(), 0);
5903 assert_eq!(metric.loc.blank(), 0);
5904 insta::assert_json_snapshot!(metric.loc);
5905 },
5906 );
5907 }
5908
5909 #[cfg(feature = "csharp")]
5910 #[test]
5911 fn csharp_foreach_lloc() {
5912 check_metrics::<CsharpParser>(
5913 "foreach (var item in items) {
5914 System.Console.WriteLine(item);
5915 }",
5916 "foo.cs",
5917 |metric| {
5918 assert_eq!(metric.loc.sloc(), 3);
5919 assert_eq!(metric.loc.ploc(), 3);
5920 assert_eq!(metric.loc.lloc(), 2);
5921 assert_eq!(metric.loc.cloc(), 0);
5922 assert_eq!(metric.loc.blank(), 0);
5923 insta::assert_json_snapshot!(metric.loc);
5924 },
5925 );
5926 }
5927
5928 #[cfg(feature = "csharp")]
5929 #[test]
5930 fn csharp_while_lloc() {
5931 check_metrics::<CsharpParser>(
5932 "int i = 0;
5933 while (i < 10) {
5934 i++;
5935 }",
5936 "foo.cs",
5937 |metric| {
5938 assert_eq!(metric.loc.sloc(), 4);
5939 assert_eq!(metric.loc.ploc(), 4);
5940 assert_eq!(metric.loc.lloc(), 3);
5941 assert_eq!(metric.loc.cloc(), 0);
5942 assert_eq!(metric.loc.blank(), 0);
5943 insta::assert_json_snapshot!(metric.loc);
5944 },
5945 );
5946 }
5947
5948 #[cfg(feature = "csharp")]
5949 #[test]
5950 fn csharp_do_while_lloc() {
5951 check_metrics::<CsharpParser>(
5952 "int i = 0;
5953 do {
5954 i++;
5955 } while (i < 10);",
5956 "foo.cs",
5957 |metric| {
5958 assert_eq!(metric.loc.sloc(), 4);
5959 assert_eq!(metric.loc.ploc(), 4);
5960 assert_eq!(metric.loc.lloc(), 3);
5961 assert_eq!(metric.loc.cloc(), 0);
5962 assert_eq!(metric.loc.blank(), 0);
5963 insta::assert_json_snapshot!(metric.loc);
5964 },
5965 );
5966 }
5967
5968 #[cfg(feature = "csharp")]
5969 #[test]
5970 fn csharp_switch_lloc() {
5971 check_metrics::<CsharpParser>(
5972 "switch (x) {
5973 case 1: System.Console.WriteLine(1); break;
5974 case 2: System.Console.WriteLine(2); break;
5975 default: System.Console.WriteLine(0); break;
5976 }
5977 string s = x switch { 1 => \"one\", _ => \"other\" };",
5978 "foo.cs",
5979 |metric| {
5980 assert_eq!(metric.loc.sloc(), 6);
5981 assert_eq!(metric.loc.ploc(), 6);
5982 assert_eq!(metric.loc.lloc(), 8);
5983 assert_eq!(metric.loc.cloc(), 0);
5984 assert_eq!(metric.loc.blank(), 0);
5985 insta::assert_json_snapshot!(metric.loc);
5986 },
5987 );
5988 }
5989
5990 #[cfg(feature = "csharp")]
5991 #[test]
5992 fn csharp_continue_lloc() {
5993 check_metrics::<CsharpParser>(
5994 "for (int i = 0; i < 10; i++) {
5995 if (i == 5) continue;
5996 System.Console.WriteLine(i);
5997 }",
5998 "foo.cs",
5999 |metric| {
6000 assert_eq!(metric.loc.sloc(), 4);
6001 assert_eq!(metric.loc.ploc(), 4);
6002 assert_eq!(metric.loc.lloc(), 4);
6003 assert_eq!(metric.loc.cloc(), 0);
6004 assert_eq!(metric.loc.blank(), 0);
6005 insta::assert_json_snapshot!(metric.loc);
6006 },
6007 );
6008 }
6009
6010 #[cfg(feature = "csharp")]
6011 #[test]
6012 fn csharp_try_lloc() {
6013 check_metrics::<CsharpParser>(
6014 "try {
6015 System.Console.WriteLine(\"try\");
6016 } catch (System.Exception e) {
6017 throw new System.Exception(\"caught\");
6018 } finally {
6019 System.Console.WriteLine(\"done\");
6020 }",
6021 "foo.cs",
6022 |metric| {
6023 assert_eq!(metric.loc.sloc(), 7);
6024 assert_eq!(metric.loc.ploc(), 7);
6025 assert_eq!(metric.loc.lloc(), 4);
6026 assert_eq!(metric.loc.cloc(), 0);
6027 assert_eq!(metric.loc.blank(), 0);
6028 insta::assert_json_snapshot!(metric.loc);
6029 },
6030 );
6031 }
6032
6033 #[cfg(feature = "csharp")]
6034 #[test]
6035 fn csharp_class_loc() {
6036 check_metrics::<CsharpParser>(
6037 "class A {
6038 int x;
6039 public void M() {
6040 System.Console.WriteLine(x);
6041 }
6042 }",
6043 "foo.cs",
6044 |metric| {
6045 assert_eq!(metric.loc.sloc(), 6);
6046 assert_eq!(metric.loc.ploc(), 6);
6047 assert_eq!(metric.loc.lloc(), 1);
6048 assert_eq!(metric.loc.cloc(), 0);
6049 assert_eq!(metric.loc.blank(), 0);
6050 insta::assert_json_snapshot!(metric.loc);
6051 },
6052 );
6053 }
6054
6055 #[cfg(feature = "csharp")]
6056 #[test]
6057 fn csharp_expressions_lloc() {
6058 check_metrics::<CsharpParser>(
6059 "int a = 1;
6060 int b = 2;
6061 int c = a + b;
6062 System.Console.WriteLine(c);",
6063 "foo.cs",
6064 |metric| {
6065 assert_eq!(metric.loc.sloc(), 4);
6066 assert_eq!(metric.loc.ploc(), 4);
6067 assert_eq!(metric.loc.lloc(), 4);
6068 assert_eq!(metric.loc.cloc(), 0);
6069 assert_eq!(metric.loc.blank(), 0);
6070 insta::assert_json_snapshot!(metric.loc);
6071 },
6072 );
6073 }
6074
6075 #[cfg(feature = "csharp")]
6076 #[test]
6077 fn csharp_statement_inline_loc() {
6078 check_metrics::<CsharpParser>(
6079 "if (x > 0) System.Console.WriteLine(x);",
6080 "foo.cs",
6081 |metric| {
6082 assert_eq!(metric.loc.sloc(), 1);
6083 assert_eq!(metric.loc.ploc(), 1);
6084 assert_eq!(metric.loc.lloc(), 2);
6085 assert_eq!(metric.loc.cloc(), 0);
6086 assert_eq!(metric.loc.blank(), 0);
6087 insta::assert_json_snapshot!(metric.loc);
6088 },
6089 );
6090 }
6091
6092 #[cfg(feature = "csharp")]
6093 #[test]
6094 fn csharp_general_loc() {
6095 check_metrics::<CsharpParser>(
6096 "using System;
6097 namespace Demo {
6098 class A {
6099 public void M() {
6100 Console.WriteLine(\"hi\");
6101 }
6102 }
6103 class B {
6104 public int N() { return 0; }
6105 }
6106 }",
6107 "foo.cs",
6108 |metric| {
6109 assert_eq!(metric.loc.sloc(), 11);
6110 assert_eq!(metric.loc.ploc(), 11);
6111 assert_eq!(metric.loc.lloc(), 2);
6112 assert_eq!(metric.loc.cloc(), 0);
6113 assert_eq!(metric.loc.blank(), 0);
6114 insta::assert_json_snapshot!(metric.loc);
6115 },
6116 );
6117 }
6118
6119 #[cfg(feature = "csharp")]
6120 #[test]
6121 fn csharp_using_lloc() {
6122 // EC11 — `using_directive` does not bump LLOC; `using_statement`
6123 // (block form) and the C# 8 simple-using local-declaration
6124 // (`using var x = ...;`) both do, the latter via the standard
6125 // `LocalDeclarationStatement` path.
6126 check_metrics::<CsharpParser>(
6127 "using System;
6128 using System.IO;
6129 class A {
6130 public void M() {
6131 using (var s = File.OpenRead(\"x\")) {
6132 Console.WriteLine(s);
6133 }
6134 using var t = File.OpenRead(\"y\");
6135 Console.WriteLine(t);
6136 }
6137 }",
6138 "foo.cs",
6139 |metric| {
6140 assert_eq!(metric.loc.sloc(), 11);
6141 assert_eq!(metric.loc.ploc(), 11);
6142 assert_eq!(metric.loc.lloc(), 4);
6143 assert_eq!(metric.loc.cloc(), 0);
6144 assert_eq!(metric.loc.blank(), 0);
6145 insta::assert_json_snapshot!(metric.loc);
6146 },
6147 );
6148 }
6149
6150 #[cfg(feature = "kotlin")]
6151 #[test]
6152 fn kotlin_loc_basic() {
6153 check_metrics::<KotlinParser>(
6154 "// A simple function
6155 fun greet(name: String): String {
6156 val greeting = \"Hello, \" + name
6157 if (name.isEmpty()) {
6158 return \"Hello, World!\"
6159 }
6160 return greeting
6161 }",
6162 "foo.kt",
6163 |metric| {
6164 insta::assert_json_snapshot!(
6165 metric.loc,
6166 @r#"
6167 {
6168 "sloc": 8,
6169 "ploc": 7,
6170 "lloc": 4,
6171 "cloc": 1,
6172 "blank": 0,
6173 "sloc_average": 4.0,
6174 "ploc_average": 3.5,
6175 "lloc_average": 2.0,
6176 "cloc_average": 0.5,
6177 "blank_average": 0.0,
6178 "sloc_min": 7,
6179 "sloc_max": 8,
6180 "cloc_min": 0,
6181 "cloc_max": 1,
6182 "ploc_min": 7,
6183 "ploc_max": 7,
6184 "lloc_min": 4,
6185 "lloc_max": 4,
6186 "blank_min": 0,
6187 "blank_max": 0
6188 }
6189 "#
6190 );
6191 },
6192 );
6193 }
6194
6195 #[cfg(feature = "kotlin")]
6196 #[test]
6197 fn kotlin_loc_bare_expression() {
6198 check_metrics::<KotlinParser>(
6199 "fun main() {
6200 val x = 42
6201 println(x)
6202 listOf(1, 2, 3).forEach { println(it) }
6203 }",
6204 "foo.kt",
6205 |metric| {
6206 // lloc should count: val x = 42 (PropertyDeclaration, +1)
6207 // + println(x) (CallExpression, parent=Block, +1)
6208 // + listOf(1, 2, 3).forEach { ... } (CallExpression, parent=Block, +1) = 3
6209 insta::assert_json_snapshot!(
6210 metric.loc,
6211 @r#"
6212 {
6213 "sloc": 5,
6214 "ploc": 5,
6215 "lloc": 3,
6216 "cloc": 0,
6217 "blank": 0,
6218 "sloc_average": 2.5,
6219 "ploc_average": 2.5,
6220 "lloc_average": 1.5,
6221 "cloc_average": 0.0,
6222 "blank_average": 0.0,
6223 "sloc_min": 5,
6224 "sloc_max": 5,
6225 "cloc_min": 0,
6226 "cloc_max": 0,
6227 "ploc_min": 5,
6228 "ploc_max": 5,
6229 "lloc_min": 3,
6230 "lloc_max": 3,
6231 "blank_min": 0,
6232 "blank_max": 0
6233 }
6234 "#
6235 );
6236 },
6237 );
6238 }
6239
6240 #[cfg(feature = "bash")]
6241 #[test]
6242 fn bash_loc() {
6243 check_metrics::<BashParser>(
6244 "#!/bin/bash
6245# This is a comment
6246f() {
6247 echo 'hello'
6248}
6249
6250# Another comment
6251f",
6252 "foo.sh",
6253 |metric| {
6254 assert_eq!(metric.loc.sloc(), 8);
6255 assert_eq!(metric.loc.ploc(), 4);
6256 assert_eq!(metric.loc.lloc(), 3);
6257 assert_eq!(metric.loc.cloc(), 3);
6258 assert_eq!(metric.loc.blank(), 1);
6259 insta::assert_json_snapshot!(metric.loc);
6260 },
6261 );
6262 }
6263
6264 // CRLF regression tests: metrics must be identical regardless of line ending style.
6265 // These also serve as canaries for tree-sitter row-counting behaviour with \r bytes.
6266
6267 #[cfg(feature = "python")]
6268 #[test]
6269 fn python_cloc_crlf_matches_lf() {
6270 check_metrics::<PythonParser>("# comment\nx = 1", "foo.py", |m| {
6271 assert_eq!(m.loc.cloc(), 1);
6272 assert_eq!(m.loc.ploc(), 1);
6273 assert_eq!(m.loc.sloc(), 2);
6274 assert_eq!(m.loc.blank(), 0);
6275 });
6276 check_metrics::<PythonParser>("# comment\r\nx = 1", "foo.py", |m| {
6277 assert_eq!(m.loc.cloc(), 1);
6278 assert_eq!(m.loc.ploc(), 1);
6279 assert_eq!(m.loc.sloc(), 2);
6280 assert_eq!(m.loc.blank(), 0);
6281 });
6282 // Lone-CR (old Mac line endings) is the true canary: without CR normalisation,
6283 // tree-sitter 0.26.8 only advances its row counter on \n, collapsing all content
6284 // onto row 0 and producing wrong sloc/cloc metrics.
6285 check_metrics::<PythonParser>("# comment\rx = 1", "foo.py", |m| {
6286 assert_eq!(m.loc.cloc(), 1);
6287 assert_eq!(m.loc.ploc(), 1);
6288 assert_eq!(m.loc.sloc(), 2);
6289 assert_eq!(m.loc.blank(), 0);
6290 });
6291 }
6292
6293 #[cfg(feature = "python")]
6294 #[test]
6295 fn python_blank_crlf_matches_lf() {
6296 check_metrics::<PythonParser>("# comment\n\nx = 1", "foo.py", |m| {
6297 assert_eq!(m.loc.blank(), 1);
6298 });
6299 check_metrics::<PythonParser>("# comment\r\n\r\nx = 1", "foo.py", |m| {
6300 assert_eq!(m.loc.blank(), 1);
6301 });
6302 // Lone-CR: without normalisation the blank \r line stays on row 0 and is not counted.
6303 check_metrics::<PythonParser>("# comment\r\rx = 1", "foo.py", |m| {
6304 assert_eq!(m.loc.blank(), 1);
6305 });
6306 }
6307
6308 #[cfg(feature = "rust")]
6309 #[test]
6310 fn rust_cloc_crlf_matches_lf() {
6311 check_metrics::<RustParser>(
6312 "fn f() {\n // comment\n let x = 1;\n}",
6313 "foo.rs",
6314 |m| {
6315 assert_eq!(m.loc.cloc(), 1);
6316 assert_eq!(m.loc.sloc(), 4);
6317 },
6318 );
6319 check_metrics::<RustParser>(
6320 "fn f() {\r\n // comment\r\n let x = 1;\r\n}",
6321 "foo.rs",
6322 |m| {
6323 assert_eq!(m.loc.cloc(), 1);
6324 assert_eq!(m.loc.sloc(), 4);
6325 },
6326 );
6327 // Lone-CR: without normalisation, tree-sitter 0.26.8 only advances its row counter on
6328 // \n, so all content collapses onto row 0 and sloc becomes 1 instead of 4.
6329 check_metrics::<RustParser>(
6330 "fn f() {\r // comment\r let x = 1;\r}",
6331 "foo.rs",
6332 |m| {
6333 assert_eq!(m.loc.cloc(), 1);
6334 assert_eq!(m.loc.sloc(), 4);
6335 },
6336 );
6337 }
6338
6339 #[cfg(feature = "tcl")]
6340 #[test]
6341 fn tcl_blank() {
6342 check_metrics::<TclParser>("set x 1\n\nset y 2", "foo.tcl", |metric| {
6343 assert_eq!(metric.loc.sloc(), 3);
6344 assert_eq!(metric.loc.ploc(), 2);
6345 assert_eq!(metric.loc.lloc(), 2);
6346 assert_eq!(metric.loc.cloc(), 0);
6347 assert_eq!(metric.loc.blank(), 1);
6348 insta::assert_json_snapshot!(metric.loc);
6349 });
6350 }
6351
6352 #[cfg(feature = "tcl")]
6353 #[test]
6354 fn tcl_no_zero_blank() {
6355 // Blank line interleaved with code that carries trailing comments —
6356 // ensures the `blank = sloc - (ploc ∪ cloc lines)` union math holds
6357 // when code and comment lines coincide.
6358 check_metrics::<TclParser>(
6359 "set a 1\nset b 2\n\nset c 3 ;# trailing\nset d 4 ;# trailing\nset e 5",
6360 "foo.tcl",
6361 |metric| {
6362 assert_eq!(metric.loc.sloc(), 6);
6363 assert_eq!(metric.loc.ploc(), 5);
6364 assert_eq!(metric.loc.cloc(), 2);
6365 assert_eq!(metric.loc.blank(), 1);
6366 },
6367 );
6368 }
6369
6370 /// expected: row 0 is comment-only, row 1 is the sole code row — so
6371 /// `cloc 1 + ploc 1 == sloc 2`. This test pinned `ploc == 2` until
6372 /// #1135: the `LF` token terminating the comment row landed in the
6373 /// `_` catch-all and inserted that row into PLOC, which also drove
6374 /// `cloc + ploc` past `sloc`.
6375 #[cfg(feature = "tcl")]
6376 #[test]
6377 fn tcl_cloc() {
6378 check_metrics::<TclParser>("# This is a comment\nset x 1", "foo.tcl", |metric| {
6379 assert_eq!(metric.loc.sloc(), 2);
6380 assert_eq!(metric.loc.ploc(), 1);
6381 assert_eq!(metric.loc.lloc(), 1);
6382 assert_eq!(metric.loc.cloc(), 1);
6383 assert_eq!(metric.loc.blank(), 0);
6384 insta::assert_json_snapshot!(metric.loc);
6385 });
6386 }
6387
6388 #[cfg(feature = "tcl")]
6389 #[test]
6390 fn tcl_lloc() {
6391 check_metrics::<TclParser>(
6392 "proc f {x} {
6393 while {$x > 0} {
6394 if {$x > 10} {
6395 set x [expr {$x - 1}]
6396 }
6397 }
6398}",
6399 "foo.tcl",
6400 |metric| {
6401 assert_eq!(metric.loc.sloc(), 7);
6402 assert_eq!(metric.loc.ploc(), 7);
6403 assert_eq!(metric.loc.lloc(), 4);
6404 assert_eq!(metric.loc.cloc(), 0);
6405 assert_eq!(metric.loc.blank(), 0);
6406 insta::assert_json_snapshot!(metric.loc);
6407 },
6408 );
6409 }
6410
6411 #[cfg(feature = "tcl")]
6412 #[test]
6413 fn tcl_no_command_substitution_lloc() {
6414 // `string toupper` inside [...] is a sub-expression; only `puts` is top-level.
6415 check_metrics::<TclParser>("puts [string toupper x]", "foo.tcl", |metric| {
6416 assert_eq!(metric.loc.sloc(), 1);
6417 assert_eq!(metric.loc.ploc(), 1);
6418 assert_eq!(metric.loc.lloc(), 1);
6419 assert_eq!(metric.loc.cloc(), 0);
6420 assert_eq!(metric.loc.blank(), 0);
6421 insta::assert_json_snapshot!(metric.loc);
6422 });
6423 }
6424
6425 #[cfg(feature = "tcl")]
6426 #[test]
6427 fn tcl_procedure_lloc() {
6428 check_metrics::<TclParser>("proc foo {} {\n puts hello\n}", "foo.tcl", |metric| {
6429 assert_eq!(metric.loc.sloc(), 3);
6430 assert_eq!(metric.loc.ploc(), 3);
6431 assert_eq!(metric.loc.lloc(), 2);
6432 assert_eq!(metric.loc.cloc(), 0);
6433 assert_eq!(metric.loc.blank(), 0);
6434 insta::assert_json_snapshot!(metric.loc);
6435 });
6436 }
6437
6438 #[cfg(feature = "tcl")]
6439 #[test]
6440 fn tcl_if_lloc() {
6441 check_metrics::<TclParser>("if {1} {\n puts hello\n}", "foo.tcl", |metric| {
6442 assert_eq!(metric.loc.sloc(), 3);
6443 assert_eq!(metric.loc.ploc(), 3);
6444 assert_eq!(metric.loc.lloc(), 2);
6445 assert_eq!(metric.loc.cloc(), 0);
6446 assert_eq!(metric.loc.blank(), 0);
6447 insta::assert_json_snapshot!(metric.loc);
6448 });
6449 }
6450
6451 #[cfg(feature = "tcl")]
6452 #[test]
6453 fn tcl_elseif_lloc() {
6454 // if=1 lloc, elseif=1 lloc, else adds 0 lloc
6455 check_metrics::<TclParser>(
6456 "if {$x > 10} {
6457 puts big
6458} elseif {$x > 5} {
6459 puts medium
6460} else {
6461 puts small
6462}",
6463 "foo.tcl",
6464 |metric| {
6465 assert_eq!(metric.loc.sloc(), 7);
6466 assert_eq!(metric.loc.ploc(), 7);
6467 assert_eq!(metric.loc.lloc(), 5);
6468 assert_eq!(metric.loc.cloc(), 0);
6469 assert_eq!(metric.loc.blank(), 0);
6470 insta::assert_json_snapshot!(metric.loc);
6471 },
6472 );
6473 }
6474
6475 #[cfg(feature = "tcl")]
6476 #[test]
6477 fn tcl_while_lloc() {
6478 check_metrics::<TclParser>(
6479 "while {$x > 0} {\n set x [expr {$x - 1}]\n}",
6480 "foo.tcl",
6481 |metric| {
6482 assert_eq!(metric.loc.sloc(), 3);
6483 assert_eq!(metric.loc.ploc(), 3);
6484 assert_eq!(metric.loc.lloc(), 2);
6485 assert_eq!(metric.loc.cloc(), 0);
6486 assert_eq!(metric.loc.blank(), 0);
6487 insta::assert_json_snapshot!(metric.loc);
6488 },
6489 );
6490 }
6491
6492 #[cfg(feature = "tcl")]
6493 #[test]
6494 fn tcl_foreach_lloc() {
6495 check_metrics::<TclParser>(
6496 "foreach item {a b c} {\n puts $item\n}",
6497 "foo.tcl",
6498 |metric| {
6499 assert_eq!(metric.loc.sloc(), 3);
6500 assert_eq!(metric.loc.ploc(), 3);
6501 assert_eq!(metric.loc.lloc(), 2);
6502 assert_eq!(metric.loc.cloc(), 0);
6503 assert_eq!(metric.loc.blank(), 0);
6504 insta::assert_json_snapshot!(metric.loc);
6505 },
6506 );
6507 }
6508
6509 #[cfg(feature = "tcl")]
6510 #[test]
6511 fn tcl_set_lloc() {
6512 check_metrics::<TclParser>("set x 42", "foo.tcl", |metric| {
6513 assert_eq!(metric.loc.sloc(), 1);
6514 assert_eq!(metric.loc.ploc(), 1);
6515 assert_eq!(metric.loc.lloc(), 1);
6516 assert_eq!(metric.loc.cloc(), 0);
6517 assert_eq!(metric.loc.blank(), 0);
6518 insta::assert_json_snapshot!(metric.loc);
6519 });
6520 }
6521
6522 #[cfg(feature = "tcl")]
6523 #[test]
6524 fn tcl_global_lloc() {
6525 check_metrics::<TclParser>("global x", "foo.tcl", |metric| {
6526 assert_eq!(metric.loc.sloc(), 1);
6527 assert_eq!(metric.loc.ploc(), 1);
6528 assert_eq!(metric.loc.lloc(), 1);
6529 assert_eq!(metric.loc.cloc(), 0);
6530 assert_eq!(metric.loc.blank(), 0);
6531 insta::assert_json_snapshot!(metric.loc);
6532 });
6533 }
6534
6535 #[cfg(feature = "tcl")]
6536 #[test]
6537 fn tcl_try_catch_lloc() {
6538 // try=1 lloc; catch command=1 lloc; commands inside bodies count separately
6539 check_metrics::<TclParser>(
6540 "catch {
6541 set x 1
6542} result
6543try {
6544 set y 2
6545} on error {msg} {
6546 puts $msg
6547}",
6548 "foo.tcl",
6549 |metric| {
6550 assert_eq!(metric.loc.sloc(), 8);
6551 assert_eq!(metric.loc.ploc(), 8);
6552 assert_eq!(metric.loc.lloc(), 5);
6553 assert_eq!(metric.loc.cloc(), 0);
6554 assert_eq!(metric.loc.blank(), 0);
6555 insta::assert_json_snapshot!(metric.loc);
6556 },
6557 );
6558 }
6559
6560 #[cfg(feature = "tcl")]
6561 #[test]
6562 fn tcl_namespace_lloc() {
6563 check_metrics::<TclParser>(
6564 "namespace eval myns {\n set x 1\n}",
6565 "foo.tcl",
6566 |metric| {
6567 assert_eq!(metric.loc.sloc(), 3);
6568 assert_eq!(metric.loc.ploc(), 3);
6569 assert_eq!(metric.loc.lloc(), 2);
6570 assert_eq!(metric.loc.cloc(), 0);
6571 assert_eq!(metric.loc.blank(), 0);
6572 insta::assert_json_snapshot!(metric.loc);
6573 },
6574 );
6575 }
6576
6577 #[cfg(feature = "tcl")]
6578 #[test]
6579 fn tcl_regexp_lloc() {
6580 check_metrics::<TclParser>("regexp {^[0-9]+$} $x", "foo.tcl", |metric| {
6581 assert_eq!(metric.loc.sloc(), 1);
6582 assert_eq!(metric.loc.ploc(), 1);
6583 assert_eq!(metric.loc.lloc(), 1);
6584 assert_eq!(metric.loc.cloc(), 0);
6585 assert_eq!(metric.loc.blank(), 0);
6586 insta::assert_json_snapshot!(metric.loc);
6587 });
6588 }
6589
6590 #[cfg(feature = "tcl")]
6591 #[test]
6592 fn tcl_expr_cmd_lloc() {
6593 check_metrics::<TclParser>("expr {1 + 2}", "foo.tcl", |metric| {
6594 assert_eq!(metric.loc.sloc(), 1);
6595 assert_eq!(metric.loc.ploc(), 1);
6596 assert_eq!(metric.loc.lloc(), 1);
6597 assert_eq!(metric.loc.cloc(), 0);
6598 assert_eq!(metric.loc.blank(), 0);
6599 insta::assert_json_snapshot!(metric.loc);
6600 });
6601 }
6602
6603 #[cfg(feature = "tcl")]
6604 #[test]
6605 fn tcl_no_expr_cmd_substitution_lloc() {
6606 // `expr` inside [...] is a sub-expression, not a statement; only `set` counts.
6607 check_metrics::<TclParser>("set x [expr {1 + 2}]", "foo.tcl", |metric| {
6608 assert_eq!(metric.loc.sloc(), 1);
6609 assert_eq!(metric.loc.ploc(), 1);
6610 assert_eq!(metric.loc.lloc(), 1);
6611 assert_eq!(metric.loc.cloc(), 0);
6612 assert_eq!(metric.loc.blank(), 0);
6613 insta::assert_json_snapshot!(metric.loc);
6614 });
6615 }
6616
6617 #[cfg(feature = "tcl")]
6618 #[test]
6619 fn tcl_nested_commands_lloc() {
6620 // Commands inside proc body are recursively parsed; verify each counts.
6621 check_metrics::<TclParser>(
6622 "proc f {x} {
6623 set y [expr {$x * 2}]
6624 puts $y
6625}",
6626 "foo.tcl",
6627 |metric| {
6628 assert_eq!(metric.loc.sloc(), 4);
6629 assert_eq!(metric.loc.ploc(), 4);
6630 assert_eq!(metric.loc.lloc(), 3);
6631 assert_eq!(metric.loc.cloc(), 0);
6632 assert_eq!(metric.loc.blank(), 0);
6633 insta::assert_json_snapshot!(metric.loc);
6634 },
6635 );
6636 }
6637
6638 #[cfg(feature = "tcl")]
6639 #[test]
6640 fn tcl_command_lloc() {
6641 check_metrics::<TclParser>("puts hello", "foo.tcl", |metric| {
6642 assert_eq!(metric.loc.sloc(), 1);
6643 assert_eq!(metric.loc.ploc(), 1);
6644 assert_eq!(metric.loc.lloc(), 1);
6645 assert_eq!(metric.loc.cloc(), 0);
6646 assert_eq!(metric.loc.blank(), 0);
6647 insta::assert_json_snapshot!(metric.loc);
6648 });
6649 }
6650
6651 #[cfg(feature = "tcl")]
6652 #[test]
6653 fn tcl_no_else_lloc() {
6654 // `else` block does not add a logical line.
6655 check_metrics::<TclParser>(
6656 "if {1} {\n puts yes\n} else {\n puts no\n}",
6657 "foo.tcl",
6658 |metric| {
6659 assert_eq!(metric.loc.sloc(), 5);
6660 assert_eq!(metric.loc.ploc(), 5);
6661 assert_eq!(metric.loc.lloc(), 3);
6662 assert_eq!(metric.loc.cloc(), 0);
6663 assert_eq!(metric.loc.blank(), 0);
6664 insta::assert_json_snapshot!(metric.loc);
6665 },
6666 );
6667 }
6668
6669 #[cfg(feature = "tcl")]
6670 #[test]
6671 fn tcl_no_finally_lloc() {
6672 // `finally` block, like `else`, does not add a logical line.
6673 // proc(1) + try(1) + puts_hi(1) + puts_done(1) + finally(0) = 4.
6674 check_metrics::<TclParser>(
6675 "proc f {} {\n try {\n puts hi\n } finally {\n puts done\n }\n}",
6676 "foo.tcl",
6677 |metric| {
6678 assert_eq!(
6679 metric.loc.lloc(),
6680 4,
6681 "finally adds 0 lloc; would be 5 if finally counted"
6682 );
6683 },
6684 );
6685 }
6686
6687 #[cfg(feature = "tcl")]
6688 #[test]
6689 fn tcl_multiline_block() {
6690 check_metrics::<TclParser>(
6691 "proc f {x} {
6692 set a 1
6693
6694 set b 2
6695 return [expr {$a + $b}]
6696}",
6697 "foo.tcl",
6698 |metric| {
6699 assert_eq!(metric.loc.sloc(), 6);
6700 assert_eq!(metric.loc.ploc(), 5);
6701 assert_eq!(metric.loc.lloc(), 4);
6702 assert_eq!(metric.loc.cloc(), 0);
6703 assert_eq!(metric.loc.blank(), 1);
6704 insta::assert_json_snapshot!(metric.loc);
6705 },
6706 );
6707 }
6708
6709 #[cfg(feature = "tcl")]
6710 #[test]
6711 fn tcl_no_string_lloc() {
6712 // Multi-line double-quoted strings must not inflate lloc — only the
6713 // surrounding command should count. Mirrors lua_no_string_lloc and
6714 // elixir_no_string_content_lloc; pins the heredoc-shaped invariant
6715 // for Tcl quoted_word bodies.
6716 //
6717 // expected: three physical rows (`set s "line one`, `line two`,
6718 // `line three"`), all code — the interior row of a multi-line
6719 // string is PLOC, not blank (#1260, following #778 / #415). This
6720 // test pinned `ploc 2 / blank 1` until #1260; the row `line two`
6721 // held no node at all under the old arm list, so it reached
6722 // neither PLOC nor CLOC and `blank = sloc - ploc - cloc` claimed
6723 // it. `lloc` is unchanged: only the `set` command counts.
6724 check_metrics::<TclParser>(
6725 "set s \"line one\nline two\nline three\"",
6726 "foo.tcl",
6727 |metric| {
6728 assert_eq!(metric.loc.sloc(), 3);
6729 assert_eq!(metric.loc.ploc(), 3);
6730 assert_eq!(metric.loc.lloc(), 1);
6731 assert_eq!(metric.loc.cloc(), 0);
6732 assert_eq!(metric.loc.blank(), 0);
6733 insta::assert_json_snapshot!(metric.loc);
6734 },
6735 );
6736 }
6737
6738 #[cfg(feature = "tcl")]
6739 #[test]
6740 fn tcl_multiline_quoted_word_credits_every_row_to_ploc() {
6741 // Regression test for #1260. A Tcl `quoted_word` carries no child
6742 // per row — only the two `"` tokens — so its interior rows reached
6743 // neither PLOC nor CLOC and `blank = sloc - ploc - cloc` claimed
6744 // them.
6745
6746 // expected: 4 rows — `set msg "line one`, `line two`,
6747 // `line three"`, `puts $msg` — all code, none blank. lloc 2 for the
6748 // `set` and the `puts` command.
6749 check_metrics::<TclParser>(
6750 "set msg \"line one\nline two\nline three\"\nputs $msg",
6751 "foo.tcl",
6752 |metric| {
6753 assert_eq!(metric.loc.sloc(), 4);
6754 assert_eq!(metric.loc.ploc(), 4);
6755 assert_eq!(metric.loc.lloc(), 2);
6756 assert_eq!(metric.loc.cloc(), 0);
6757 assert_eq!(metric.loc.blank(), 0);
6758 },
6759 );
6760
6761 // expected: 3 rows whose middle row is empty *inside* the literal —
6762 // `set msg "one`, ``, `three"`. Code, not blank, matching every
6763 // language that already routes a multi-line literal.
6764 check_metrics::<TclParser>("set msg \"one\n\nthree\"", "foo.tcl", |metric| {
6765 assert_eq!(metric.loc.sloc(), 3);
6766 assert_eq!(metric.loc.ploc(), 3);
6767 assert_eq!(metric.loc.cloc(), 0);
6768 assert_eq!(metric.loc.blank(), 0);
6769 });
6770 }
6771
6772 #[cfg(feature = "tcl")]
6773 #[test]
6774 fn tcl_braced_word_body_blank_rows_stay_blank() {
6775 // The #1260 arm deliberately stops at `quoted_word`. Tcl spells a
6776 // script body and a braced literal with one kind, `braced_word`,
6777 // and the grammar parses both as scripts — `puts {a\n\nb}` yields
6778 // `command` children exactly as a `proc` body does, which is why
6779 // #1318 had to tell the two roles apart out-of-band. Routing it
6780 // would turn every blank line inside every procedure into code.
6781 //
6782 // expected: 5 rows — `proc p {} {`, ` set x 1`, ``,
6783 // ` set y 2`, `}` — with row 3 genuinely blank, so sloc 5,
6784 // ploc 4, blank 1. lloc 3 = the procedure plus two `set` commands.
6785 check_metrics::<TclParser>(
6786 "proc p {} {\n set x 1\n\n set y 2\n}",
6787 "foo.tcl",
6788 |metric| {
6789 assert_eq!(metric.loc.sloc(), 5);
6790 assert_eq!(metric.loc.ploc(), 4);
6791 assert_eq!(metric.loc.lloc(), 3);
6792 assert_eq!(metric.loc.cloc(), 0);
6793 assert_eq!(metric.loc.blank(), 1);
6794 },
6795 );
6796 }
6797
6798 #[cfg(feature = "javascript")]
6799 #[test]
6800 fn javascript_blank() {
6801 check_metrics::<JavascriptParser>(
6802 "// header comment
6803 function f() {
6804
6805 var x = 1;
6806
6807 var y = 2;
6808 }",
6809 "foo.js",
6810 |metric| {
6811 assert_eq!(metric.loc.sloc(), 7);
6812 assert_eq!(metric.loc.ploc(), 4);
6813 assert_eq!(metric.loc.lloc(), 2);
6814 assert_eq!(metric.loc.cloc(), 1);
6815 assert_eq!(metric.loc.blank(), 2);
6816 insta::assert_json_snapshot!(metric.loc);
6817 },
6818 );
6819 }
6820
6821 #[cfg(feature = "javascript")]
6822 #[test]
6823 fn javascript_cloc() {
6824 check_metrics::<JavascriptParser>(
6825 "// line comment
6826 /* block
6827 comment */
6828 function f() {
6829 return 1; // inline
6830 }",
6831 "foo.js",
6832 |metric| {
6833 assert_eq!(metric.loc.sloc(), 6);
6834 assert_eq!(metric.loc.ploc(), 3);
6835 assert_eq!(metric.loc.lloc(), 1);
6836 assert_eq!(metric.loc.cloc(), 4);
6837 assert_eq!(metric.loc.blank(), 0);
6838 insta::assert_json_snapshot!(metric.loc);
6839 },
6840 );
6841 }
6842
6843 #[cfg(feature = "javascript")]
6844 #[test]
6845 fn javascript_cloc_html_comment() {
6846 // The Annex-B `<!-- -->` `html_comment` must count as CLOC, not
6847 // fall to the `_` arm and inflate PLOC (#697). Pre-fix this
6848 // fixture measured cloc 0 / ploc 4.
6849 check_metrics::<JavascriptParser>(
6850 "<!-- header comment -->
6851function f() {
6852 return 1;
6853}",
6854 "foo.js",
6855 |metric| {
6856 assert_eq!(metric.loc.sloc(), 4);
6857 assert_eq!(metric.loc.ploc(), 3);
6858 assert_eq!(metric.loc.lloc(), 1);
6859 assert_eq!(metric.loc.cloc(), 1);
6860 assert_eq!(metric.loc.blank(), 0);
6861 },
6862 );
6863 }
6864
6865 #[cfg(feature = "mozjs")]
6866 #[test]
6867 fn mozjs_blank() {
6868 check_metrics::<MozjsParser>(
6869 "function f() {
6870
6871 var x = 1;
6872
6873 }",
6874 "foo.js",
6875 |metric| {
6876 assert_eq!(metric.loc.sloc(), 5);
6877 assert_eq!(metric.loc.ploc(), 3);
6878 assert_eq!(metric.loc.lloc(), 1);
6879 assert_eq!(metric.loc.cloc(), 0);
6880 assert_eq!(metric.loc.blank(), 2);
6881 insta::assert_json_snapshot!(metric.loc);
6882 },
6883 );
6884 }
6885
6886 #[cfg(feature = "mozjs")]
6887 #[test]
6888 fn mozjs_cloc() {
6889 check_metrics::<MozjsParser>(
6890 "// header
6891 /* block comment */
6892 function f() {
6893 return 42;
6894 }",
6895 "foo.js",
6896 |metric| {
6897 assert_eq!(metric.loc.sloc(), 5);
6898 assert_eq!(metric.loc.ploc(), 3);
6899 assert_eq!(metric.loc.lloc(), 1);
6900 assert_eq!(metric.loc.cloc(), 2);
6901 assert_eq!(metric.loc.blank(), 0);
6902 insta::assert_json_snapshot!(metric.loc);
6903 },
6904 );
6905 }
6906
6907 #[cfg(feature = "mozjs")]
6908 #[test]
6909 fn mozjs_no_zero_blank() {
6910 // Blank line interleaved with code that carries trailing comments —
6911 // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
6912 check_metrics::<MozjsParser>(
6913 "function f() {
6914 var a = 1;
6915
6916 var b = 2; // trailing
6917 var c = 3; // trailing
6918}",
6919 "foo.js",
6920 |metric| {
6921 assert_eq!(metric.loc.sloc(), 6);
6922 assert_eq!(metric.loc.ploc(), 5);
6923 assert_eq!(metric.loc.cloc(), 2);
6924 assert_eq!(metric.loc.blank(), 1);
6925 insta::assert_json_snapshot!(metric.loc);
6926 },
6927 );
6928 }
6929
6930 #[cfg(feature = "mozjs")]
6931 #[test]
6932 fn mozjs_arrow_function_loc() {
6933 check_metrics::<MozjsParser>(
6934 "const add = (a, b) => a + b;
6935 const greet = name => {
6936 return 'Hello ' + name;
6937 };",
6938 "foo.js",
6939 |metric| {
6940 assert_eq!(metric.loc.sloc(), 4);
6941 assert_eq!(metric.loc.ploc(), 4);
6942 assert_eq!(metric.loc.lloc(), 3);
6943 assert_eq!(metric.loc.cloc(), 0);
6944 assert_eq!(metric.loc.blank(), 0);
6945 insta::assert_json_snapshot!(metric.loc);
6946 },
6947 );
6948 }
6949
6950 #[cfg(feature = "mozjs")]
6951 #[test]
6952 fn mozjs_multiple_functions_loc() {
6953 check_metrics::<MozjsParser>(
6954 "function f() {
6955 return 1;
6956 }
6957 function g() {
6958 return 2;
6959 }",
6960 "foo.js",
6961 |metric| {
6962 assert_eq!(metric.loc.sloc(), 6);
6963 assert_eq!(metric.loc.ploc(), 6);
6964 assert_eq!(metric.loc.lloc(), 2);
6965 assert_eq!(metric.loc.cloc(), 0);
6966 assert_eq!(metric.loc.blank(), 0);
6967 insta::assert_json_snapshot!(metric.loc);
6968 },
6969 );
6970 }
6971
6972 #[cfg(feature = "mozjs")]
6973 #[test]
6974 fn mozjs_nested_function_loc() {
6975 check_metrics::<MozjsParser>(
6976 "function outer() {
6977 function inner() {
6978 return 1;
6979 }
6980 return inner();
6981 }",
6982 "foo.js",
6983 |metric| {
6984 assert_eq!(metric.loc.sloc(), 6);
6985 assert_eq!(metric.loc.ploc(), 6);
6986 assert_eq!(metric.loc.lloc(), 2);
6987 assert_eq!(metric.loc.cloc(), 0);
6988 assert_eq!(metric.loc.blank(), 0);
6989 insta::assert_json_snapshot!(metric.loc);
6990 },
6991 );
6992 }
6993
6994 #[cfg(feature = "mozjs")]
6995 #[test]
6996 fn mozjs_if_lloc() {
6997 check_metrics::<MozjsParser>(
6998 "function f(x) {
6999 if (x > 0) {
7000 return 1;
7001 } else {
7002 return -1;
7003 }
7004 }",
7005 "foo.js",
7006 |metric| {
7007 assert_eq!(metric.loc.sloc(), 7);
7008 assert_eq!(metric.loc.ploc(), 7);
7009 // lloc = 3: the `if` statement plus the two `return`
7010 // statements. The three `{ … }` brace blocks (function
7011 // body, `if` consequent, `else` alternative) are syntactic
7012 // groupings, not logical statements, and contribute 0 —
7013 // matching C/Rust/Java for the equivalent code (#777).
7014 // Pre-#777 this asserted 6 (every StatementBlock counted).
7015 assert_eq!(metric.loc.lloc(), 3);
7016 assert_eq!(metric.loc.cloc(), 0);
7017 assert_eq!(metric.loc.blank(), 0);
7018 insta::assert_json_snapshot!(metric.loc);
7019 },
7020 );
7021 }
7022
7023 // Cross-language parity (#777): the same if/else function body yields
7024 // identical lloc across the JS family and the C-family / Rust baselines.
7025 // Removing `StatementBlock` from the JS-family lloc arms restored this
7026 // invariant — every brace block now contributes 0 lloc, as it always
7027 // had elsewhere. Pre-#777 the JS variants reported lloc 6 (three brace
7028 // blocks over-counted) against C's and Rust's 3.
7029 #[cfg(all(
7030 feature = "cpp",
7031 feature = "javascript",
7032 feature = "mozjs",
7033 feature = "rust",
7034 feature = "typescript",
7035 ))]
7036 #[test]
7037 fn js_family_if_lloc_matches_c_and_rust() {
7038 #[cfg(any(
7039 feature = "cpp",
7040 feature = "javascript",
7041 feature = "mozjs",
7042 feature = "rust",
7043 feature = "typescript",
7044 ))]
7045 const JS_SRC: &str = "function f(x) {
7046 if (x > 0) {
7047 return 1;
7048 } else {
7049 return -1;
7050 }
7051 }";
7052 #[cfg(any(
7053 feature = "cpp",
7054 feature = "javascript",
7055 feature = "mozjs",
7056 feature = "rust",
7057 feature = "typescript",
7058 ))]
7059 const C_SRC: &str = "int f(int x) {
7060 if (x > 0) {
7061 return 1;
7062 } else {
7063 return -1;
7064 }
7065 }";
7066 #[cfg(any(
7067 feature = "cpp",
7068 feature = "javascript",
7069 feature = "mozjs",
7070 feature = "rust",
7071 feature = "typescript",
7072 ))]
7073 const RUST_SRC: &str = "fn f(x: i32) -> i32 {
7074 if x > 0 {
7075 return 1;
7076 } else {
7077 return -1;
7078 }
7079 }";
7080
7081 // The logical-statement count is grammar-independent: one `if`
7082 // plus two `return`s, regardless of brace style or language.
7083 #[cfg(any(
7084 feature = "cpp",
7085 feature = "javascript",
7086 feature = "mozjs",
7087 feature = "rust",
7088 feature = "typescript",
7089 ))]
7090 const EXPECTED_LLOC: usize = 3;
7091
7092 check_metrics::<CppParser>(C_SRC, "f.c", |m| {
7093 assert_eq!(m.loc.lloc() as usize, EXPECTED_LLOC);
7094 });
7095 check_metrics::<RustParser>(RUST_SRC, "f.rs", |m| {
7096 assert_eq!(m.loc.lloc() as usize, EXPECTED_LLOC);
7097 });
7098 check_metrics::<MozjsParser>(JS_SRC, "f.js", |m| {
7099 assert_eq!(m.loc.lloc() as usize, EXPECTED_LLOC);
7100 });
7101 check_metrics::<JavascriptParser>(JS_SRC, "f.js", |m| {
7102 assert_eq!(m.loc.lloc() as usize, EXPECTED_LLOC);
7103 });
7104 check_metrics::<TypescriptParser>(JS_SRC, "f.ts", |m| {
7105 assert_eq!(m.loc.lloc() as usize, EXPECTED_LLOC);
7106 });
7107 check_metrics::<TsxParser>(JS_SRC, "f.tsx", |m| {
7108 assert_eq!(m.loc.lloc() as usize, EXPECTED_LLOC);
7109 });
7110 }
7111
7112 #[cfg(feature = "mozjs")]
7113 #[test]
7114 fn mozjs_for_lloc() {
7115 check_metrics::<MozjsParser>(
7116 "function f(n) {
7117 var s = 0;
7118 for (var i = 0; i < n; i++) {
7119 s += i;
7120 }
7121 return s;
7122 }",
7123 "foo.js",
7124 |metric| {
7125 assert_eq!(metric.loc.sloc(), 7);
7126 assert_eq!(metric.loc.ploc(), 7);
7127 assert_eq!(metric.loc.lloc(), 4);
7128 assert_eq!(metric.loc.cloc(), 0);
7129 assert_eq!(metric.loc.blank(), 0);
7130 insta::assert_json_snapshot!(metric.loc);
7131 },
7132 );
7133 }
7134
7135 #[cfg(feature = "bash")]
7136 #[test]
7137 fn bash_blank() {
7138 check_metrics::<BashParser>(
7139 "#!/bin/bash
7140
7141 f() {
7142
7143 echo hello
7144
7145 }",
7146 "foo.sh",
7147 |metric| {
7148 assert_eq!(metric.loc.sloc(), 7);
7149 assert_eq!(metric.loc.ploc(), 3);
7150 assert_eq!(metric.loc.lloc(), 2);
7151 assert_eq!(metric.loc.cloc(), 1);
7152 assert_eq!(metric.loc.blank(), 3);
7153 insta::assert_json_snapshot!(metric.loc);
7154 },
7155 );
7156 }
7157
7158 #[cfg(feature = "bash")]
7159 #[test]
7160 fn bash_cloc() {
7161 check_metrics::<BashParser>(
7162 "# header comment
7163 f() {
7164 # body comment
7165 echo hello
7166 }",
7167 "foo.sh",
7168 |metric| {
7169 assert_eq!(metric.loc.sloc(), 5);
7170 assert_eq!(metric.loc.ploc(), 3);
7171 assert_eq!(metric.loc.lloc(), 2);
7172 assert_eq!(metric.loc.cloc(), 2);
7173 assert_eq!(metric.loc.blank(), 0);
7174 insta::assert_json_snapshot!(metric.loc);
7175 },
7176 );
7177 }
7178
7179 #[cfg(feature = "bash")]
7180 #[test]
7181 fn bash_no_zero_blank() {
7182 // Blank line interleaved with code that carries trailing comments —
7183 // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
7184 check_metrics::<BashParser>(
7185 "f() {
7186 echo a
7187
7188 echo b # trailing
7189 echo c # trailing
7190}",
7191 "foo.sh",
7192 |metric| {
7193 assert_eq!(metric.loc.sloc(), 6);
7194 assert_eq!(metric.loc.ploc(), 5);
7195 assert_eq!(metric.loc.cloc(), 2);
7196 assert_eq!(metric.loc.blank(), 1);
7197 insta::assert_json_snapshot!(metric.loc);
7198 },
7199 );
7200 }
7201
7202 #[cfg(feature = "bash")]
7203 #[test]
7204 fn bash_comment_before_code_line_reclassified() {
7205 // Regression for #547: a standalone `#` comment sitting on a line
7206 // that the grammar *also* anchors a zero-width code leaf to (the
7207 // empty `word` tree-sitter-bash emits inside a `$(...)` command
7208 // substitution that contains only a comment) must reclassify that
7209 // row from comment-only to code-comment. The Bash `Loc` leaf arm
7210 // previously omitted `check_comment_ends_on_code_line` (unlike
7211 // Elixir and every other impl), so the row was credited to BOTH
7212 // `ploc` and the comment-only set and `blank` was undercounted by
7213 // one.
7214 //
7215 // Source rows: 0 `echo a`, 1 blank, 2 `echo "$(`, 3 ` # c`,
7216 // 4 `)"`. expected: sloc=5 (every physical row),
7217 // ploc=4 (rows 0/2/3/4 — row 3 carries the phantom code leaf),
7218 // lloc=3, cloc=1 (row 3, now a code-comment line, not comment-only),
7219 // blank=1 (row 1). Without the fix `blank` collapses to 0 because
7220 // row 3 is double-counted. Verified fail-on-revert per
7221 // .claude/rules/testing.md.
7222 check_metrics::<BashParser>("echo a\n\necho \"$(\n # c\n)\"\n", "foo.sh", |metric| {
7223 assert_eq!(metric.loc.sloc(), 5);
7224 assert_eq!(metric.loc.ploc(), 4);
7225 assert_eq!(metric.loc.lloc(), 3);
7226 assert_eq!(metric.loc.cloc(), 1);
7227 assert_eq!(metric.loc.blank(), 1);
7228 insta::assert_json_snapshot!(
7229 metric.loc,
7230 @r#"
7231 {
7232 "sloc": 5,
7233 "ploc": 4,
7234 "lloc": 3,
7235 "cloc": 1,
7236 "blank": 1,
7237 "sloc_average": 5.0,
7238 "ploc_average": 4.0,
7239 "lloc_average": 3.0,
7240 "cloc_average": 1.0,
7241 "blank_average": 1.0,
7242 "sloc_min": 5,
7243 "sloc_max": 5,
7244 "cloc_min": 1,
7245 "cloc_max": 1,
7246 "ploc_min": 4,
7247 "ploc_max": 4,
7248 "lloc_min": 3,
7249 "lloc_max": 3,
7250 "blank_min": 1,
7251 "blank_max": 1
7252 }
7253 "#
7254 );
7255 });
7256 }
7257
7258 #[cfg(feature = "bash")]
7259 #[test]
7260 fn bash_if_lloc() {
7261 check_metrics::<BashParser>(
7262 "f() {
7263 if [ $1 -gt 0 ]; then
7264 echo positive
7265 else
7266 echo negative
7267 fi
7268 }",
7269 "foo.sh",
7270 |metric| {
7271 assert_eq!(metric.loc.sloc(), 7);
7272 assert_eq!(metric.loc.ploc(), 7);
7273 assert_eq!(metric.loc.lloc(), 4);
7274 assert_eq!(metric.loc.cloc(), 0);
7275 assert_eq!(metric.loc.blank(), 0);
7276 insta::assert_json_snapshot!(metric.loc);
7277 },
7278 );
7279 }
7280
7281 #[cfg(feature = "bash")]
7282 #[test]
7283 fn bash_for_lloc() {
7284 check_metrics::<BashParser>(
7285 "f() {
7286 for i in 1 2 3; do
7287 echo $i
7288 done
7289 }",
7290 "foo.sh",
7291 |metric| {
7292 assert_eq!(metric.loc.sloc(), 5);
7293 assert_eq!(metric.loc.ploc(), 5);
7294 assert_eq!(metric.loc.lloc(), 3);
7295 assert_eq!(metric.loc.cloc(), 0);
7296 assert_eq!(metric.loc.blank(), 0);
7297 insta::assert_json_snapshot!(metric.loc);
7298 },
7299 );
7300 }
7301
7302 #[cfg(feature = "bash")]
7303 #[test]
7304 fn bash_while_lloc() {
7305 check_metrics::<BashParser>(
7306 "f() {
7307 local n=5
7308 while [ $n -gt 0 ]; do
7309 echo $n
7310 n=$((n - 1))
7311 done
7312 }",
7313 "foo.sh",
7314 |metric| {
7315 assert_eq!(metric.loc.sloc(), 7);
7316 assert_eq!(metric.loc.ploc(), 7);
7317 // `f()`, `local n=5`, `while`, `echo $n`, and the
7318 // `n=$((n - 1))` assignment. That last one read as zero
7319 // until the LLOC arm learned the `variable_assignment`
7320 // alias the parser actually emits, so this asserted 4.
7321 assert_eq!(metric.loc.lloc(), 5);
7322 assert_eq!(metric.loc.cloc(), 0);
7323 assert_eq!(metric.loc.blank(), 0);
7324 insta::assert_json_snapshot!(metric.loc);
7325 },
7326 );
7327 }
7328
7329 /// A Bash assignment is a logical line where it stands as a
7330 /// statement, and no line of its own where it is part of one.
7331 ///
7332 /// The parser emits `variable_assignment` under the id the enum
7333 /// spells `VariableAssignment2`; the unsuffixed `VariableAssignment`
7334 /// it never emits, so the LLOC arm listing only that scored a bare
7335 /// `a=1` zero (`.claude/rules/grammar-dispatch.md` §1). Adding the
7336 /// alias ungated then double-counted the two positions where the
7337 /// assignment hangs off a node the same arm already counts — a
7338 /// `declaration_command` and a `command`'s environment prefix (§5).
7339 ///
7340 /// Both halves need a row: a table of standalone assignments alone
7341 /// passes with the parent gate deleted, and a table of wrapped ones
7342 /// alone passes with the alias never added.
7343 #[cfg(feature = "bash")]
7344 #[test]
7345 fn bash_assignment_counts_one_logical_line_per_statement() {
7346 for (source, lloc) in [
7347 (&b"a=1\nb=2\nc=3\n"[..], 3),
7348 (&b"local d=4\n"[..], 1),
7349 (&b"export e=5\n"[..], 1),
7350 (&b"declare -a g=(1 2)\n"[..], 1),
7351 (&b"X=1 cmd arg\n"[..], 1),
7352 (&b"(h=6)\n"[..], 1),
7353 (&b"for i in 1; do j=7; done\n"[..], 2),
7354 ] {
7355 let measured = metrics_verbatim(crate::LANG::Bash, source, MetricsOptions::default())
7356 .loc
7357 .lloc();
7358 assert_eq!(measured, lloc, "{:?}", String::from_utf8_lossy(source));
7359 }
7360 }
7361
7362 #[cfg(feature = "bash")]
7363 #[test]
7364 fn bash_case_lloc() {
7365 check_metrics::<BashParser>(
7366 "f() {
7367 case $1 in
7368 start) echo starting ;;
7369 stop) echo stopping ;;
7370 *) echo unknown ;;
7371 esac
7372 }",
7373 "foo.sh",
7374 |metric| {
7375 assert_eq!(metric.loc.sloc(), 7);
7376 assert_eq!(metric.loc.ploc(), 7);
7377 assert_eq!(metric.loc.lloc(), 5);
7378 assert_eq!(metric.loc.cloc(), 0);
7379 assert_eq!(metric.loc.blank(), 0);
7380 insta::assert_json_snapshot!(metric.loc);
7381 },
7382 );
7383 }
7384
7385 #[cfg(feature = "bash")]
7386 #[test]
7387 fn bash_multiple_functions_loc() {
7388 check_metrics::<BashParser>(
7389 "f() {
7390 echo hello
7391 }
7392 g() {
7393 echo world
7394 }",
7395 "foo.sh",
7396 |metric| {
7397 assert_eq!(metric.loc.sloc(), 6);
7398 assert_eq!(metric.loc.ploc(), 6);
7399 assert_eq!(metric.loc.lloc(), 4);
7400 assert_eq!(metric.loc.cloc(), 0);
7401 assert_eq!(metric.loc.blank(), 0);
7402 insta::assert_json_snapshot!(metric.loc);
7403 },
7404 );
7405 }
7406
7407 #[cfg(feature = "bash")]
7408 #[test]
7409 fn bash_nested_function_loc() {
7410 check_metrics::<BashParser>(
7411 "outer() {
7412 inner() {
7413 echo inner
7414 }
7415 inner
7416 echo outer
7417 }",
7418 "foo.sh",
7419 |metric| {
7420 assert_eq!(metric.loc.sloc(), 7);
7421 assert_eq!(metric.loc.ploc(), 7);
7422 assert_eq!(metric.loc.lloc(), 5);
7423 assert_eq!(metric.loc.cloc(), 0);
7424 assert_eq!(metric.loc.blank(), 0);
7425 insta::assert_json_snapshot!(metric.loc);
7426 },
7427 );
7428 }
7429
7430 #[cfg(feature = "bash")]
7431 #[test]
7432 fn bash_heredoc_loc() {
7433 // expected: six physical rows (`f() {`, `cat <<EOF`, `line1`,
7434 // `line2`, `EOF`, `}`), all code and none blank — a heredoc body's
7435 // rows are real source text (#1260, following #778 / #415). This
7436 // test pinned `ploc 5 / blank 1` until #1260: `heredoc_body` is a
7437 // childless node spanning both body rows, so the leaf arm credited
7438 // only `line1` and `line2` reached neither PLOC nor CLOC.
7439 check_metrics::<BashParser>(
7440 "f() {
7441 cat <<EOF
7442line1
7443line2
7444EOF
7445 }",
7446 "foo.sh",
7447 |metric| {
7448 assert_eq!(metric.loc.sloc(), 6);
7449 assert_eq!(metric.loc.ploc(), 6);
7450 assert_eq!(metric.loc.lloc(), 2);
7451 assert_eq!(metric.loc.cloc(), 0);
7452 assert_eq!(metric.loc.blank(), 0);
7453 insta::assert_json_snapshot!(metric.loc);
7454 },
7455 );
7456 }
7457
7458 #[cfg(feature = "bash")]
7459 #[test]
7460 fn bash_multiline_literals_credit_every_row_to_ploc() {
7461 // Regression test for #1260. Bash routed no string kind to a
7462 // PLOC/CLOC bucket, so the interior rows of every multi-row literal
7463 // reached neither and `blank = sloc - ploc - cloc` claimed them.
7464 //
7465 // Each fixture isolates one arm of the new match, so a dropped kind
7466 // fails here rather than being covered by a sibling
7467 // (`.claude/rules/grammar-dispatch.md` section 11): `raw_string`,
7468 // `ansi_c_string` and `heredoc_body` are childless nodes the old
7469 // leaf arm credited only one row of, while `string` emits a
7470 // `string_content` child per row that *has* text and so skipped an
7471 // empty interior row.
7472
7473 // expected: 5 rows — `cat <<DOC`, `line one`, `line two`, `DOC`,
7474 // `echo done` — all code, none blank. lloc 2 for the two commands.
7475 check_metrics::<BashParser>(
7476 "cat <<DOC\nline one\nline two\nDOC\necho done",
7477 "foo.sh",
7478 |metric| {
7479 assert_eq!(metric.loc.sloc(), 5);
7480 assert_eq!(metric.loc.ploc(), 5);
7481 assert_eq!(metric.loc.lloc(), 2);
7482 assert_eq!(metric.loc.cloc(), 0);
7483 assert_eq!(metric.loc.blank(), 0);
7484 },
7485 );
7486
7487 // expected: 3 rows of a single-quoted `raw_string` — `s='one`,
7488 // `two`, `three'` — all code, none blank.
7489 check_metrics::<BashParser>("s='one\ntwo\nthree'", "foo.sh", |metric| {
7490 assert_eq!(metric.loc.sloc(), 3);
7491 assert_eq!(metric.loc.ploc(), 3);
7492 assert_eq!(metric.loc.cloc(), 0);
7493 assert_eq!(metric.loc.blank(), 0);
7494 });
7495
7496 // expected: 2 rows of an `ansi_c_string` (`$'…'`) — `s=$'one` and
7497 // `two'` — both code, neither blank.
7498 check_metrics::<BashParser>("s=$'one\ntwo'", "foo.sh", |metric| {
7499 assert_eq!(metric.loc.sloc(), 2);
7500 assert_eq!(metric.loc.ploc(), 2);
7501 assert_eq!(metric.loc.cloc(), 0);
7502 assert_eq!(metric.loc.blank(), 0);
7503 });
7504
7505 // expected: 3 rows of a double-quoted `string` whose middle row is
7506 // empty — `s="one`, ``, `three"`. The empty row is inside the
7507 // literal, so it is code, not blank: every one of the eighteen
7508 // languages that already route a multi-line literal answers 0 blank
7509 // for this shape, and Bash now agrees.
7510 check_metrics::<BashParser>("s=\"one\n\nthree\"", "foo.sh", |metric| {
7511 assert_eq!(metric.loc.sloc(), 3);
7512 assert_eq!(metric.loc.ploc(), 3);
7513 assert_eq!(metric.loc.cloc(), 0);
7514 assert_eq!(metric.loc.blank(), 0);
7515 });
7516
7517 // expected: 4 rows of a quoted, tab-stripping heredoc — the
7518 // `cat <<-'DOC'` opener, one body row, an empty body row, and the
7519 // `DOC` terminator — all code, none blank. `<<-'DOC'` suppresses
7520 // interpolation, which is a distinct grammar path from the bare
7521 // `<<DOC` above.
7522 check_metrics::<BashParser>("cat <<-'DOC'\n\tline one\n\nDOC", "foo.sh", |metric| {
7523 assert_eq!(metric.loc.sloc(), 4);
7524 assert_eq!(metric.loc.ploc(), 4);
7525 assert_eq!(metric.loc.lloc(), 1);
7526 assert_eq!(metric.loc.cloc(), 0);
7527 assert_eq!(metric.loc.blank(), 0);
7528 });
7529
7530 // expected: 2 rows for a heredoc with an empty body — `cat <<DOC`
7531 // and the `DOC` terminator. The body node is zero-width here, so
7532 // this pins that the new arm adds no phantom row.
7533 check_metrics::<BashParser>("cat <<DOC\nDOC", "foo.sh", |metric| {
7534 assert_eq!(metric.loc.sloc(), 2);
7535 assert_eq!(metric.loc.ploc(), 2);
7536 assert_eq!(metric.loc.cloc(), 0);
7537 assert_eq!(metric.loc.blank(), 0);
7538 });
7539 }
7540
7541 /// Analyses `source` byte-for-byte as Bash, for the #1412 fixtures
7542 /// `check_metrics` cannot carry: that shim trims trailing newlines,
7543 /// and a heredoc whose terminator is the last row is exactly the
7544 /// shape it normalises away (`.claude/rules/testing.md`).
7545 #[cfg(feature = "bash")]
7546 fn bash_loc_verbatim(source: &[u8]) -> Stats {
7547 metrics_verbatim(
7548 crate::LANG::Bash,
7549 source,
7550 crate::MetricsOptions::default().with_only(&[crate::Metric::Loc]),
7551 )
7552 .loc
7553 }
7554
7555 /// #1412: a heredoc body that is empty at the top, or empty
7556 /// throughout, read as blank. `heredoc_body`'s span begins at the
7557 /// first body row that *has* text and collapses to zero width when
7558 /// there is none, so the leading empty rows sat inside no node at
7559 /// all and `blank = sloc - ploc - cloc` claimed them.
7560 ///
7561 /// #1260 routed the body, which fixed only the interior of a body
7562 /// that has text — the shape all of its fixtures had. The arm now
7563 /// routes the `heredoc_redirect` wrapper, the node present for every
7564 /// spelling (`.claude/rules/grammar-dispatch.md` section 6), which
7565 /// is what #1396 did for PHP.
7566 #[cfg(feature = "bash")]
7567 #[test]
7568 fn bash_heredoc_bodies_that_open_or_stay_empty_are_not_blank() {
7569 // `(source, rows the file has, blank reported before #1412)`.
7570 // The last column is what makes each row a regression test
7571 // rather than a restatement of the fixed behaviour.
7572 //
7573 // `sloc` is the second axis the fixtures are anchored on
7574 // (`.claude/rules/testing.md`): trimming the heredoc out of any
7575 // of them leaves a bare `cat` — or, in the last row, a bare
7576 // function — which fails the `sloc` assertion before the `blank`
7577 // one can go vacuous.
7578 const FIXTURES: &[(&[u8], u64, u64)] = &[
7579 // The issue's four reproductions. The fourth is the control:
7580 // it is the shape #1260's own fixtures had and was already
7581 // correct, so a "fix" that simply stopped counting body rows
7582 // moves it off `ploc == rows`.
7583 (b"cat <<EOT\n\nEOT\n", 3, 1),
7584 (b"cat <<EOT\n\n\nEOT\n", 4, 2),
7585 (b"cat <<EOT\n\na\nEOT\n", 4, 1),
7586 (b"cat <<EOT\na\n\nEOT\n", 4, 0),
7587 // The spellings and positions those four do not reach, each
7588 // a different grammar path to the same wrapper: a body empty
7589 // *and* terminated at EOF (`heredoc_end` is the last byte,
7590 // which `check_metrics` cannot express), the tab-stripping
7591 // `<<-` opener, the quoted `<<'EOT'` opener that suppresses
7592 // interpolation, and a heredoc nested in a function body.
7593 (b"cat <<EOT\n\nEOT", 3, 1),
7594 (b"cat <<-EOT\n\n\tx\n\tEOT\n", 4, 1),
7595 (b"cat <<'EOT'\n\nx\nEOT\n", 4, 1),
7596 (b"f() {\n cat <<EOT\n\n x\nEOT\n}\n", 6, 1),
7597 ];
7598
7599 for &(source, rows, blank_before) in FIXTURES {
7600 let text = String::from_utf8_lossy(source);
7601 let loc = bash_loc_verbatim(source);
7602 assert_eq!(loc.sloc(), rows, "sloc for {text:?}");
7603 assert_eq!(loc.ploc(), rows, "ploc for {text:?}");
7604 assert_eq!(loc.cloc(), 0, "cloc for {text:?}");
7605 assert_eq!(
7606 loc.blank(),
7607 0,
7608 "blank for {text:?}, which reported {blank_before} before #1412"
7609 );
7610 }
7611 }
7612
7613 /// The wrapper's span must not reach past the heredoc: rows after
7614 /// the terminator are the caller's own, blank ones included. Routing
7615 /// a *container* rather than the body is exactly the change that
7616 /// could over-credit, so this is the upper bound for #1412 the way
7617 /// `a_multiline_literal_credits_no_row_past_its_own_span` is for
7618 /// #1260.
7619 #[cfg(feature = "bash")]
7620 #[test]
7621 fn bash_heredoc_wrapper_credits_no_row_past_its_terminator() {
7622 // rows: 0 `cat <<EOT`, 1 (empty body), 2 `EOT`, 3 (blank),
7623 // 4 `# note`, 5 `echo after`. The blank row and the comment row
7624 // are outside the literal and must stay that way.
7625 let loc = bash_loc_verbatim(b"cat <<EOT\n\nEOT\n\n# note\necho after\n");
7626 assert_eq!(loc.sloc(), 6);
7627 assert_eq!(loc.ploc(), 4);
7628 assert_eq!(loc.cloc(), 1);
7629 assert_eq!(loc.blank(), 1);
7630 }
7631
7632 /// A `\`-continuation row inside a heredoc's command prefix is code
7633 /// (#1443, #1445).
7634 ///
7635 /// `heredoc_redirect` spans the command-line prefix as well as the
7636 /// literal, so crediting its whole interior over-credits a blank or
7637 /// comment-only prefix row. #1443 narrowed the range to the literal's
7638 /// own rows to fix that, and the narrowing was reverted: the shape it
7639 /// fixed needs `bash -n` to reject the file, while the shape it broke
7640 /// — this one — runs.
7641 ///
7642 /// A continuation row holding only `\` carries no leaf for the
7643 /// leaf-gated catch-all to credit, so the wrapper's blanket range is
7644 /// the only thing covering it. Narrowing took it from `ploc 5` to
7645 /// `ploc 4`. The general gap is #1445 (`echo a | \` + `\` +
7646 /// ` grep b` loses the row with no heredoc involved); until that
7647 /// lands, this pins the row the blanket range saves, so a second
7648 /// attempt at the narrowing fails here rather than shipping.
7649 #[cfg(feature = "bash")]
7650 #[test]
7651 fn bash_heredoc_continuation_row_is_code() {
7652 // rows: 0 `cat <<EOT | \`, 1 `\`, 2 ` grep x`, 3 `body`, 4 `EOT`.
7653 // Valid Bash — `bash -n` accepts it — and every row is code.
7654 let continuation = bash_loc_verbatim(b"cat <<EOT | \\\n\\\n grep x\nbody\nEOT\n");
7655 assert_eq!(continuation.sloc(), 5);
7656 assert_eq!(
7657 continuation.ploc(),
7658 5,
7659 "the `\\`-only continuation row is part of the command"
7660 );
7661 assert_eq!(continuation.cloc(), 0);
7662 assert_eq!(continuation.blank(), 0);
7663
7664 // The same command with content on the continuation row, which
7665 // the catch-all credits through its own leaves. Unaffected by the
7666 // range either way, and here so the pair brackets the shape.
7667 let with_content = bash_loc_verbatim(b"cat <<EOT | \\\n grep x\nbody\nEOT\n");
7668 assert_eq!(
7669 (
7670 with_content.sloc(),
7671 with_content.ploc(),
7672 with_content.blank()
7673 ),
7674 (4, 4, 0)
7675 );
7676 }
7677
7678 /// The orphan `heredoc_body` — the shape that makes routing the
7679 /// wrapper an *addition* rather than a replacement.
7680 ///
7681 /// `node-types.json` lists `heredoc_body` only as a child of
7682 /// `heredoc_redirect`, which describes the well-formed grammar and
7683 /// says nothing about error recovery. A single-line compound
7684 /// carrying a heredoc parses to an `{ERROR}` root whose direct child
7685 /// is the body, with no wrapper anywhere — so an arm keyed on the
7686 /// wrapper alone leaves that node to the leaf-gated catch-all, which
7687 /// credits only its start row, and the terminator row falls to
7688 /// `blank`. That is #1412's own symptom.
7689 ///
7690 /// Every input here is valid, executable Bash. Measured: each
7691 /// reported `blank 1` while the body kinds were dropped from the arm
7692 /// and `blank 0` both before and after.
7693 #[cfg(feature = "bash")]
7694 #[test]
7695 fn bash_heredoc_body_without_a_wrapper_still_credits_every_row() {
7696 // Three rows each: the compound, the body, the terminator. All
7697 // code, none blank — the same answer the well-formed spellings
7698 // give above.
7699 for source in [
7700 &b"f() { cat <<EOT; }\nbody\nEOT\n"[..],
7701 &b"if true; then cat <<EOT; fi\nx\nEOT\n"[..],
7702 &b"for i in 1; do cat <<EOT; done\nx\nEOT\n"[..],
7703 ] {
7704 let loc = bash_loc_verbatim(source);
7705 let text = String::from_utf8_lossy(source);
7706 assert_eq!(loc.sloc(), 3, "sloc for {text:?}");
7707 assert_eq!(loc.ploc(), 3, "ploc for {text:?}");
7708 assert_eq!(loc.cloc(), 0, "cloc for {text:?}");
7709 assert_eq!(
7710 loc.blank(),
7711 0,
7712 "the terminator row of a wrapper-less heredoc body is code, not blank, for {text:?}"
7713 );
7714 }
7715 }
7716
7717 #[cfg(feature = "bash")]
7718 #[test]
7719 fn bash_literal_that_owns_its_opening_row_still_credits_it() {
7720 // The other branch of `add_multiline_string_ploc`'s parent gate,
7721 // and the one every fixture above misses: each of those writes the
7722 // literal after `s=`, so the opening row is already PLOC and the
7723 // gate's *skip* path is harmless. Here the literal is the only
7724 // thing on its row.
7725 //
7726 // Bash is the language where that distinction bites. The helper
7727 // skips the opening row when the parent starts on it, which is safe
7728 // only where the catch-all credits every node's start row — and
7729 // Bash's is leaf-gated, so `command` / `command_name` contribute
7730 // nothing and a childless `raw_string` is the sole node covering
7731 // its own row. Routing it through the helper alone reported
7732 // `ploc 0, blank 1` for a one-line file.
7733
7734 // expected: one row, one code row, nothing blank. `'ls'` parses as
7735 // `command → command_name → raw_string`, none of whose ancestors
7736 // reaches PLOC.
7737 check_metrics::<BashParser>("'ls'", "foo.sh", |metric| {
7738 assert_eq!(metric.loc.sloc(), 1);
7739 assert_eq!(metric.loc.ploc(), 1);
7740 assert_eq!(metric.loc.cloc(), 0);
7741 assert_eq!(metric.loc.blank(), 0);
7742 });
7743
7744 // expected: the same for an `ansi_c_string`, the other childless
7745 // single-row literal.
7746 check_metrics::<BashParser>("$'ls'", "foo.sh", |metric| {
7747 assert_eq!(metric.loc.sloc(), 1);
7748 assert_eq!(metric.loc.ploc(), 1);
7749 assert_eq!(metric.loc.cloc(), 0);
7750 assert_eq!(metric.loc.blank(), 0);
7751 });
7752
7753 // expected: 3 rows — `f() {`, ` 'ls'`, `}` — all code. The literal
7754 // sits inside a function body, so a lost row shows as blank 1
7755 // rather than as an empty file.
7756 check_metrics::<BashParser>("f() {\n 'ls'\n}", "foo.sh", |metric| {
7757 assert_eq!(metric.loc.sloc(), 3);
7758 assert_eq!(metric.loc.ploc(), 3);
7759 assert_eq!(metric.loc.cloc(), 0);
7760 assert_eq!(metric.loc.blank(), 0);
7761 });
7762
7763 // expected: 2 rows — `# c` (comment-only) and `'ls'` (code). This
7764 // also covers the `check_comment_ends_on_code_line` call the gate
7765 // skipped along with the row: cloc stays 1 and blank stays 0.
7766 check_metrics::<BashParser>("# c\n'ls'", "foo.sh", |metric| {
7767 assert_eq!(metric.loc.sloc(), 2);
7768 assert_eq!(metric.loc.ploc(), 1);
7769 assert_eq!(metric.loc.cloc(), 1);
7770 assert_eq!(metric.loc.blank(), 0);
7771 });
7772
7773 // expected: 2 rows of a multi-row raw string that is the whole
7774 // command — `'a` and `b'`. Both code. Before #1260 this reported
7775 // `ploc 1` (the opening row only); the gate alone would have
7776 // reported `ploc 1` again, having traded the opening row for the
7777 // closing one.
7778 check_metrics::<BashParser>("'a\nb'", "foo.sh", |metric| {
7779 assert_eq!(metric.loc.sloc(), 2);
7780 assert_eq!(metric.loc.ploc(), 2);
7781 assert_eq!(metric.loc.cloc(), 0);
7782 assert_eq!(metric.loc.blank(), 0);
7783 });
7784 }
7785
7786 #[cfg(all(feature = "bash", feature = "tcl"))]
7787 #[test]
7788 fn a_multiline_literal_credits_no_row_past_its_own_span() {
7789 // The #1260 arms insert a *range* of rows, so the row after the
7790 // literal is the boundary worth pinning: an off-by-one in the range
7791 // end, or a literal node whose span is read one row too wide, would
7792 // pull the following comment row into PLOC. Each fixture puts a
7793 // comment-only row immediately after a closed literal, so that row
7794 // must stay comment-only.
7795
7796 // expected: 7 rows — `cat <<A`, `a1`, `A`, `# note`, `cat <<B`,
7797 // `b1`, `B`. Six are code and row 4 is comment-only: ploc 6,
7798 // cloc 1, blank 0. Two heredocs, so the row is bracketed on both
7799 // sides by a literal.
7800 check_metrics::<BashParser>(
7801 "cat <<A\na1\nA\n# note\ncat <<B\nb1\nB",
7802 "foo.sh",
7803 |metric| {
7804 assert_eq!(metric.loc.sloc(), 7);
7805 assert_eq!(metric.loc.ploc(), 6);
7806 assert_eq!(metric.loc.cloc(), 1);
7807 assert_eq!(metric.loc.blank(), 0);
7808 },
7809 );
7810
7811 // expected: 4 rows — `set a "x`, `y"`, `# note`, `set b 1` — with
7812 // row 3 comment-only: ploc 3, cloc 1, blank 0.
7813 check_metrics::<TclParser>("set a \"x\ny\"\n# note\nset b 1", "foo.tcl", |metric| {
7814 assert_eq!(metric.loc.sloc(), 4);
7815 assert_eq!(metric.loc.ploc(), 3);
7816 assert_eq!(metric.loc.cloc(), 1);
7817 assert_eq!(metric.loc.blank(), 0);
7818 });
7819 }
7820
7821 #[cfg(feature = "kotlin")]
7822 #[test]
7823 fn kotlin_loc_blank() {
7824 check_metrics::<KotlinParser>(
7825 "fun f(): Int {
7826
7827 val x = 1
7828
7829 return x
7830 }",
7831 "foo.kt",
7832 |metric| {
7833 assert_eq!(metric.loc.sloc(), 6);
7834 assert_eq!(metric.loc.ploc(), 4);
7835 assert_eq!(metric.loc.lloc(), 2);
7836 assert_eq!(metric.loc.cloc(), 0);
7837 assert_eq!(metric.loc.blank(), 2);
7838 insta::assert_json_snapshot!(metric.loc);
7839 },
7840 );
7841 }
7842
7843 #[cfg(feature = "kotlin")]
7844 #[test]
7845 fn kotlin_loc_cloc() {
7846 check_metrics::<KotlinParser>(
7847 "// header comment
7848 /* block
7849 comment */
7850 fun f(): Int {
7851 return 42 // inline
7852 }",
7853 "foo.kt",
7854 |metric| {
7855 assert_eq!(metric.loc.sloc(), 6);
7856 assert_eq!(metric.loc.ploc(), 3);
7857 assert_eq!(metric.loc.lloc(), 1);
7858 assert_eq!(metric.loc.cloc(), 4);
7859 assert_eq!(metric.loc.blank(), 0);
7860 insta::assert_json_snapshot!(metric.loc);
7861 },
7862 );
7863 }
7864
7865 #[cfg(feature = "kotlin")]
7866 #[test]
7867 fn kotlin_loc_no_zero_blank() {
7868 // Checks that the blank metric is not equal to 0 when there are some
7869 // comments next to code lines. Mirrors rust_no_zero_blank.
7870 check_metrics::<KotlinParser>(
7871 "fun connectToUpdateServer() {
7872 val pool = 0
7873
7874 val updateServer = -42
7875 val isConnected = false
7876 val currTry = 0
7877 val numRetries = 10 // Number of IPC connection retries before
7878 // giving up.
7879 val numTries = 20 // Number of IPC connection tries before
7880 // giving up.
7881 }",
7882 "foo.kt",
7883 |metric| {
7884 // Anchor the headline integer values; in particular
7885 // `blank() > 0` is the contract this test's name advertises.
7886 assert_eq!(metric.loc.sloc(), 11);
7887 assert_eq!(metric.loc.ploc(), 8);
7888 assert_eq!(metric.loc.cloc(), 4);
7889 assert_eq!(metric.loc.blank(), 1);
7890 insta::assert_json_snapshot!(
7891 metric.loc,
7892 @r#"
7893 {
7894 "sloc": 11,
7895 "ploc": 8,
7896 "lloc": 6,
7897 "cloc": 4,
7898 "blank": 1,
7899 "sloc_average": 5.5,
7900 "ploc_average": 4.0,
7901 "lloc_average": 3.0,
7902 "cloc_average": 2.0,
7903 "blank_average": 0.5,
7904 "sloc_min": 11,
7905 "sloc_max": 11,
7906 "cloc_min": 4,
7907 "cloc_max": 4,
7908 "ploc_min": 8,
7909 "ploc_max": 8,
7910 "lloc_min": 6,
7911 "lloc_max": 6,
7912 "blank_min": 1,
7913 "blank_max": 1
7914 }
7915 "#
7916 );
7917 },
7918 );
7919 }
7920
7921 #[cfg(feature = "kotlin")]
7922 #[test]
7923 fn kotlin_loc_blank_zero_sanity() {
7924 // Sanity: when the source has no blank lines, blank() must be 0.
7925 // Preserves the no-blank coverage previously held by
7926 // kotlin_loc_no_zero_blank before it was rewritten to assert the
7927 // positive case its name advertises.
7928 check_metrics::<KotlinParser>(
7929 "fun f(): Int {
7930 val x = 1 // x
7931 val y = 2 // y
7932 return x + y
7933 }",
7934 "foo.kt",
7935 |metric| {
7936 assert_eq!(metric.loc.sloc(), 5);
7937 assert_eq!(metric.loc.ploc(), 5);
7938 assert_eq!(metric.loc.lloc(), 3);
7939 assert_eq!(metric.loc.cloc(), 2);
7940 assert_eq!(metric.loc.blank(), 0);
7941 },
7942 );
7943 }
7944
7945 #[cfg(feature = "kotlin")]
7946 #[test]
7947 fn kotlin_loc_if_lloc() {
7948 check_metrics::<KotlinParser>(
7949 "fun classify(n: Int): String {
7950 if (n > 0) {
7951 return \"positive\"
7952 } else if (n < 0) {
7953 return \"negative\"
7954 }
7955 return \"zero\"
7956 }",
7957 "foo.kt",
7958 |metric| {
7959 assert_eq!(metric.loc.sloc(), 8);
7960 assert_eq!(metric.loc.ploc(), 8);
7961 assert_eq!(metric.loc.lloc(), 5);
7962 assert_eq!(metric.loc.cloc(), 0);
7963 assert_eq!(metric.loc.blank(), 0);
7964 insta::assert_json_snapshot!(metric.loc);
7965 },
7966 );
7967 }
7968
7969 #[cfg(feature = "kotlin")]
7970 #[test]
7971 fn kotlin_loc_for_lloc() {
7972 check_metrics::<KotlinParser>(
7973 "fun sum(n: Int): Int {
7974 var s = 0
7975 for (i in 1..n) {
7976 s += i
7977 }
7978 return s
7979 }",
7980 "foo.kt",
7981 |metric| {
7982 assert_eq!(metric.loc.sloc(), 7);
7983 assert_eq!(metric.loc.ploc(), 7);
7984 assert_eq!(metric.loc.lloc(), 4);
7985 assert_eq!(metric.loc.cloc(), 0);
7986 assert_eq!(metric.loc.blank(), 0);
7987 insta::assert_json_snapshot!(metric.loc);
7988 },
7989 );
7990 }
7991
7992 #[cfg(feature = "kotlin")]
7993 #[test]
7994 fn kotlin_loc_when_lloc() {
7995 check_metrics::<KotlinParser>(
7996 "fun describe(x: Int): String {
7997 return when (x) {
7998 1 -> \"one\"
7999 2 -> \"two\"
8000 else -> \"other\"
8001 }
8002 }",
8003 "foo.kt",
8004 |metric| {
8005 assert_eq!(metric.loc.sloc(), 7);
8006 assert_eq!(metric.loc.ploc(), 7);
8007 assert_eq!(metric.loc.lloc(), 2);
8008 assert_eq!(metric.loc.cloc(), 0);
8009 assert_eq!(metric.loc.blank(), 0);
8010 insta::assert_json_snapshot!(metric.loc);
8011 },
8012 );
8013 }
8014
8015 #[cfg(feature = "kotlin")]
8016 #[test]
8017 fn kotlin_loc_lambda_lloc() {
8018 check_metrics::<KotlinParser>(
8019 "fun f(list: List<Int>): List<Int> {
8020 return list.filter { it > 0 }
8021 .map { it * 2 }
8022 }",
8023 "foo.kt",
8024 |metric| {
8025 assert_eq!(metric.loc.sloc(), 4);
8026 assert_eq!(metric.loc.ploc(), 4);
8027 assert_eq!(metric.loc.lloc(), 1);
8028 assert_eq!(metric.loc.cloc(), 0);
8029 assert_eq!(metric.loc.blank(), 0);
8030 insta::assert_json_snapshot!(metric.loc);
8031 },
8032 );
8033 }
8034
8035 #[cfg(feature = "kotlin")]
8036 #[test]
8037 fn kotlin_loc_class_loc() {
8038 check_metrics::<KotlinParser>(
8039 "class Counter {
8040 private var count = 0
8041 fun increment() { count++ }
8042 fun get(): Int = count
8043 }",
8044 "foo.kt",
8045 |metric| {
8046 assert_eq!(metric.loc.sloc(), 5);
8047 assert_eq!(metric.loc.ploc(), 5);
8048 assert_eq!(metric.loc.lloc(), 1);
8049 assert_eq!(metric.loc.cloc(), 0);
8050 assert_eq!(metric.loc.blank(), 0);
8051 insta::assert_json_snapshot!(metric.loc);
8052 },
8053 );
8054 }
8055
8056 #[cfg(feature = "kotlin")]
8057 #[test]
8058 fn kotlin_loc_multiple_functions_loc() {
8059 check_metrics::<KotlinParser>(
8060 "fun f(): Int {
8061 return 1
8062 }
8063 fun g(): Int {
8064 return 2
8065 }",
8066 "foo.kt",
8067 |metric| {
8068 assert_eq!(metric.loc.sloc(), 6);
8069 assert_eq!(metric.loc.ploc(), 6);
8070 assert_eq!(metric.loc.lloc(), 2);
8071 assert_eq!(metric.loc.cloc(), 0);
8072 assert_eq!(metric.loc.blank(), 0);
8073 insta::assert_json_snapshot!(metric.loc);
8074 },
8075 );
8076 }
8077
8078 #[cfg(feature = "kotlin")]
8079 #[test]
8080 fn kotlin_loc_while_lloc() {
8081 check_metrics::<KotlinParser>(
8082 "fun countdown(n: Int) {
8083 var i = n
8084 while (i > 0) {
8085 println(i)
8086 i--
8087 }
8088 }",
8089 "foo.kt",
8090 |metric| {
8091 assert_eq!(metric.loc.sloc(), 7);
8092 assert_eq!(metric.loc.ploc(), 7);
8093 assert_eq!(metric.loc.lloc(), 3);
8094 assert_eq!(metric.loc.cloc(), 0);
8095 assert_eq!(metric.loc.blank(), 0);
8096 insta::assert_json_snapshot!(metric.loc);
8097 },
8098 );
8099 }
8100
8101 #[cfg(feature = "typescript")]
8102 #[test]
8103 fn typescript_blank() {
8104 check_metrics::<TypescriptParser>(
8105 "function f(): void {
8106
8107 const x = 1;
8108
8109 }",
8110 "foo.ts",
8111 |metric| {
8112 assert_eq!(metric.loc.sloc(), 5);
8113 assert_eq!(metric.loc.ploc(), 3);
8114 assert_eq!(metric.loc.lloc(), 1);
8115 assert_eq!(metric.loc.cloc(), 0);
8116 assert_eq!(metric.loc.blank(), 2);
8117 insta::assert_json_snapshot!(metric.loc);
8118 },
8119 );
8120 }
8121
8122 #[cfg(feature = "typescript")]
8123 #[test]
8124 fn typescript_cloc() {
8125 check_metrics::<TypescriptParser>(
8126 "// header
8127 /* block
8128 comment */
8129 function f(): number {
8130 return 42; // inline
8131 }",
8132 "foo.ts",
8133 |metric| {
8134 assert_eq!(metric.loc.sloc(), 6);
8135 assert_eq!(metric.loc.ploc(), 3);
8136 assert_eq!(metric.loc.lloc(), 1);
8137 assert_eq!(metric.loc.cloc(), 4);
8138 assert_eq!(metric.loc.blank(), 0);
8139 insta::assert_json_snapshot!(metric.loc);
8140 },
8141 );
8142 }
8143
8144 #[cfg(feature = "typescript")]
8145 #[test]
8146 fn typescript_no_zero_blank() {
8147 // Blank line interleaved with code that carries trailing comments —
8148 // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
8149 check_metrics::<TypescriptParser>(
8150 "function f(): void {
8151 const a = 1;
8152
8153 const b = 2; // trailing
8154 const c = 3; // trailing
8155}",
8156 "foo.ts",
8157 |metric| {
8158 assert_eq!(metric.loc.sloc(), 6);
8159 assert_eq!(metric.loc.ploc(), 5);
8160 assert_eq!(metric.loc.cloc(), 2);
8161 assert_eq!(metric.loc.blank(), 1);
8162 insta::assert_json_snapshot!(metric.loc);
8163 },
8164 );
8165 }
8166
8167 #[cfg(feature = "typescript")]
8168 #[test]
8169 fn typescript_if_lloc() {
8170 check_metrics::<TypescriptParser>(
8171 "function classify(n: number): string {
8172 if (n > 0) {
8173 return 'positive';
8174 } else {
8175 return 'non-positive';
8176 }
8177 }",
8178 "foo.ts",
8179 |metric| {
8180 assert_eq!(metric.loc.sloc(), 7);
8181 assert_eq!(metric.loc.ploc(), 7);
8182 assert_eq!(metric.loc.lloc(), 3);
8183 assert_eq!(metric.loc.cloc(), 0);
8184 assert_eq!(metric.loc.blank(), 0);
8185 insta::assert_json_snapshot!(metric.loc);
8186 },
8187 );
8188 }
8189
8190 #[cfg(feature = "typescript")]
8191 #[test]
8192 fn typescript_for_lloc() {
8193 check_metrics::<TypescriptParser>(
8194 "function sum(n: number): number {
8195 let s = 0;
8196 for (let i = 0; i < n; i++) {
8197 s += i;
8198 }
8199 return s;
8200 }",
8201 "foo.ts",
8202 |metric| {
8203 assert_eq!(metric.loc.sloc(), 7);
8204 assert_eq!(metric.loc.ploc(), 7);
8205 assert_eq!(metric.loc.lloc(), 4);
8206 assert_eq!(metric.loc.cloc(), 0);
8207 assert_eq!(metric.loc.blank(), 0);
8208 insta::assert_json_snapshot!(metric.loc);
8209 },
8210 );
8211 }
8212
8213 #[cfg(feature = "typescript")]
8214 #[test]
8215 fn typescript_while_lloc() {
8216 check_metrics::<TypescriptParser>(
8217 "function countdown(n: number): void {
8218 let i = n;
8219 while (i > 0) {
8220 console.log(i);
8221 i--;
8222 }
8223 }",
8224 "foo.ts",
8225 |metric| {
8226 assert_eq!(metric.loc.sloc(), 7);
8227 assert_eq!(metric.loc.ploc(), 7);
8228 assert_eq!(metric.loc.lloc(), 4);
8229 assert_eq!(metric.loc.cloc(), 0);
8230 assert_eq!(metric.loc.blank(), 0);
8231 insta::assert_json_snapshot!(metric.loc);
8232 },
8233 );
8234 }
8235
8236 #[cfg(feature = "typescript")]
8237 #[test]
8238 fn typescript_switch_lloc() {
8239 check_metrics::<TypescriptParser>(
8240 "function describe(x: number): string {
8241 switch (x) {
8242 case 1: return 'one';
8243 case 2: return 'two';
8244 default: return 'other';
8245 }
8246 }",
8247 "foo.ts",
8248 |metric| {
8249 assert_eq!(metric.loc.sloc(), 7);
8250 assert_eq!(metric.loc.ploc(), 7);
8251 assert_eq!(metric.loc.lloc(), 4);
8252 assert_eq!(metric.loc.cloc(), 0);
8253 assert_eq!(metric.loc.blank(), 0);
8254 insta::assert_json_snapshot!(metric.loc);
8255 },
8256 );
8257 }
8258
8259 #[cfg(feature = "typescript")]
8260 #[test]
8261 fn typescript_class_loc() {
8262 check_metrics::<TypescriptParser>(
8263 "class Counter {
8264 private count: number = 0;
8265 increment(): void { this.count++; }
8266 get(): number { return this.count; }
8267 }",
8268 "foo.ts",
8269 |metric| {
8270 assert_eq!(metric.loc.sloc(), 5);
8271 assert_eq!(metric.loc.ploc(), 5);
8272 assert_eq!(metric.loc.lloc(), 2);
8273 assert_eq!(metric.loc.cloc(), 0);
8274 assert_eq!(metric.loc.blank(), 0);
8275 insta::assert_json_snapshot!(metric.loc);
8276 },
8277 );
8278 }
8279
8280 #[cfg(feature = "typescript")]
8281 #[test]
8282 fn typescript_arrow_function_loc() {
8283 check_metrics::<TypescriptParser>(
8284 "const add = (a: number, b: number): number => a + b;
8285 const greet = (name: string): string => {
8286 return `Hello, ${name}`;
8287 };",
8288 "foo.ts",
8289 |metric| {
8290 assert_eq!(metric.loc.sloc(), 4);
8291 assert_eq!(metric.loc.ploc(), 4);
8292 assert_eq!(metric.loc.lloc(), 3);
8293 assert_eq!(metric.loc.cloc(), 0);
8294 assert_eq!(metric.loc.blank(), 0);
8295 insta::assert_json_snapshot!(metric.loc);
8296 },
8297 );
8298 }
8299
8300 #[cfg(feature = "typescript")]
8301 #[test]
8302 fn typescript_interface_loc() {
8303 check_metrics::<TypescriptParser>(
8304 "interface Shape {
8305 area(): number;
8306 perimeter(): number;
8307 }
8308 function describe(s: Shape): string {
8309 return `area=${s.area()}`;
8310 }",
8311 "foo.ts",
8312 |metric| {
8313 assert_eq!(metric.loc.sloc(), 7);
8314 assert_eq!(metric.loc.ploc(), 7);
8315 assert_eq!(metric.loc.lloc(), 1);
8316 assert_eq!(metric.loc.cloc(), 0);
8317 assert_eq!(metric.loc.blank(), 0);
8318 insta::assert_json_snapshot!(metric.loc);
8319 },
8320 );
8321 }
8322
8323 #[cfg(feature = "typescript")]
8324 #[test]
8325 fn typescript_multiple_functions_loc() {
8326 check_metrics::<TypescriptParser>(
8327 "function f(): number {
8328 return 1;
8329 }
8330 function g(): number {
8331 return 2;
8332 }
8333 function h(): number {
8334 return 3;
8335 }",
8336 "foo.ts",
8337 |metric| {
8338 assert_eq!(metric.loc.sloc(), 9);
8339 assert_eq!(metric.loc.ploc(), 9);
8340 assert_eq!(metric.loc.lloc(), 3);
8341 assert_eq!(metric.loc.cloc(), 0);
8342 assert_eq!(metric.loc.blank(), 0);
8343 insta::assert_json_snapshot!(metric.loc);
8344 },
8345 );
8346 }
8347
8348 #[cfg(feature = "typescript")]
8349 #[test]
8350 fn typescript_try_catch_lloc() {
8351 check_metrics::<TypescriptParser>(
8352 "function safe(x: number): number {
8353 try {
8354 return 1 / x;
8355 } catch (e) {
8356 return 0;
8357 }
8358 }",
8359 "foo.ts",
8360 |metric| {
8361 assert_eq!(metric.loc.sloc(), 7);
8362 assert_eq!(metric.loc.ploc(), 7);
8363 assert_eq!(metric.loc.lloc(), 3);
8364 assert_eq!(metric.loc.cloc(), 0);
8365 assert_eq!(metric.loc.blank(), 0);
8366 insta::assert_json_snapshot!(metric.loc);
8367 },
8368 );
8369 }
8370
8371 #[cfg(feature = "typescript")]
8372 #[test]
8373 fn typescript_nested_functions_loc() {
8374 check_metrics::<TypescriptParser>(
8375 "function outer(x: number): number {
8376 function inner(y: number): number {
8377 return y * 2;
8378 }
8379 return inner(x) + 1;
8380 }",
8381 "foo.ts",
8382 |metric| {
8383 assert_eq!(metric.loc.sloc(), 6);
8384 assert_eq!(metric.loc.ploc(), 6);
8385 assert_eq!(metric.loc.lloc(), 2);
8386 assert_eq!(metric.loc.cloc(), 0);
8387 assert_eq!(metric.loc.blank(), 0);
8388 insta::assert_json_snapshot!(metric.loc);
8389 },
8390 );
8391 }
8392
8393 #[cfg(feature = "typescript")]
8394 #[test]
8395 fn typescript_generic_function_loc() {
8396 check_metrics::<TypescriptParser>(
8397 "function identity<T>(value: T): T {
8398 return value;
8399 }
8400 function first<T>(arr: T[]): T | undefined {
8401 return arr[0];
8402 }",
8403 "foo.ts",
8404 |metric| {
8405 assert_eq!(metric.loc.sloc(), 6);
8406 assert_eq!(metric.loc.ploc(), 6);
8407 assert_eq!(metric.loc.lloc(), 2);
8408 assert_eq!(metric.loc.cloc(), 0);
8409 assert_eq!(metric.loc.blank(), 0);
8410 insta::assert_json_snapshot!(metric.loc);
8411 },
8412 );
8413 }
8414
8415 #[cfg(feature = "typescript")]
8416 #[test]
8417 fn tsx_blank() {
8418 check_metrics::<TsxParser>(
8419 "function f(): void {
8420
8421 const x = 1;
8422
8423 }",
8424 "foo.tsx",
8425 |metric| {
8426 assert_eq!(metric.loc.sloc(), 5);
8427 assert_eq!(metric.loc.ploc(), 3);
8428 assert_eq!(metric.loc.lloc(), 1);
8429 assert_eq!(metric.loc.cloc(), 0);
8430 assert_eq!(metric.loc.blank(), 2);
8431 insta::assert_json_snapshot!(metric.loc);
8432 },
8433 );
8434 }
8435
8436 #[cfg(feature = "typescript")]
8437 #[test]
8438 fn tsx_cloc() {
8439 check_metrics::<TsxParser>(
8440 "// header
8441 /* block
8442 comment */
8443 function f(): number {
8444 return 42; // inline
8445 }",
8446 "foo.tsx",
8447 |metric| {
8448 assert_eq!(metric.loc.sloc(), 6);
8449 assert_eq!(metric.loc.ploc(), 3);
8450 assert_eq!(metric.loc.lloc(), 1);
8451 assert_eq!(metric.loc.cloc(), 4);
8452 assert_eq!(metric.loc.blank(), 0);
8453 insta::assert_json_snapshot!(metric.loc);
8454 },
8455 );
8456 }
8457
8458 #[cfg(feature = "typescript")]
8459 #[test]
8460 fn tsx_no_zero_blank() {
8461 // Blank line interleaved with code that carries trailing comments —
8462 // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
8463 check_metrics::<TsxParser>(
8464 "function f(): void {
8465 const a = 1;
8466
8467 const b = 2; // trailing
8468 const c = 3; // trailing
8469}",
8470 "foo.tsx",
8471 |metric| {
8472 assert_eq!(metric.loc.sloc(), 6);
8473 assert_eq!(metric.loc.ploc(), 5);
8474 assert_eq!(metric.loc.cloc(), 2);
8475 assert_eq!(metric.loc.blank(), 1);
8476 insta::assert_json_snapshot!(metric.loc);
8477 },
8478 );
8479 }
8480
8481 #[cfg(feature = "typescript")]
8482 #[test]
8483 fn tsx_if_lloc() {
8484 check_metrics::<TsxParser>(
8485 "function classify(n: number): string {
8486 if (n > 0) {
8487 return 'positive';
8488 } else {
8489 return 'non-positive';
8490 }
8491 }",
8492 "foo.tsx",
8493 |metric| {
8494 assert_eq!(metric.loc.sloc(), 7);
8495 assert_eq!(metric.loc.ploc(), 7);
8496 assert_eq!(metric.loc.lloc(), 3);
8497 assert_eq!(metric.loc.cloc(), 0);
8498 assert_eq!(metric.loc.blank(), 0);
8499 insta::assert_json_snapshot!(metric.loc);
8500 },
8501 );
8502 }
8503
8504 #[cfg(feature = "typescript")]
8505 #[test]
8506 fn tsx_for_lloc() {
8507 check_metrics::<TsxParser>(
8508 "function sum(n: number): number {
8509 let s = 0;
8510 for (let i = 0; i < n; i++) {
8511 s += i;
8512 }
8513 return s;
8514 }",
8515 "foo.tsx",
8516 |metric| {
8517 assert_eq!(metric.loc.sloc(), 7);
8518 assert_eq!(metric.loc.ploc(), 7);
8519 assert_eq!(metric.loc.lloc(), 4);
8520 assert_eq!(metric.loc.cloc(), 0);
8521 assert_eq!(metric.loc.blank(), 0);
8522 insta::assert_json_snapshot!(metric.loc);
8523 },
8524 );
8525 }
8526
8527 #[cfg(feature = "typescript")]
8528 #[test]
8529 fn tsx_while_lloc() {
8530 check_metrics::<TsxParser>(
8531 "function countdown(n: number): void {
8532 let i = n;
8533 while (i > 0) {
8534 console.log(i);
8535 i--;
8536 }
8537 }",
8538 "foo.tsx",
8539 |metric| {
8540 assert_eq!(metric.loc.sloc(), 7);
8541 assert_eq!(metric.loc.ploc(), 7);
8542 assert_eq!(metric.loc.lloc(), 4);
8543 assert_eq!(metric.loc.cloc(), 0);
8544 assert_eq!(metric.loc.blank(), 0);
8545 insta::assert_json_snapshot!(metric.loc);
8546 },
8547 );
8548 }
8549
8550 #[cfg(feature = "typescript")]
8551 #[test]
8552 fn tsx_switch_lloc() {
8553 check_metrics::<TsxParser>(
8554 "function describe(x: number): string {
8555 switch (x) {
8556 case 1: return 'one';
8557 case 2: return 'two';
8558 default: return 'other';
8559 }
8560 }",
8561 "foo.tsx",
8562 |metric| {
8563 assert_eq!(metric.loc.sloc(), 7);
8564 assert_eq!(metric.loc.ploc(), 7);
8565 assert_eq!(metric.loc.lloc(), 4);
8566 assert_eq!(metric.loc.cloc(), 0);
8567 assert_eq!(metric.loc.blank(), 0);
8568 insta::assert_json_snapshot!(metric.loc);
8569 },
8570 );
8571 }
8572
8573 #[cfg(feature = "typescript")]
8574 #[test]
8575 fn tsx_class_loc() {
8576 check_metrics::<TsxParser>(
8577 "class Counter {
8578 private count: number = 0;
8579 increment(): void { this.count++; }
8580 get(): number { return this.count; }
8581 }",
8582 "foo.tsx",
8583 |metric| {
8584 assert_eq!(metric.loc.sloc(), 5);
8585 assert_eq!(metric.loc.ploc(), 5);
8586 assert_eq!(metric.loc.lloc(), 2);
8587 assert_eq!(metric.loc.cloc(), 0);
8588 assert_eq!(metric.loc.blank(), 0);
8589 insta::assert_json_snapshot!(metric.loc);
8590 },
8591 );
8592 }
8593
8594 #[cfg(feature = "typescript")]
8595 #[test]
8596 fn tsx_arrow_function_loc() {
8597 check_metrics::<TsxParser>(
8598 "const add = (a: number, b: number): number => a + b;
8599 const greet = (name: string): string => {
8600 return `Hello, ${name}`;
8601 };",
8602 "foo.tsx",
8603 |metric| {
8604 assert_eq!(metric.loc.sloc(), 4);
8605 assert_eq!(metric.loc.ploc(), 4);
8606 assert_eq!(metric.loc.lloc(), 3);
8607 assert_eq!(metric.loc.cloc(), 0);
8608 assert_eq!(metric.loc.blank(), 0);
8609 insta::assert_json_snapshot!(metric.loc);
8610 },
8611 );
8612 }
8613
8614 #[cfg(feature = "typescript")]
8615 #[test]
8616 fn tsx_multiple_functions_loc() {
8617 check_metrics::<TsxParser>(
8618 "function f(): number {
8619 return 1;
8620 }
8621 function g(): number {
8622 return 2;
8623 }
8624 function h(): number {
8625 return 3;
8626 }",
8627 "foo.tsx",
8628 |metric| {
8629 assert_eq!(metric.loc.sloc(), 9);
8630 assert_eq!(metric.loc.ploc(), 9);
8631 assert_eq!(metric.loc.lloc(), 3);
8632 assert_eq!(metric.loc.cloc(), 0);
8633 assert_eq!(metric.loc.blank(), 0);
8634 insta::assert_json_snapshot!(metric.loc);
8635 },
8636 );
8637 }
8638
8639 #[cfg(feature = "typescript")]
8640 #[test]
8641 fn tsx_try_catch_lloc() {
8642 check_metrics::<TsxParser>(
8643 "function safe(x: number): number {
8644 try {
8645 return 1 / x;
8646 } catch (e) {
8647 return 0;
8648 }
8649 }",
8650 "foo.tsx",
8651 |metric| {
8652 assert_eq!(metric.loc.sloc(), 7);
8653 assert_eq!(metric.loc.ploc(), 7);
8654 assert_eq!(metric.loc.lloc(), 3);
8655 assert_eq!(metric.loc.cloc(), 0);
8656 assert_eq!(metric.loc.blank(), 0);
8657 insta::assert_json_snapshot!(metric.loc);
8658 },
8659 );
8660 }
8661
8662 #[cfg(feature = "typescript")]
8663 #[test]
8664 fn tsx_nested_functions_loc() {
8665 check_metrics::<TsxParser>(
8666 "function outer(x: number): number {
8667 function inner(y: number): number {
8668 return y * 2;
8669 }
8670 return inner(x) + 1;
8671 }",
8672 "foo.tsx",
8673 |metric| {
8674 assert_eq!(metric.loc.sloc(), 6);
8675 assert_eq!(metric.loc.ploc(), 6);
8676 assert_eq!(metric.loc.lloc(), 2);
8677 assert_eq!(metric.loc.cloc(), 0);
8678 assert_eq!(metric.loc.blank(), 0);
8679 insta::assert_json_snapshot!(metric.loc);
8680 },
8681 );
8682 }
8683
8684 #[cfg(feature = "typescript")]
8685 #[test]
8686 fn tsx_interface_loc() {
8687 check_metrics::<TsxParser>(
8688 "interface Shape {
8689 area(): number;
8690 perimeter(): number;
8691 }
8692 function describe(s: Shape): string {
8693 return `area=${s.area()}`;
8694 }",
8695 "foo.tsx",
8696 |metric| {
8697 assert_eq!(metric.loc.sloc(), 7);
8698 assert_eq!(metric.loc.ploc(), 7);
8699 assert_eq!(metric.loc.lloc(), 1);
8700 assert_eq!(metric.loc.cloc(), 0);
8701 assert_eq!(metric.loc.blank(), 0);
8702 insta::assert_json_snapshot!(metric.loc);
8703 },
8704 );
8705 }
8706
8707 #[cfg(feature = "typescript")]
8708 #[test]
8709 fn tsx_generic_function_loc() {
8710 check_metrics::<TsxParser>(
8711 "function identity<T>(value: T): T {
8712 return value;
8713 }
8714 function first<T>(arr: T[]): T | undefined {
8715 return arr[0];
8716 }",
8717 "foo.tsx",
8718 |metric| {
8719 assert_eq!(metric.loc.sloc(), 6);
8720 assert_eq!(metric.loc.ploc(), 6);
8721 assert_eq!(metric.loc.lloc(), 2);
8722 assert_eq!(metric.loc.cloc(), 0);
8723 assert_eq!(metric.loc.blank(), 0);
8724 insta::assert_json_snapshot!(metric.loc);
8725 },
8726 );
8727 }
8728
8729 #[cfg(feature = "php")]
8730 #[test]
8731 fn php_blank() {
8732 check_metrics::<PhpParser>(
8733 "<?php
8734
8735$a = 1;
8736
8737$b = 2;
8738
8739",
8740 "foo.php",
8741 |metric| {
8742 assert_eq!(metric.loc.sloc(), 5);
8743 assert_eq!(metric.loc.ploc(), 3);
8744 assert_eq!(metric.loc.lloc(), 2);
8745 assert_eq!(metric.loc.cloc(), 0);
8746 assert_eq!(metric.loc.blank(), 2);
8747 insta::assert_json_snapshot!(metric.loc);
8748 },
8749 );
8750 }
8751
8752 #[cfg(feature = "php")]
8753 #[test]
8754 fn php_no_zero_blank() {
8755 // Blank line interleaved with code that carries trailing comments —
8756 // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
8757 check_metrics::<PhpParser>(
8758 "<?php
8759$a = 1;
8760
8761$b = 2; // trailing
8762$c = 3; // trailing
8763",
8764 "foo.php",
8765 |metric| {
8766 assert_eq!(metric.loc.sloc(), 5);
8767 assert_eq!(metric.loc.ploc(), 4);
8768 assert_eq!(metric.loc.cloc(), 2);
8769 assert_eq!(metric.loc.blank(), 1);
8770 insta::assert_json_snapshot!(metric.loc);
8771 },
8772 );
8773 }
8774
8775 #[cfg(feature = "php")]
8776 #[test]
8777 fn php_cloc_double_slash() {
8778 check_metrics::<PhpParser>(
8779 "<?php
8780// first
8781// second
8782$a = 1; // trailing",
8783 "foo.php",
8784 |metric| {
8785 assert_eq!(metric.loc.sloc(), 4);
8786 assert_eq!(metric.loc.ploc(), 2);
8787 assert_eq!(metric.loc.lloc(), 1);
8788 assert_eq!(metric.loc.cloc(), 3);
8789 assert_eq!(metric.loc.blank(), 0);
8790 insta::assert_json_snapshot!(metric.loc);
8791 },
8792 );
8793 }
8794
8795 #[cfg(feature = "php")]
8796 #[test]
8797 fn php_cloc_hash() {
8798 check_metrics::<PhpParser>(
8799 "<?php
8800# first
8801# second
8802$a = 1;",
8803 "foo.php",
8804 |metric| {
8805 assert_eq!(metric.loc.sloc(), 4);
8806 assert_eq!(metric.loc.ploc(), 2);
8807 assert_eq!(metric.loc.lloc(), 1);
8808 assert_eq!(metric.loc.cloc(), 2);
8809 assert_eq!(metric.loc.blank(), 0);
8810 insta::assert_json_snapshot!(metric.loc);
8811 },
8812 );
8813 }
8814
8815 #[cfg(feature = "php")]
8816 #[test]
8817 fn php_cloc_block() {
8818 check_metrics::<PhpParser>(
8819 "<?php
8820/*
8821 * block
8822 * comment
8823 */
8824$a = 1;",
8825 "foo.php",
8826 |metric| {
8827 assert_eq!(metric.loc.sloc(), 6);
8828 assert_eq!(metric.loc.ploc(), 2);
8829 assert_eq!(metric.loc.lloc(), 1);
8830 assert_eq!(metric.loc.cloc(), 4);
8831 assert_eq!(metric.loc.blank(), 0);
8832 insta::assert_json_snapshot!(metric.loc);
8833 },
8834 );
8835 }
8836
8837 #[cfg(feature = "php")]
8838 #[test]
8839 fn php_lloc() {
8840 // Three statements: assignment, if (with body), echo.
8841 check_metrics::<PhpParser>(
8842 "<?php
8843$a = 1;
8844if ($a > 0) {
8845 echo $a;
8846}",
8847 "foo.php",
8848 |metric| {
8849 assert_eq!(metric.loc.sloc(), 5);
8850 assert_eq!(metric.loc.ploc(), 5);
8851 assert_eq!(metric.loc.lloc(), 3);
8852 assert_eq!(metric.loc.cloc(), 0);
8853 assert_eq!(metric.loc.blank(), 0);
8854 insta::assert_json_snapshot!(metric.loc);
8855 },
8856 );
8857 }
8858
8859 #[cfg(feature = "php")]
8860 #[test]
8861 fn php_no_parenthesized_expression_lloc() {
8862 // Parenthesized expression should not add an extra LLOC over the
8863 // surrounding expression_statement.
8864 check_metrics::<PhpParser>(
8865 "<?php
8866$a = (1 + 2);",
8867 "foo.php",
8868 |metric| {
8869 assert_eq!(metric.loc.sloc(), 2);
8870 assert_eq!(metric.loc.ploc(), 2);
8871 assert_eq!(metric.loc.lloc(), 1);
8872 assert_eq!(metric.loc.cloc(), 0);
8873 assert_eq!(metric.loc.blank(), 0);
8874 insta::assert_json_snapshot!(metric.loc);
8875 },
8876 );
8877 }
8878
8879 #[cfg(feature = "php")]
8880 #[test]
8881 fn php_no_compound_statement_lloc() {
8882 // Block wrappers (`{ … }`) are not LLOC themselves.
8883 check_metrics::<PhpParser>(
8884 "<?php
8885function f(): void {
8886 $a = 1;
8887}",
8888 "foo.php",
8889 |metric| {
8890 assert_eq!(metric.loc.sloc(), 4);
8891 assert_eq!(metric.loc.ploc(), 4);
8892 assert_eq!(metric.loc.lloc(), 1);
8893 assert_eq!(metric.loc.cloc(), 0);
8894 assert_eq!(metric.loc.blank(), 0);
8895 insta::assert_json_snapshot!(metric.loc);
8896 },
8897 );
8898 }
8899
8900 #[cfg(feature = "php")]
8901 #[test]
8902 fn php_no_colon_block_lloc() {
8903 // Alternative syntax (`if: … endif;`) uses ColonBlock instead of
8904 // CompoundStatement; it is also not LLOC.
8905 check_metrics::<PhpParser>(
8906 "<?php
8907if (true):
8908 $a = 1;
8909endif;",
8910 "foo.php",
8911 |metric| {
8912 assert_eq!(metric.loc.sloc(), 4);
8913 assert_eq!(metric.loc.ploc(), 4);
8914 assert_eq!(metric.loc.lloc(), 2);
8915 assert_eq!(metric.loc.cloc(), 0);
8916 assert_eq!(metric.loc.blank(), 0);
8917 insta::assert_json_snapshot!(metric.loc);
8918 },
8919 );
8920 }
8921
8922 #[cfg(feature = "php")]
8923 #[test]
8924 fn php_no_else_clause_lloc() {
8925 // ElseClause and ElseIfClause are sub-parts of IfStatement.
8926 check_metrics::<PhpParser>(
8927 "<?php
8928if ($x) {
8929 $a = 1;
8930} elseif ($y) {
8931 $a = 2;
8932} else {
8933 $a = 3;
8934}",
8935 "foo.php",
8936 |metric| {
8937 assert_eq!(metric.loc.sloc(), 8);
8938 assert_eq!(metric.loc.ploc(), 8);
8939 assert_eq!(metric.loc.lloc(), 4);
8940 assert_eq!(metric.loc.cloc(), 0);
8941 assert_eq!(metric.loc.blank(), 0);
8942 insta::assert_json_snapshot!(metric.loc);
8943 },
8944 );
8945 }
8946
8947 #[cfg(feature = "php")]
8948 #[test]
8949 fn php_no_case_statement_lloc() {
8950 // CaseStatement / DefaultStatement are switch arms, not separate
8951 // statements.
8952 check_metrics::<PhpParser>(
8953 "<?php
8954switch ($x) {
8955 case 1:
8956 $a = 1;
8957 break;
8958 case 2:
8959 $a = 2;
8960 break;
8961 default:
8962 $a = 0;
8963}",
8964 "foo.php",
8965 |metric| {
8966 assert_eq!(metric.loc.sloc(), 11);
8967 assert_eq!(metric.loc.ploc(), 11);
8968 assert_eq!(metric.loc.lloc(), 6);
8969 assert_eq!(metric.loc.cloc(), 0);
8970 assert_eq!(metric.loc.blank(), 0);
8971 insta::assert_json_snapshot!(metric.loc);
8972 },
8973 );
8974 }
8975
8976 #[cfg(feature = "php")]
8977 #[test]
8978 fn php_no_match_arm_lloc() {
8979 // MatchConditionalExpression / MatchDefaultExpression are arms;
8980 // only the surrounding expression_statement counts.
8981 check_metrics::<PhpParser>(
8982 "<?php
8983$a = match ($x) {
8984 1 => 'one',
8985 2 => 'two',
8986 default => 'other',
8987};",
8988 "foo.php",
8989 |metric| {
8990 assert_eq!(metric.loc.sloc(), 6);
8991 assert_eq!(metric.loc.ploc(), 6);
8992 assert_eq!(metric.loc.lloc(), 1);
8993 assert_eq!(metric.loc.cloc(), 0);
8994 assert_eq!(metric.loc.blank(), 0);
8995 insta::assert_json_snapshot!(metric.loc);
8996 },
8997 );
8998 }
8999
9000 #[cfg(feature = "php")]
9001 #[test]
9002 fn php_no_throw_in_expression_lloc() {
9003 // PHP 8 `throw` as expression: only the surrounding statement
9004 // counts (the `??` in this example), not the throw_expression.
9005 check_metrics::<PhpParser>(
9006 "<?php
9007$x = $y ?? throw new \\Exception('nope');",
9008 "foo.php",
9009 |metric| {
9010 assert_eq!(metric.loc.sloc(), 2);
9011 assert_eq!(metric.loc.ploc(), 2);
9012 assert_eq!(metric.loc.lloc(), 1);
9013 assert_eq!(metric.loc.cloc(), 0);
9014 assert_eq!(metric.loc.blank(), 0);
9015 insta::assert_json_snapshot!(metric.loc);
9016 },
9017 );
9018 }
9019
9020 #[cfg(feature = "php")]
9021 #[test]
9022 fn php_no_closure_in_assignment_lloc() {
9023 // Anonymous function as RHS does not add an LLOC; only the
9024 // expression_statement counts. The closure body's statements are
9025 // counted in its own FuncSpace.
9026 check_metrics::<PhpParser>(
9027 "<?php
9028$f = function (): int {
9029 return 42;
9030};",
9031 "foo.php",
9032 |metric| {
9033 assert_eq!(metric.loc.sloc(), 4);
9034 assert_eq!(metric.loc.ploc(), 4);
9035 assert_eq!(metric.loc.lloc(), 2);
9036 assert_eq!(metric.loc.cloc(), 0);
9037 assert_eq!(metric.loc.blank(), 0);
9038 insta::assert_json_snapshot!(metric.loc);
9039 },
9040 );
9041 }
9042
9043 #[cfg(feature = "php")]
9044 #[test]
9045 fn php_for_lloc() {
9046 // The for_statement contributes 1 LLOC; init/cond/update are NOT
9047 // separate statements in PHP's grammar.
9048 check_metrics::<PhpParser>(
9049 "<?php
9050for ($i = 0; $i < 10; $i++) {
9051 echo $i;
9052}",
9053 "foo.php",
9054 |metric| {
9055 assert_eq!(metric.loc.sloc(), 4);
9056 assert_eq!(metric.loc.ploc(), 4);
9057 assert_eq!(metric.loc.lloc(), 2);
9058 assert_eq!(metric.loc.cloc(), 0);
9059 assert_eq!(metric.loc.blank(), 0);
9060 insta::assert_json_snapshot!(metric.loc);
9061 },
9062 );
9063 }
9064
9065 #[cfg(feature = "php")]
9066 #[test]
9067 fn php_foreach_lloc() {
9068 check_metrics::<PhpParser>(
9069 "<?php
9070foreach ($items as $k => $v) {
9071 echo $v;
9072}",
9073 "foo.php",
9074 |metric| {
9075 assert_eq!(metric.loc.sloc(), 4);
9076 assert_eq!(metric.loc.ploc(), 4);
9077 assert_eq!(metric.loc.lloc(), 2);
9078 assert_eq!(metric.loc.cloc(), 0);
9079 assert_eq!(metric.loc.blank(), 0);
9080 insta::assert_json_snapshot!(metric.loc);
9081 },
9082 );
9083 }
9084
9085 #[cfg(feature = "php")]
9086 #[test]
9087 fn php_try_lloc() {
9088 check_metrics::<PhpParser>(
9089 "<?php
9090try {
9091 $a = 1;
9092} catch (\\Exception $e) {
9093 $a = 0;
9094} finally {
9095 $b = 2;
9096}",
9097 "foo.php",
9098 |metric| {
9099 assert_eq!(metric.loc.sloc(), 8);
9100 assert_eq!(metric.loc.ploc(), 8);
9101 assert_eq!(metric.loc.lloc(), 4);
9102 assert_eq!(metric.loc.cloc(), 0);
9103 assert_eq!(metric.loc.blank(), 0);
9104 insta::assert_json_snapshot!(metric.loc);
9105 },
9106 );
9107 }
9108
9109 #[cfg(feature = "php")]
9110 #[test]
9111 fn php_class_loc() {
9112 check_metrics::<PhpParser>(
9113 "<?php
9114class A {
9115 public int $x = 0;
9116 private const Y = 1;
9117 public function f(): int {
9118 return $this->x;
9119 }
9120}",
9121 "foo.php",
9122 |metric| {
9123 assert_eq!(metric.loc.sloc(), 8);
9124 assert_eq!(metric.loc.ploc(), 8);
9125 assert_eq!(metric.loc.lloc(), 3);
9126 assert_eq!(metric.loc.cloc(), 0);
9127 assert_eq!(metric.loc.blank(), 0);
9128 insta::assert_json_snapshot!(metric.loc);
9129 },
9130 );
9131 }
9132
9133 #[cfg(feature = "php")]
9134 #[test]
9135 fn php_namespace_use_lloc() {
9136 check_metrics::<PhpParser>(
9137 "<?php
9138namespace App;
9139use App\\Foo;
9140use App\\Bar;
9141$a = 1;",
9142 "foo.php",
9143 |metric| {
9144 assert_eq!(metric.loc.sloc(), 5);
9145 assert_eq!(metric.loc.ploc(), 5);
9146 assert_eq!(metric.loc.lloc(), 3);
9147 assert_eq!(metric.loc.cloc(), 0);
9148 assert_eq!(metric.loc.blank(), 0);
9149 insta::assert_json_snapshot!(metric.loc);
9150 },
9151 );
9152 }
9153
9154 #[cfg(feature = "php")]
9155 #[test]
9156 fn php_general_loc() {
9157 check_metrics::<PhpParser>(
9158 "<?php
9159// header
9160namespace App;
9161use App\\Foo;
9162
9163class Bar {
9164 public int $n = 0;
9165
9166 public function add(int $x): int {
9167 if ($x > 0) {
9168 return $this->n + $x;
9169 }
9170 return $this->n;
9171 }
9172}",
9173 "foo.php",
9174 |metric| {
9175 assert_eq!(metric.loc.sloc(), 15);
9176 assert_eq!(metric.loc.ploc(), 12);
9177 assert_eq!(metric.loc.lloc(), 5);
9178 assert_eq!(metric.loc.cloc(), 1);
9179 assert_eq!(metric.loc.blank(), 2);
9180 insta::assert_json_snapshot!(metric.loc);
9181 },
9182 );
9183 }
9184
9185 #[cfg(feature = "php")]
9186 #[test]
9187 fn php_match_in_expression_lloc() {
9188 // Match inside another expression (e.g. assignment RHS) — the
9189 // outer expression_statement counts, the inner match arms do not.
9190 check_metrics::<PhpParser>(
9191 "<?php
9192$y = 10 + match ($x) { 1 => 2, default => 0 };",
9193 "foo.php",
9194 |metric| {
9195 assert_eq!(metric.loc.sloc(), 2);
9196 assert_eq!(metric.loc.ploc(), 2);
9197 assert_eq!(metric.loc.lloc(), 1);
9198 assert_eq!(metric.loc.cloc(), 0);
9199 assert_eq!(metric.loc.blank(), 0);
9200 insta::assert_json_snapshot!(metric.loc);
9201 },
9202 );
9203 }
9204
9205 #[cfg(feature = "php")]
9206 #[test]
9207 fn php_html_island_ploc() {
9208 // Embedded HTML between PHP tags ("text interpolation"). HTML
9209 // rows must contribute to PLOC (they are not blank and not a
9210 // PHP comment); this test locks that behavior so a future
9211 // grammar bump or impl tweak that excludes `text` nodes from
9212 // the default PLOC branch is caught.
9213 check_metrics::<PhpParser>(
9214 "<?php if ($cond): ?>
9215<div>hello</div>
9216<p>world</p>
9217<?php endif; ?>",
9218 "foo.php",
9219 |metric| {
9220 assert_eq!(metric.loc.sloc(), 4);
9221 assert_eq!(metric.loc.ploc(), 3);
9222 assert_eq!(metric.loc.lloc(), 1);
9223 assert_eq!(metric.loc.cloc(), 0);
9224 assert_eq!(metric.loc.blank(), 1);
9225 insta::assert_json_snapshot!(metric.loc);
9226 },
9227 );
9228 }
9229
9230 #[cfg(feature = "php")]
9231 #[test]
9232 fn php_short_echo_tag_ploc() {
9233 // `<?=` is the same `php_tag` kind as `<?php` per
9234 // tree-sitter-php 0.24.2. A regression that re-classified `<?=`
9235 // would shift PLOC; this test pins the current behavior.
9236 check_metrics::<PhpParser>("<p><?= $name ?></p>", "foo.php", |metric| {
9237 assert_eq!(metric.loc.sloc(), 1);
9238 assert_eq!(metric.loc.ploc(), 1);
9239 assert_eq!(metric.loc.lloc(), 1);
9240 assert_eq!(metric.loc.cloc(), 0);
9241 assert_eq!(metric.loc.blank(), 0);
9242 insta::assert_json_snapshot!(metric.loc);
9243 });
9244 }
9245
9246 /// Analyses `source` byte-for-byte as PHP, for the #1396 tests
9247 /// `check_metrics` cannot carry — one ends at EOF, which that shim
9248 /// normalises away, and one loops over labelled cases, which its bare
9249 /// `fn` callback cannot close over — and for the bounds test beside
9250 /// them.
9251 #[cfg(feature = "php")]
9252 fn php_loc(source: &[u8]) -> Stats {
9253 metrics_verbatim(
9254 crate::LANG::Php,
9255 source,
9256 crate::MetricsOptions::default().with_only(&[crate::Metric::Loc]),
9257 )
9258 .loc
9259 }
9260
9261 /// #1396: a heredoc row that is empty *inside* the literal read as
9262 /// blank. tree-sitter-php emits one `string_content` child per body
9263 /// row that **has text**, so a row empty inside the literal held no
9264 /// node at all and `blank = sloc - ploc - cloc` claimed it. #778
9265 /// routed PHP's quoted literals through `add_multiline_string_ploc`
9266 /// but excluded the heredoc, on the premise that its inner nodes
9267 /// already covered every row.
9268 ///
9269 /// `sloc` is the fixture anchor (`.claude/rules/testing.md`):
9270 /// deleting the empty row drops it to 5 and fails this test, rather
9271 /// than leaving it quietly asserting `blank 0` about a literal with
9272 /// no interior gap left to misclassify.
9273 #[cfg(feature = "php")]
9274 #[test]
9275 fn php_heredoc_empty_interior_row_is_code_not_blank() {
9276 // expected: six rows, every one code — `<?php`, the opening
9277 // `<<<EOT`, `a`, the empty body row, `b`, and the closing marker.
9278 check_metrics::<PhpParser>("<?php\n$s = <<<EOT\na\n\nb\nEOT;\n", "foo.php", |metric| {
9279 assert_eq!(metric.loc.sloc(), 6);
9280 assert_eq!(metric.loc.ploc(), 6);
9281 assert_eq!(metric.loc.cloc(), 0);
9282 assert_eq!(metric.loc.lloc(), 1);
9283 assert_eq!(metric.loc.blank(), 0);
9284 });
9285 }
9286
9287 /// The nowdoc half of #1396, which the issue did not measure and
9288 /// which was worse: it reported `ploc 4, blank 2` on this fixture
9289 /// against the heredoc's `ploc 5, blank 1`. The grammar shape
9290 /// differs: tree-sitter-php 0.24.2 emits one `nowdoc_string` per body
9291 /// line, but each after the first starts at the *end of the row
9292 /// before it*, so the catch-all's start-row insertion credited every
9293 /// body row to its predecessor and the last one (`b`) to nothing, on
9294 /// top of the empty row.
9295 ///
9296 /// Same `sloc` fixture anchor as the heredoc test above.
9297 #[cfg(feature = "php")]
9298 #[test]
9299 fn php_nowdoc_empty_interior_row_is_code_not_blank() {
9300 // expected: the heredoc fixture's six rows, all code.
9301 check_metrics::<PhpParser>(
9302 "<?php\n$s = <<<'EOT'\na\n\nb\nEOT;\n",
9303 "foo.php",
9304 |metric| {
9305 assert_eq!(metric.loc.sloc(), 6);
9306 assert_eq!(metric.loc.ploc(), 6);
9307 assert_eq!(metric.loc.cloc(), 0);
9308 assert_eq!(metric.loc.lloc(), 1);
9309 assert_eq!(metric.loc.blank(), 0);
9310 },
9311 );
9312 }
9313
9314 /// The four #1396 spellings the two tests above do not cover, each
9315 /// exercising a different part of the new arm.
9316 ///
9317 /// The all-empty body is why the arm routes the `heredoc` / `nowdoc`
9318 /// wrapper rather than `heredoc_body` / `nowdoc_body`: for a body of
9319 /// one empty row the grammar emits **no body node at all**, so an
9320 /// arm keyed on the body would still report that row blank
9321 /// (`.claude/rules/grammar-dispatch.md` section 6). The interpolated
9322 /// form checks that an interior `{$v}` — whose own nodes cover only
9323 /// their row — does not stop the empty row above it being credited.
9324 /// The PHP 7.3 indented closing marker is syntax coverage rather
9325 /// than a distinct code path: `heredoc_end` is credited by the
9326 /// catch-all whatever its indentation, so nothing here can observe
9327 /// an end-row error. It earns its place by failing pre-fix on its
9328 /// interior empty row, like the rest.
9329 ///
9330 /// The backtick `shell_command_expression` is the fifth PHP literal
9331 /// that can span rows and had the nowdoc shape — one multi-row
9332 /// `string_content` — so it lost interior rows too.
9333 ///
9334 /// Every row of every fixture is code, so each case's `sloc` is the
9335 /// fixture anchor: trimming a row from any of them fails that row's
9336 /// case by name. `php_heredoc_credits_only_the_literals_rows` below
9337 /// carries the other half — that the arm credits no row the literal
9338 /// does not span.
9339 #[cfg(feature = "php")]
9340 #[test]
9341 fn php_heredoc_spellings_credit_every_row_to_ploc() {
9342 // (label, source, expected sloc) — expected ploc is that same
9343 // sloc and expected blank is 0 for all five, which is the
9344 // property under test.
9345 let cases = [
9346 ("all-empty heredoc body", "<?php\n$s = <<<EOT\n\nEOT;\n", 4),
9347 ("all-empty nowdoc body", "<?php\n$s = <<<'EOT'\n\nEOT;\n", 4),
9348 (
9349 "interpolated heredoc",
9350 "<?php\n$v = 1;\n$s = <<<EOT\na\n\n{$v}\nEOT;\n",
9351 7,
9352 ),
9353 (
9354 "indented closing marker",
9355 "<?php\n$s = <<<EOT\n a\n\n b\n EOT;\n",
9356 6,
9357 ),
9358 ("backtick shell command", "<?php\n$s = `ls\n\n-l`;\n", 4),
9359 ];
9360 for (label, source, sloc) in cases {
9361 let loc = php_loc(source.as_bytes());
9362 assert_eq!(loc.sloc(), sloc, "{label} sloc");
9363 assert_eq!(loc.ploc(), sloc, "{label} ploc");
9364 assert_eq!(loc.cloc(), 0, "{label} cloc");
9365 assert_eq!(loc.blank(), 0, "{label} blank");
9366 }
9367 }
9368
9369 /// The upper bound the other #1396 tests cannot state. Every fixture
9370 /// above is 100% code, so `ploc == sloc` and `blank == 0` hold just
9371 /// as well for an arm that credits *more* than the literal — an arm
9372 /// inserting `start - 1`, or the whole file from row 0, passes all
9373 /// of them and the rest of the lib suite besides (measured).
9374 ///
9375 /// This fixture carries a comment row and a genuine blank row
9376 /// *outside* the literal, so `cloc` and `blank` are the axes that
9377 /// only a correctly-bounded arm can hit. Both over-crediting
9378 /// perturbations above drive `blank` to 0 and fail here.
9379 ///
9380 /// It is also a #1396 regression test in its own right: pre-fix the
9381 /// heredoc's empty interior row made this `ploc 5, blank 2`.
9382 #[cfg(feature = "php")]
9383 #[test]
9384 fn php_heredoc_credits_only_the_literals_rows() {
9385 // rows: 1 `<?php`, 2 comment, 3 blank, 4 opening, 5 `a`,
9386 // 6 empty-inside-the-literal, 7 `b`, 8 closing marker.
9387 // expected: ploc 6 (1, 4-8), cloc 1 (row 2), blank 1 (row 3).
9388 let loc = php_loc(b"<?php\n// note\n\n$s = <<<EOT\na\n\nb\nEOT;\n");
9389 assert_eq!(loc.sloc(), 8);
9390 assert_eq!(loc.ploc(), 6);
9391 assert_eq!(loc.cloc(), 1);
9392 assert_eq!(loc.blank(), 1);
9393 }
9394
9395 /// #1396 in the input class both metric harnesses normalise away: a
9396 /// heredoc whose closing marker is the last byte of the file.
9397 /// `check_metrics` trims and re-appends a trailing newline, so that
9398 /// shape cannot reach the parser through it at all
9399 /// (`.claude/rules/testing.md`); `metrics_verbatim` hands the bytes
9400 /// over untouched.
9401 ///
9402 /// Unlike #1051 and #1067, no *known* defect hides in this class for
9403 /// #1396 — the trailing-newline twin above already fails against
9404 /// unfixed code, and no perturbation yet found separates the two.
9405 /// A distinct input class costs one call, so it stays.
9406 #[cfg(feature = "php")]
9407 #[test]
9408 fn php_heredoc_ending_at_eof_credits_every_row() {
9409 // expected: the six rows of the issue's fixture, all code, with
9410 // no newline after `EOT;`.
9411 let loc = php_loc(b"<?php\n$s = <<<EOT\na\n\nb\nEOT;");
9412 assert_eq!(loc.sloc(), 6);
9413 assert_eq!(loc.ploc(), 6);
9414 assert_eq!(loc.cloc(), 0);
9415 assert_eq!(loc.blank(), 0);
9416 }
9417
9418 /// The #1414 fixture for [`add_multiline_string_ploc`]'s parent gate:
9419 /// two multi-row Rust literals, the first opening on row 1 under an
9420 /// `array_expression` that started on row 0, the second opening on
9421 /// row 4 alongside the `const_item` that owns it.
9422 ///
9423 /// Returned as a [`crate::Ast`] rather than as a pair of [`Node`]s
9424 /// because the nodes borrow the held tree.
9425 #[cfg(feature = "rust")]
9426 fn multiline_string_gate_fixture() -> crate::Ast {
9427 crate::test_support::parse_named(
9428 LANG::Rust,
9429 "lib.rs",
9430 "const A: [&str; 1] = [\n \"a\nb\",\n];\nconst B: &str = \"c\nd\";\n",
9431 )
9432 }
9433
9434 /// Returns the `index`-th `string_literal` of
9435 /// [`multiline_string_gate_fixture`], asserting the fixture still
9436 /// holds both and that this one still spans `rows`.
9437 ///
9438 /// The span check is the fixture's own guard: a grammar bump that
9439 /// re-shaped the literal would otherwise turn the row assertions
9440 /// below into claims about some other node.
9441 #[cfg(feature = "rust")]
9442 fn multiline_string_gate_literal<'a>(
9443 root: &Node<'a>,
9444 index: usize,
9445 rows: (usize, usize),
9446 ) -> Node<'a> {
9447 let literals = root.descendants_by_kind(&["string_literal"]);
9448 assert_eq!(
9449 literals.len(),
9450 2,
9451 "fixture drift: expected exactly two string literals"
9452 );
9453 let literal = literals[index];
9454 assert_eq!(
9455 (literal.start_row(), literal.end_row()),
9456 rows,
9457 "fixture drift: literal {index} no longer spans the expected rows"
9458 );
9459 literal
9460 }
9461
9462 /// [`add_multiline_string_ploc`] credits the opening row when the
9463 /// parent started earlier — the branch #1414 measured as unguarded.
9464 ///
9465 /// End-to-end neither half of that gate is observable, which is why
9466 /// this test is at the helper's level rather than through a
9467 /// fixture. Every language routing a literal through this helper
9468 /// ends its match with an unconditional catch-all that has already
9469 /// inserted the literal's start row, and both `LineSet::insert` and
9470 /// `check_comment_ends_on_code_line` are idempotent — so crediting
9471 /// the row twice reads the same as crediting it once, and skipping
9472 /// it drops nothing the catch-all does not put back. Before these
9473 /// tests, removing the gate and inverting it each failed zero of
9474 /// the ~3,330 lib tests. Only a call that reaches the helper with
9475 /// the row still absent separates them.
9476 #[cfg(feature = "rust")]
9477 #[test]
9478 fn multiline_string_credits_an_opening_row_its_parent_did_not_start() {
9479 let ast = multiline_string_gate_fixture();
9480 let root = ast.root_node();
9481 let literal = multiline_string_gate_literal(&root, 0, (1, 2));
9482
9483 let mut stats = Stats::default();
9484 // Seed the row a real caller's catch-all would already hold, so
9485 // nothing below can pass against a default-empty set.
9486 stats.ploc.lines.insert(0);
9487
9488 add_multiline_string_ploc(
9489 &literal,
9490 Ancestors::unknown(),
9491 &mut stats,
9492 literal.start_row(),
9493 );
9494
9495 assert!(stats.ploc.lines.contains(0), "the seeded row must survive");
9496 assert!(
9497 stats.ploc.lines.contains(1),
9498 "the `array_expression` parent starts on row 0, so nothing but \
9499 this helper covers the literal's opening row"
9500 );
9501 assert!(
9502 stats.ploc.lines.contains(2),
9503 "the literal's closing row is interior to it"
9504 );
9505 assert!(
9506 !stats.ploc.lines.contains(3),
9507 "row 3 is `];`, past the literal"
9508 );
9509 }
9510
9511 /// The gate's other side: [`add_multiline_string_ploc`] leaves the
9512 /// opening row alone when the parent started on it, and still runs
9513 /// the interior insertion. See the sibling above for why this is
9514 /// invisible end-to-end.
9515 #[cfg(feature = "rust")]
9516 #[test]
9517 fn multiline_string_leaves_an_opening_row_its_parent_started() {
9518 let ast = multiline_string_gate_fixture();
9519 let root = ast.root_node();
9520 let literal = multiline_string_gate_literal(&root, 1, (4, 5));
9521
9522 let mut stats = Stats::default();
9523 stats.ploc.lines.insert(0);
9524
9525 add_multiline_string_ploc(
9526 &literal,
9527 Ancestors::unknown(),
9528 &mut stats,
9529 literal.start_row(),
9530 );
9531
9532 assert!(stats.ploc.lines.contains(0), "the seeded row must survive");
9533 assert!(
9534 !stats.ploc.lines.contains(4),
9535 "the `const_item` parent starts on row 4 and already covers it, \
9536 so the gate must skip it"
9537 );
9538 assert!(
9539 stats.ploc.lines.contains(5),
9540 "the interior insertion runs whichever way the gate goes"
9541 );
9542 }
9543
9544 /// [`add_string_interior_ploc`] stops at the last row the literal
9545 /// *occupies*, not at its raw end row — the third #1414 branch, and
9546 /// the one that helper's own doc comment argues for.
9547 ///
9548 /// The two spellings differ only for a node whose end column is 0,
9549 /// and the one *string literal* known to have that shape here is a
9550 /// Bash `heredoc_body`: it absorbs the newline after its last
9551 /// content row and so ends at column 0 of the terminator's row.
9552 /// Everywhere else a literal stops just past its closing delimiter,
9553 /// where `end_line() - 1` and `end_row()` agree. Since #1423 the
9554 /// C-family `preproc_arg` is a second caller of this helper that
9555 /// reaches column 0 — a macro body ending on a dangling backslash —
9556 /// and `a_dangling_macro_continuation_does_not_credit_the_row_below`
9557 /// covers that one end-to-end.
9558 ///
9559 /// The call is direct rather than through `bca metrics`
9560 /// deliberately — which node `bash.rs` routes is a separate
9561 /// question (#1412), and an end-to-end fixture would stop
9562 /// discriminating the moment that answer changed.
9563 #[cfg(feature = "bash")]
9564 #[test]
9565 fn string_interior_stops_at_the_last_row_the_literal_occupies() {
9566 // rows: 0 `cat <<EOF`, 1 `a`, 2 `b`, 3 `EOF`.
9567 let ast = crate::test_support::parse_named(LANG::Bash, "f.sh", "cat <<EOF\na\nb\nEOF\n");
9568 let root = ast.root_node();
9569 let bodies = root.descendants_by_kind(&["heredoc_body"]);
9570 let body = bodies.first().expect("the fixture holds one heredoc body");
9571 assert_eq!(
9572 (body.start_position(), body.end_position()),
9573 ((1, 0), (3, 0)),
9574 "fixture drift: the end column must be 0 for this test to mean anything"
9575 );
9576
9577 let mut stats = Stats::default();
9578 // Row 0 is the `cat <<EOF` line, which Bash's arm credits
9579 // through a different node; seeding it keeps every assertion
9580 // below distinguishable from the default-empty set.
9581 stats.ploc.lines.insert(0);
9582
9583 add_string_interior_ploc(body, &mut stats, body.start_row());
9584
9585 assert!(stats.ploc.lines.contains(0), "the seeded row must survive");
9586 assert!(
9587 !stats.ploc.lines.contains(1),
9588 "this helper leaves its `start` row to the caller"
9589 );
9590 assert!(
9591 stats.ploc.lines.contains(2),
9592 "row 2 is the body's last content row"
9593 );
9594 assert!(
9595 !stats.ploc.lines.contains(3),
9596 "row 3 holds the `EOF` terminator: the body ends at its column 0 \
9597 and so does not occupy it"
9598 );
9599 }
9600
9601 #[cfg(feature = "elixir")]
9602 #[test]
9603 fn elixir_blank() {
9604 // Two blank lines separate three top-level expressions.
9605 check_metrics::<ElixirParser>(
9606 "defmodule Foo do\n\n def a, do: :a\n\n def b, do: :b\nend\n",
9607 "foo.ex",
9608 |metric| {
9609 assert_eq!(metric.loc.sloc(), 6);
9610 assert_eq!(metric.loc.ploc(), 4);
9611 assert_eq!(metric.loc.lloc(), 3);
9612 assert_eq!(metric.loc.cloc(), 0);
9613 assert_eq!(metric.loc.blank(), 2);
9614 insta::assert_json_snapshot!(
9615 metric.loc,
9616 @r#"
9617 {
9618 "sloc": 6,
9619 "ploc": 4,
9620 "lloc": 3,
9621 "cloc": 0,
9622 "blank": 2,
9623 "sloc_average": 1.5,
9624 "ploc_average": 1.0,
9625 "lloc_average": 0.75,
9626 "cloc_average": 0.0,
9627 "blank_average": 0.5,
9628 "sloc_min": 1,
9629 "sloc_max": 6,
9630 "cloc_min": 0,
9631 "cloc_max": 0,
9632 "ploc_min": 1,
9633 "ploc_max": 4,
9634 "lloc_min": 1,
9635 "lloc_max": 3,
9636 "blank_min": 0,
9637 "blank_max": 2
9638 }
9639 "#
9640 );
9641 },
9642 );
9643 }
9644
9645 #[cfg(feature = "elixir")]
9646 #[test]
9647 fn elixir_no_zero_blank() {
9648 // Blank line interleaved with code that carries trailing comments —
9649 // stresses the `blank = sloc - (ploc ∪ cloc lines)` union math.
9650 check_metrics::<ElixirParser>(
9651 "defmodule Foo do\n def f, do: :ok\n\n def g, do: :ok # trailing\n def h, do: :ok # trailing\nend\n",
9652 "foo.ex",
9653 |metric| {
9654 assert_eq!(metric.loc.sloc(), 6);
9655 assert_eq!(metric.loc.ploc(), 5);
9656 assert_eq!(metric.loc.cloc(), 2);
9657 assert_eq!(metric.loc.blank(), 1);
9658 },
9659 );
9660 }
9661
9662 #[cfg(feature = "elixir")]
9663 #[test]
9664 fn elixir_blank_zero_sanity() {
9665 // Sanity check: blank must report 0, never go negative, when the
9666 // input has no blank lines.
9667 check_metrics::<ElixirParser>(
9668 "defmodule Foo do\n def f, do: :ok\nend\n",
9669 "foo.ex",
9670 |metric| {
9671 assert_eq!(metric.loc.blank(), 0);
9672 },
9673 );
9674 }
9675
9676 #[cfg(feature = "elixir")]
9677 #[test]
9678 fn elixir_cloc() {
9679 // Mix of standalone comments and a comment on the same line as
9680 // code. Elixir has no block comment syntax — only `#` lines.
9681 check_metrics::<ElixirParser>(
9682 "# top\ndefmodule Foo do\n # body\n def f, do: :ok # trailing\nend\n",
9683 "foo.ex",
9684 |metric| {
9685 assert_eq!(metric.loc.cloc(), 3);
9686 },
9687 );
9688 }
9689
9690 #[cfg(feature = "elixir")]
9691 #[test]
9692 fn elixir_lloc() {
9693 // Two statements at the top level of the module body — the
9694 // `defmodule` call itself counts as one statement (since its
9695 // parent is `Source`), and each `def` inside its `do_block`
9696 // counts too: 1 + 2 = 3.
9697 check_metrics::<ElixirParser>(
9698 "defmodule Foo do\n def a, do: 1\n def b, do: 2\nend\n",
9699 "foo.ex",
9700 |metric| {
9701 assert_eq!(metric.loc.lloc(), 3);
9702 },
9703 );
9704 }
9705
9706 #[cfg(feature = "elixir")]
9707 #[test]
9708 fn elixir_no_nested_call_lloc() {
9709 // Calls nested inside another call's arguments are NOT direct
9710 // children of a statement container, so they do not bump LLOC.
9711 // Three syntactic calls (`defmodule`, `def`, `IO.puts`) → 3.
9712 check_metrics::<ElixirParser>(
9713 "defmodule Foo do\n def f do\n IO.puts(Enum.join([1, 2, 3], \", \"))\n end\nend\n",
9714 "foo.ex",
9715 |metric| {
9716 assert_eq!(metric.loc.lloc(), 3);
9717 },
9718 );
9719 }
9720
9721 #[cfg(feature = "elixir")]
9722 #[test]
9723 fn elixir_no_binary_operator_inside_call_lloc() {
9724 // Binary operators inside call arguments are sub-expressions,
9725 // not statements. A single `def` body containing `IO.puts(a + b)`
9726 // produces 3 LLOC (defmodule, def, IO.puts) — the `a + b`
9727 // binary_operator is not a direct child of any statement
9728 // container.
9729 check_metrics::<ElixirParser>(
9730 "defmodule Foo do\n def f(a, b) do\n IO.puts(a + b)\n end\nend\n",
9731 "foo.ex",
9732 |metric| {
9733 assert_eq!(metric.loc.lloc(), 3);
9734 },
9735 );
9736 }
9737
9738 #[cfg(feature = "elixir")]
9739 #[test]
9740 fn elixir_stab_clause_counts_lloc() {
9741 // Each `stab_clause` arm in a `case do ... end` is a direct
9742 // child of the inner `do_block`, so each one is its own LLOC.
9743 // defmodule + def + case + 3 arms = 6 logical lines.
9744 check_metrics::<ElixirParser>(
9745 "defmodule Foo do\n def f(x) do\n case x do\n 1 -> :a\n 2 -> :b\n _ -> :c\n end\n end\nend\n",
9746 "foo.ex",
9747 |metric| {
9748 assert_eq!(metric.loc.lloc(), 6);
9749 },
9750 );
9751 }
9752
9753 #[cfg(feature = "elixir")]
9754 #[test]
9755 fn elixir_no_comment_lloc() {
9756 // Comments are direct children of a statement container but
9757 // are routed through the dedicated `Comment` arm in `compute`,
9758 // so they MUST NOT bump LLOC. Only `defmodule` and `def`
9759 // contribute LLOC here.
9760 check_metrics::<ElixirParser>(
9761 "# leading\ndefmodule Foo do\n # inside\n def f, do: :ok\n # trailing\nend\n",
9762 "foo.ex",
9763 |metric| {
9764 assert_eq!(metric.loc.lloc(), 2);
9765 },
9766 );
9767 }
9768
9769 #[cfg(feature = "elixir")]
9770 #[test]
9771 fn elixir_no_do_token_lloc() {
9772 // The `do` and `end` keyword tokens are unnamed leaves inside a
9773 // `do_block`; they must not be counted as statements. A body
9774 // with one expression produces exactly 2 LLOC (defmodule and
9775 // the inner expression).
9776 check_metrics::<ElixirParser>("defmodule Foo do\n :ok\nend\n", "foo.ex", |metric| {
9777 // `:ok` is an `Atom` whose parent is the module-call's
9778 // `do_block`; that counts. Plus the `defmodule` call.
9779 assert_eq!(metric.loc.lloc(), 2);
9780 });
9781 }
9782
9783 #[cfg(feature = "elixir")]
9784 #[test]
9785 fn elixir_no_keyword_pair_lloc() {
9786 // `key: value` keyword pairs inside an argument list (`def f,
9787 // do: :ok`) are children of an `arguments` / `keywords` node,
9788 // not a statement container, so they don't bump LLOC.
9789 check_metrics::<ElixirParser>(
9790 "defmodule Foo do\n def add(a, b), do: a + b\nend\n",
9791 "foo.ex",
9792 |metric| {
9793 // defmodule (1) + def (1) = 2
9794 assert_eq!(metric.loc.lloc(), 2);
9795 },
9796 );
9797 }
9798
9799 #[cfg(feature = "elixir")]
9800 #[test]
9801 fn elixir_no_string_content_lloc() {
9802 // `quoted_content` chunks inside a heredoc / regular string are
9803 // structural and don't represent statements. A `@moduledoc`
9804 // attribute call with a multi-line string contributes exactly
9805 // one LLOC (the `@moduledoc` call), not one per content line.
9806 check_metrics::<ElixirParser>(
9807 "defmodule Foo do\n @moduledoc \"\"\"\n line one\n line two\n \"\"\"\n def f, do: :ok\nend\n",
9808 "foo.ex",
9809 |metric| {
9810 // defmodule + @moduledoc + def = 3
9811 assert_eq!(metric.loc.lloc(), 3);
9812 },
9813 );
9814 }
9815
9816 #[cfg(feature = "elixir")]
9817 #[test]
9818 fn elixir_multiline_string_forms_credit_every_row_to_ploc() {
9819 // Regression test for #1260. `quoted_content` — the literal text of
9820 // every Elixir string form — is childless, so the leaf branch of
9821 // the catch-all credited only its opening row and the interior rows
9822 // reached neither PLOC nor CLOC.
9823
9824 // expected: 3 rows of a plain `"…"` literal — `x = "line one`,
9825 // `line two`, `line three"` — all code, none blank.
9826 check_metrics::<ElixirParser>(
9827 "x = \"line one\nline two\nline three\"",
9828 "foo.ex",
9829 |metric| {
9830 assert_eq!(metric.loc.sloc(), 3);
9831 assert_eq!(metric.loc.ploc(), 3);
9832 assert_eq!(metric.loc.cloc(), 0);
9833 assert_eq!(metric.loc.blank(), 0);
9834 },
9835 );
9836
9837 // expected: 4 rows of a `'''` charlist whose third row is empty
9838 // inside the literal — `c = '''`, `c1`, ``, `'''`. Code, not blank.
9839 check_metrics::<ElixirParser>("c = '''\nc1\n\n'''", "foo.ex", |metric| {
9840 assert_eq!(metric.loc.sloc(), 4);
9841 assert_eq!(metric.loc.ploc(), 4);
9842 assert_eq!(metric.loc.cloc(), 0);
9843 assert_eq!(metric.loc.blank(), 0);
9844 });
9845
9846 // expected: 4 rows of a `~s"""` sigil heredoc — `s = ~s"""`, `s1`,
9847 // `s2`, `"""`. A sigil is a distinct grammar node from `string` and
9848 // `charlist`; all three wrap the same `quoted_content`, which is
9849 // why one arm covers them.
9850 check_metrics::<ElixirParser>("s = ~s\"\"\"\ns1\ns2\n\"\"\"", "foo.ex", |metric| {
9851 assert_eq!(metric.loc.sloc(), 4);
9852 assert_eq!(metric.loc.ploc(), 4);
9853 assert_eq!(metric.loc.cloc(), 0);
9854 assert_eq!(metric.loc.blank(), 0);
9855 });
9856 }
9857
9858 #[cfg(feature = "elixir")]
9859 #[test]
9860 fn elixir_module_attribute_docstring_rows_are_ploc_not_cloc() {
9861 // #1260 had to choose a bucket for `@doc` / `@moduledoc` heredoc
9862 // rows, and this pins it: PLOC.
9863 //
9864 // Python's one carve-out to CLOC is a *bare* string expression
9865 // statement whose value is discarded — a docstring by position. An
9866 // Elixir module attribute is not that shape: `@doc "…"` is an
9867 // assignment whose value the compiler stores and
9868 // `Code.fetch_docs/1` reads back, so its Python analogue is
9869 // `x = """…"""`, which Python counts as PLOC (#415).
9870 //
9871 // expected: 7 rows — `defmodule M do`, ` @doc """`, ` docs one`,
9872 // ` docs two`, ` """`, ` def f, do: :ok`, `end` — all code, none
9873 // blank and none comment. lloc 3 = defmodule + @doc + def.
9874 check_metrics::<ElixirParser>(
9875 "defmodule M do\n @doc \"\"\"\n docs one\n docs two\n \"\"\"\n def f, do: :ok\nend",
9876 "foo.ex",
9877 |metric| {
9878 assert_eq!(metric.loc.sloc(), 7);
9879 assert_eq!(metric.loc.ploc(), 7);
9880 assert_eq!(metric.loc.lloc(), 3);
9881 assert_eq!(metric.loc.cloc(), 0);
9882 assert_eq!(metric.loc.blank(), 0);
9883 },
9884 );
9885 }
9886
9887 #[cfg(feature = "elixir")]
9888 #[test]
9889 fn elixir_rescue_arm_counts_lloc() {
9890 // Each rescue arm's body has a single expression (e.g. `:bad`)
9891 // that counts as one LLOC; the `stab_clause` header itself is
9892 // skipped. The rescue_block named node is also a direct child
9893 // of try's do_block, so it contributes one LLOC too.
9894 // Total: defmodule + def + try + do_it() + rescue_block
9895 // + 2 arm bodies = 7.
9896 check_metrics::<ElixirParser>(
9897 "defmodule Foo do\n def safe do\n try do\n do_it()\n rescue\n ArgumentError -> :bad\n RuntimeError -> :worse\n end\n end\nend\n",
9898 "foo.ex",
9899 |metric| {
9900 assert_eq!(metric.loc.lloc(), 7);
9901 },
9902 );
9903 }
9904
9905 #[cfg(feature = "elixir")]
9906 #[test]
9907 fn elixir_no_arg_punctuation_lloc() {
9908 // Function-call arguments (`a, b` inside `def add(a, b)`) are
9909 // children of an `arguments` node, not of a statement container.
9910 // They MUST NOT inflate LLOC.
9911 check_metrics::<ElixirParser>(
9912 "defmodule Foo do\n def add(a, b, c, d) do\n a + b + c + d\n end\nend\n",
9913 "foo.ex",
9914 |metric| {
9915 // defmodule + def + (a+b+c+d) = 3
9916 assert_eq!(metric.loc.lloc(), 3);
9917 },
9918 );
9919 }
9920
9921 #[cfg(feature = "elixir")]
9922 #[test]
9923 fn elixir_no_list_element_lloc() {
9924 // List literal elements live under a `list` node, not a
9925 // statement container — they must not bump LLOC.
9926 check_metrics::<ElixirParser>(
9927 "defmodule Foo do\n def f do\n [:a, :b, :c, :d]\n end\nend\n",
9928 "foo.ex",
9929 |metric| {
9930 // defmodule + def + the list expression = 3
9931 assert_eq!(metric.loc.lloc(), 3);
9932 },
9933 );
9934 }
9935
9936 #[cfg(feature = "elixir")]
9937 #[test]
9938 fn elixir_no_map_field_lloc() {
9939 // Map `pair`s live under `map`, not a statement container.
9940 check_metrics::<ElixirParser>(
9941 "defmodule Foo do\n def f do\n %{a: 1, b: 2, c: 3}\n end\nend\n",
9942 "foo.ex",
9943 |metric| {
9944 assert_eq!(metric.loc.lloc(), 3);
9945 },
9946 );
9947 }
9948
9949 #[cfg(feature = "elixir")]
9950 #[test]
9951 fn elixir_anonymous_fn_body_lloc() {
9952 // `lloc()` on the Unit space returns the aggregate (own +
9953 // nested-space) count. Even though the anonymous_function is
9954 // its own function space, the merge step pulls its `lloc` back
9955 // into the parent. Counts:
9956 // Unit own: defmodule, def, `add = fn ...`, final `add` = 4
9957 // anon-fn: `x + 1` body expression = 1
9958 // aggregated total = 5
9959 check_metrics::<ElixirParser>(
9960 "defmodule Foo do\n def f do\n add = fn x -> x + 1 end\n add\n end\nend\n",
9961 "foo.ex",
9962 |metric| {
9963 assert_eq!(metric.loc.lloc(), 5);
9964 },
9965 );
9966 }
9967
9968 #[cfg(feature = "ruby")]
9969 #[test]
9970 fn ruby_blank() {
9971 // The parser's root span starts at the first non-blank line, so
9972 // a blank line must sit BETWEEN code lines to be counted.
9973 // expected: line 3 is blank → blank = 1.
9974 check_metrics::<RubyParser>("def foo\n a = 1\n\n a + 1\nend\n", "foo.rb", |metric| {
9975 assert_eq!(metric.loc.blank(), 1);
9976 });
9977 }
9978
9979 #[cfg(feature = "ruby")]
9980 #[test]
9981 fn ruby_no_zero_blank() {
9982 // Mirrors `rust_no_zero_blank`: the blank counter must stay
9983 // non-zero when blank lines sit between code lines that carry
9984 // trailing comments. Catches regressions in the SLOC −
9985 // (PLOC ∪ CLOC) union math when PLOC and CLOC line-sets
9986 // overlap.
9987 check_metrics::<RubyParser>(
9988 "def foo # entry\n pool = 0\n\n server = -42 # negative\n\n ok = false\nend\n",
9989 "foo.rb",
9990 |metric| {
9991 assert_eq!(metric.loc.blank(), 2);
9992 },
9993 );
9994 }
9995
9996 #[cfg(feature = "ruby")]
9997 #[test]
9998 fn ruby_cloc() {
9999 // 3 comment lines.
10000 check_metrics::<RubyParser>(
10001 "# one\n# two\n# three\ndef foo\nend\n",
10002 "foo.rb",
10003 |metric| {
10004 assert_eq!(metric.loc.cloc(), 3);
10005 },
10006 );
10007 }
10008
10009 #[cfg(feature = "ruby")]
10010 #[test]
10011 fn ruby_lloc() {
10012 // expected: 3 logical lines = `def` (Method) + `if` (If) +
10013 // `while` (While). Bare expression-statements (assignments,
10014 // calls) are intentionally NOT counted.
10015 check_metrics::<RubyParser>(
10016 "def foo(a)\n if a\n a += 1\n end\n while a > 0\n a -= 1\n end\nend\n",
10017 "foo.rb",
10018 |metric| {
10019 assert_eq!(metric.loc.lloc(), 3);
10020 },
10021 );
10022 }
10023
10024 #[cfg(feature = "ruby")]
10025 #[test]
10026 fn ruby_no_call_lloc() {
10027 // expected: 1 logical line (the surrounding `def`). The bare
10028 // method calls `puts 'hello'` and `puts 'world'` are
10029 // intentionally NOT counted — there is no expression_statement
10030 // wrapper to disambiguate them from sub-expressions.
10031 check_metrics::<RubyParser>(
10032 "def foo\n puts 'hello'\n puts 'world'\nend\n",
10033 "foo.rb",
10034 |metric| {
10035 assert_eq!(metric.loc.lloc(), 1);
10036 },
10037 );
10038 }
10039
10040 #[cfg(feature = "ruby")]
10041 #[test]
10042 fn ruby_no_assignment_lloc() {
10043 // Same rationale as `ruby_no_call_lloc`. expected: 1 lloc
10044 // (the `def`); raw assignments aren't counted.
10045 check_metrics::<RubyParser>(
10046 "def foo\n a = 1\n b = 2\n c = a + b\nend\n",
10047 "foo.rb",
10048 |metric| {
10049 assert_eq!(metric.loc.lloc(), 1);
10050 },
10051 );
10052 }
10053
10054 #[cfg(feature = "ruby")]
10055 #[test]
10056 fn ruby_modifier_lloc() {
10057 // Postfix modifier forms each count as one logical line. A
10058 // `return … if …` parses as an `IfModifier` wrapping a `Return`;
10059 // both fire the LLOC arm so the modifier line contributes +2.
10060 // expected: def(1) + if_modifier(1) + inner return(1)
10061 // + while_modifier(1) + rescue_modifier(1) = 5.
10062 check_metrics::<RubyParser>(
10063 "def foo(a)\n return a if a.nil?\n a -= 1 while a > 0\n parse(a) rescue nil\nend\n",
10064 "foo.rb",
10065 |metric| {
10066 assert_eq!(metric.loc.lloc(), 5);
10067 },
10068 );
10069 }
10070
10071 #[cfg(feature = "ruby")]
10072 #[test]
10073 fn ruby_class_lloc() {
10074 // expected: 1 class + 1 module + 2 methods = 4.
10075 check_metrics::<RubyParser>(
10076 "module M\n class C\n def foo\n end\n def bar\n end\n end\nend\n",
10077 "foo.rb",
10078 |metric| {
10079 assert_eq!(metric.loc.lloc(), 4);
10080 },
10081 );
10082 }
10083
10084 #[cfg(feature = "ruby")]
10085 #[test]
10086 fn ruby_begin_rescue_lloc() {
10087 // expected: 1 def + 1 begin = 2. Rescue clauses are part of
10088 // the begin construct and not separately counted; the bare
10089 // expression body lines are not statements.
10090 check_metrics::<RubyParser>(
10091 "def foo\n begin\n risky\n rescue StandardError\n nil\n end\nend\n",
10092 "foo.rb",
10093 |metric| {
10094 assert_eq!(metric.loc.lloc(), 2);
10095 },
10096 );
10097 }
10098
10099 #[cfg(feature = "ruby")]
10100 #[test]
10101 fn ruby_nested_defs_lloc() {
10102 // Each `Method` declaration contributes one logical line.
10103 // expected: outer `def` + inner `def` = 2.
10104 check_metrics::<RubyParser>(
10105 "def outer\n def inner\n 1\n end\nend\n",
10106 "foo.rb",
10107 |metric| {
10108 assert_eq!(metric.loc.lloc(), 2);
10109 },
10110 );
10111 }
10112
10113 #[cfg(feature = "ruby")]
10114 #[test]
10115 fn ruby_no_block_body_lloc() {
10116 // A top-level `[1,2,3].each do |x| puts x end` produces zero
10117 // logical lines: the surrounding `.each` is a `Call` (not in
10118 // the LLOC arm), the `DoBlock` is a closure (also not a
10119 // statement), and the `puts x` inside is another call. This
10120 // pins the documented expression-statement exclusion.
10121 check_metrics::<RubyParser>(
10122 "[1, 2, 3].each do |x|\n puts x\nend\n",
10123 "foo.rb",
10124 |metric| {
10125 assert_eq!(metric.loc.lloc(), 0);
10126 },
10127 );
10128 }
10129
10130 #[cfg(feature = "ruby")]
10131 #[test]
10132 fn ruby_no_lambda_body_lloc() {
10133 // `add = ->(a, b) { a + b }` produces zero logical lines for
10134 // the same reason as `ruby_no_block_body_lloc`: assignments,
10135 // calls, and lambda bodies are intentionally not statements
10136 // in this impl.
10137 check_metrics::<RubyParser>("add = ->(a, b) {\n a + b\n}\n", "foo.rb", |metric| {
10138 assert_eq!(metric.loc.lloc(), 0);
10139 });
10140 }
10141
10142 #[cfg(feature = "ruby")]
10143 #[test]
10144 fn ruby_heredoc_lloc_and_blank() {
10145 // A `<<~TXT` heredoc contributes: SLOC = every line in the file
10146 // (including the heredoc body); LLOC = just the surrounding `def`.
10147 // #778: the heredoc-body rows (`one`, `two`) hold real string text,
10148 // so they are credited to PLOC like Python's multi-line strings
10149 // (#415) rather than mislabelled as blank. Every row is now code.
10150 // expected: sloc = 7, ploc = 7, lloc = 1, blank = 0.
10151 check_metrics::<RubyParser>(
10152 "def foo\n msg = <<~TXT\n one\n two\n TXT\n msg\nend\n",
10153 "foo.rb",
10154 |metric| {
10155 assert_eq!(metric.loc.sloc(), 7);
10156 assert_eq!(metric.loc.ploc(), 7);
10157 assert_eq!(metric.loc.lloc(), 1);
10158 assert_eq!(metric.loc.blank(), 0);
10159 },
10160 );
10161 }
10162
10163 #[cfg(feature = "ruby")]
10164 #[test]
10165 fn ruby_semicolon_multistatement_lloc_undercount() {
10166 // Documented limitation: Ruby has no `expression_statement`
10167 // wrapper, so `;`-separated multi-statement lines collapse to
10168 // a single LLOC bump (the surrounding `def`). A future
10169 // statement-counter that walks BlockBody children would
10170 // change this — pin the current behaviour so the regression
10171 // is visible.
10172 check_metrics::<RubyParser>(
10173 "def foo\n a = 1; b = 2; a + b\nend\n",
10174 "foo.rb",
10175 |metric| {
10176 assert_eq!(metric.loc.lloc(), 1);
10177 },
10178 );
10179 }
10180
10181 #[cfg(feature = "ruby")]
10182 #[test]
10183 fn ruby_ploc_skips_comments_and_blanks() {
10184 // PLOC counts physical instruction lines: code-bearing lines
10185 // only. Comments and blanks are excluded.
10186 check_metrics::<RubyParser>("# header\n\ndef foo\n a = 1\nend\n", "foo.rb", |metric| {
10187 assert_eq!(metric.loc.ploc(), 3);
10188 assert_eq!(metric.loc.cloc(), 1);
10189 assert_eq!(metric.loc.blank(), 1);
10190 });
10191 }
10192
10193 // -----------------------------------------------------------------
10194 // Issue #195: nested-function/closure LLOC tests across languages.
10195 // Mirrors the prior art for Rust (`rust_function_in_loop_lloc`,
10196 // `rust_closure_expression_lloc`), Mozjs (`mozjs_nested_function_loc`),
10197 // Bash (`bash_nested_function_loc`), and TypeScript
10198 // (`typescript_nested_functions_loc`, `tsx_nested_functions_loc`).
10199 // -----------------------------------------------------------------
10200
10201 #[cfg(feature = "python")]
10202 #[test]
10203 fn python_nested_def_lloc() {
10204 // Nested `def`: the inner function declaration plus the outer
10205 // body's `return inner()` are both LLOC; the outer `def` header
10206 // and the inner `return 1` belong to their own function spaces.
10207 check_metrics::<PythonParser>(
10208 "def outer():\n def inner():\n return 1\n return inner()\n",
10209 "foo.py",
10210 |metric| {
10211 assert_eq!(metric.loc.sloc(), 4);
10212 assert_eq!(metric.loc.ploc(), 4);
10213 assert_eq!(metric.loc.lloc(), 2);
10214 assert_eq!(metric.loc.cloc(), 0);
10215 assert_eq!(metric.loc.blank(), 0);
10216 insta::assert_json_snapshot!(metric.loc);
10217 },
10218 );
10219 }
10220
10221 #[cfg(feature = "python")]
10222 #[test]
10223 fn python_lambda_in_def_lloc() {
10224 // `lambda x: x + 1` is an expression, not a Python `function_definition`,
10225 // so it does not start a new function space. The two LLOC come from
10226 // the assignment `f = lambda ...` and the `return f(2)` statement.
10227 check_metrics::<PythonParser>(
10228 "def outer():\n f = lambda x: x + 1\n return f(2)\n",
10229 "foo.py",
10230 |metric| {
10231 assert_eq!(metric.loc.sloc(), 3);
10232 assert_eq!(metric.loc.ploc(), 3);
10233 assert_eq!(metric.loc.lloc(), 2);
10234 assert_eq!(metric.loc.cloc(), 0);
10235 assert_eq!(metric.loc.blank(), 0);
10236 insta::assert_json_snapshot!(metric.loc);
10237 },
10238 );
10239 }
10240
10241 #[cfg(feature = "python")]
10242 #[test]
10243 fn python_match_statement_lloc() {
10244 // `match` (PEP 634) is a control-flow statement that must add one
10245 // LLOC like `if`/`try`, plus each `return` in a case body (#462).
10246 // Its `case_clause` children add nothing, mirroring how
10247 // `elif_clause`/`else_clause` are absent from the LLOC arm: the
10248 // construct counts once and the statements inside count via their
10249 // own arms. Here: match(1) + two `return`s = 3.
10250 check_metrics::<PythonParser>(
10251 "def f(x):\n match x:\n case 1: return 1\n case _: return 0\n",
10252 "foo.py",
10253 |metric| {
10254 assert_eq!(metric.loc.lloc(), 3);
10255 insta::assert_json_snapshot!(metric.loc);
10256 },
10257 );
10258 }
10259
10260 #[cfg(feature = "python")]
10261 #[test]
10262 fn python_match_lloc_matches_if_else() {
10263 // Parity with the equivalent two-branch `if`/`else`: both have the
10264 // construct keyword (1) plus two `return` bodies (2), so each must
10265 // report an identical LLOC of 3. The match form previously
10266 // undercounted (2) because `match_statement` was absent from the
10267 // LLOC arm (#462). `check_metrics` takes a non-capturing `fn`, so
10268 // the shared expectation is pinned to the same literal in both
10269 // closures rather than threaded through a captured variable.
10270 check_metrics::<PythonParser>(
10271 "def f(x):\n if x == 1: return 1\n else: return 0\n",
10272 "foo.py",
10273 |metric| assert_eq!(metric.loc.lloc(), 3),
10274 );
10275 check_metrics::<PythonParser>(
10276 "def f(x):\n match x:\n case 1: return 1\n case _: return 0\n",
10277 "foo.py",
10278 |metric| assert_eq!(metric.loc.lloc(), 3),
10279 );
10280 }
10281
10282 #[cfg(feature = "python")]
10283 #[test]
10284 fn python_type_alias_lloc() {
10285 // A `type` alias (PEP 695) is a leaf statement, counted like an
10286 // assignment. `type Alias = int` followed by `x = 1` is two LLOC;
10287 // before #462 the alias fell through to the `_` arm and the file
10288 // reported only 1.
10289 check_metrics::<PythonParser>("type Alias = int\nx = 1\n", "foo.py", |metric| {
10290 assert_eq!(metric.loc.lloc(), 2);
10291 insta::assert_json_snapshot!(metric.loc);
10292 });
10293 }
10294
10295 #[cfg(feature = "java")]
10296 #[test]
10297 fn java_local_class_in_method_lloc() {
10298 // A `class` declared inside a method body produces its own function
10299 // space, so the outer method's LLOC only sees `return new Local().v();`
10300 // and the body of `v()` contributes the second LLOC.
10301 check_metrics::<JavaParser>(
10302 "class Foo {\n int bar() {\n class Local {\n int v() { return 1; }\n }\n return new Local().v();\n }\n}\n",
10303 "foo.java",
10304 |metric| {
10305 assert_eq!(metric.loc.sloc(), 8);
10306 assert_eq!(metric.loc.ploc(), 8);
10307 assert_eq!(metric.loc.lloc(), 2);
10308 assert_eq!(metric.loc.cloc(), 0);
10309 assert_eq!(metric.loc.blank(), 0);
10310 insta::assert_json_snapshot!(metric.loc);
10311 },
10312 );
10313 }
10314
10315 #[cfg(feature = "java")]
10316 #[test]
10317 fn java_lambda_in_method_lloc() {
10318 // Java lambdas are expressions; the two LLOC come from the
10319 // `IntUnaryOperator f = x -> x + 1;` declaration and the
10320 // `f.applyAsInt(3);` expression statement.
10321 check_metrics::<JavaParser>(
10322 "class Foo {\n void bar() {\n java.util.function.IntUnaryOperator f = x -> x + 1;\n f.applyAsInt(3);\n }\n}\n",
10323 "foo.java",
10324 |metric| {
10325 assert_eq!(metric.loc.sloc(), 6);
10326 assert_eq!(metric.loc.ploc(), 6);
10327 assert_eq!(metric.loc.lloc(), 2);
10328 assert_eq!(metric.loc.cloc(), 0);
10329 assert_eq!(metric.loc.blank(), 0);
10330 insta::assert_json_snapshot!(metric.loc);
10331 },
10332 );
10333 }
10334
10335 #[cfg(feature = "groovy")]
10336 #[test]
10337 fn groovy_blank() {
10338 // Blank lines + simple statements. Newlines act as the
10339 // statement terminator; PLOC counts the two declaration lines.
10340 check_metrics::<GroovyParser>("int x = 1\n\n\nint y = 2", "foo.groovy", |metric| {
10341 assert_eq!(metric.loc.sloc(), 4);
10342 assert_eq!(metric.loc.ploc(), 2);
10343 assert_eq!(metric.loc.lloc(), 2);
10344 assert_eq!(metric.loc.blank(), 2);
10345 });
10346 }
10347
10348 #[cfg(feature = "groovy")]
10349 #[test]
10350 fn groovy_no_zero_blank() {
10351 // A single line with no blanks: blank() == 0.
10352 check_metrics::<GroovyParser>("int x = 1", "foo.groovy", |metric| {
10353 assert_eq!(metric.loc.sloc(), 1);
10354 assert_eq!(metric.loc.blank(), 0);
10355 });
10356 }
10357
10358 #[cfg(feature = "groovy")]
10359 #[test]
10360 fn groovy_cloc_line_comments() {
10361 check_metrics::<GroovyParser>(
10362 "// first comment
10363 int x = 1
10364 // second comment
10365 int y = 2",
10366 "foo.groovy",
10367 |metric| {
10368 assert_eq!(metric.loc.cloc(), 2);
10369 assert_eq!(metric.loc.ploc(), 2);
10370 },
10371 );
10372 }
10373
10374 #[cfg(feature = "groovy")]
10375 #[test]
10376 fn groovy_cloc_block_comment() {
10377 check_metrics::<GroovyParser>(
10378 "/* multi
10379 line
10380 comment */
10381 int x = 1",
10382 "foo.groovy",
10383 |metric| {
10384 // Block comment spans 3 lines → cloc == 3.
10385 assert_eq!(metric.loc.cloc(), 3);
10386 },
10387 );
10388 }
10389
10390 #[cfg(feature = "groovy")]
10391 #[test]
10392 fn groovy_cloc_groovydoc_comment() {
10393 // Groovy `/** … */` `groovydoc_comment` counts as CLOC. The
10394 // `Loc` arm already handled it; this pins it alongside the
10395 // restored `is_comment` parity (#697).
10396 check_metrics::<GroovyParser>(
10397 "/** groovydoc */
10398class A {
10399 int x = 1
10400}",
10401 "foo.groovy",
10402 |metric| {
10403 assert_eq!(metric.loc.cloc(), 1);
10404 assert_eq!(metric.loc.ploc(), 3);
10405 },
10406 );
10407 }
10408
10409 #[cfg(feature = "groovy")]
10410 #[test]
10411 fn groovy_simple_lloc() {
10412 // One LLOC per simple expression statement.
10413 check_metrics::<GroovyParser>(
10414 "int a = 1
10415 int b = 2
10416 int c = 3",
10417 "foo.groovy",
10418 |metric| {
10419 assert_eq!(metric.loc.lloc(), 3);
10420 },
10421 );
10422 }
10423
10424 #[cfg(feature = "groovy")]
10425 #[test]
10426 fn groovy_no_local_variable_declaration_in_for_lloc() {
10427 // The variable declaration inside a classic `for` init slot
10428 // does NOT count as an LLOC (it's an expression part of the
10429 // for-loop). Same gating as Java's `java_for_lloc`.
10430 check_metrics::<GroovyParser>(
10431 "for (int i = 0; i < 10; i++) {
10432 println(i)
10433 }",
10434 "foo.groovy",
10435 |metric| {
10436 // for-statement (1) + expression-statement `println(i)` (1) = 2
10437 assert_eq!(metric.loc.lloc(), 2);
10438 },
10439 );
10440 }
10441
10442 #[cfg(feature = "groovy")]
10443 #[test]
10444 fn groovy_lambda_in_method_lloc() {
10445 // Closures contain a statement list — the dekobon grammar wraps
10446 // a single-expression body in `expression_statement` rather than
10447 // emitting the expression directly (as Java's `lambda_expression`
10448 // does), so a one-line closure body counts as its own LLOC.
10449 // Declaration `def f = …` (1) + closure body `x + 1` (1) +
10450 // call `f(3)` (1) = 3.
10451 check_metrics::<GroovyParser>(
10452 "class Foo {
10453 void bar() {
10454 def f = { x -> x + 1 }
10455 f(3)
10456 }
10457 }",
10458 "foo.groovy",
10459 |metric| {
10460 assert_eq!(metric.loc.lloc(), 3);
10461 },
10462 );
10463 }
10464
10465 #[cfg(feature = "groovy")]
10466 #[test]
10467 fn groovy_try_lloc() {
10468 // try-statement counts as one LLOC; the catch body's
10469 // statements count separately.
10470 check_metrics::<GroovyParser>(
10471 "void f() {
10472 try {
10473 risky()
10474 } catch (Exception e) {
10475 handle(e)
10476 }
10477 }",
10478 "foo.groovy",
10479 |metric| {
10480 // try(1) + risky() expr-stmt(1) + handle() expr-stmt(1) = 3
10481 assert_eq!(metric.loc.lloc(), 3);
10482 },
10483 );
10484 }
10485
10486 #[cfg(feature = "groovy")]
10487 #[test]
10488 fn groovy_class_loc() {
10489 // Source-file-level totals across multiple methods.
10490 check_metrics::<GroovyParser>(
10491 "class A {
10492 void f() {
10493 int x = 1
10494 }
10495 void g() {
10496 int y = 2
10497 }
10498 }",
10499 "foo.groovy",
10500 |metric| {
10501 // 8 lines of non-comment content: `class A {`, two
10502 // `void` headers, two `int … = …` body statements,
10503 // three closing braces.
10504 assert_eq!(metric.loc.ploc(), 8);
10505 assert_eq!(metric.loc.cloc(), 0);
10506 // Two expression-statement LLOCs (`int x = 1`,
10507 // `int y = 2`).
10508 assert_eq!(metric.loc.lloc(), 2);
10509 },
10510 );
10511 }
10512
10513 #[cfg(feature = "groovy")]
10514 #[test]
10515 fn groovy_partial_parse_recovers_unit() {
10516 // Malformed input parses with ERROR but still emits a Unit
10517 // root via `spaces.rs` fallback (lesson 9). The single
10518 // source line is counted as SLOC even when the parse fails
10519 // mid-expression.
10520 check_metrics::<GroovyParser>("def x = (((", "foo.groovy", |metric| {
10521 assert_eq!(metric.loc.sloc(), 1);
10522 assert_eq!(metric.loc.blank(), 0);
10523 });
10524 }
10525
10526 #[cfg(feature = "groovy")]
10527 #[test]
10528 fn groovy_sloc() {
10529 // Mirrors `java_sloc`: basic per-line count across a mix of
10530 // statements and a blank line.
10531 check_metrics::<GroovyParser>(
10532 "int a = 1
10533 int b = 2
10534
10535 int c = 3",
10536 "foo.groovy",
10537 |metric| {
10538 assert_eq!(metric.loc.sloc(), 4);
10539 assert_eq!(metric.loc.ploc(), 3);
10540 assert_eq!(metric.loc.blank(), 1);
10541 },
10542 );
10543 }
10544
10545 #[cfg(feature = "groovy")]
10546 #[test]
10547 fn groovy_single_ploc() {
10548 // Mirrors `java_single_ploc`: one non-blank, non-comment
10549 // line of code => ploc == 1.
10550 check_metrics::<GroovyParser>("int x = 42", "foo.groovy", |metric| {
10551 assert_eq!(metric.loc.ploc(), 1);
10552 assert_eq!(metric.loc.cloc(), 0);
10553 });
10554 }
10555
10556 #[cfg(feature = "groovy")]
10557 #[test]
10558 fn groovy_multi_ploc() {
10559 // Multiple statements on separate lines all contribute to
10560 // PLOC. Mirrors `java_multi_ploc`.
10561 check_metrics::<GroovyParser>(
10562 "int a = 1
10563 int b = 2
10564 int c = 3
10565 int d = 4",
10566 "foo.groovy",
10567 |metric| {
10568 assert_eq!(metric.loc.ploc(), 4);
10569 assert_eq!(metric.loc.lloc(), 4);
10570 },
10571 );
10572 }
10573
10574 #[cfg(feature = "groovy")]
10575 #[test]
10576 fn groovy_single_statement_lloc() {
10577 // A single expression statement contributes one LLOC.
10578 // Mirrors `java_single_statement_lloc`.
10579 check_metrics::<GroovyParser>("println 'hi'", "foo.groovy", |metric| {
10580 assert_eq!(metric.loc.lloc(), 1);
10581 });
10582 }
10583
10584 #[cfg(feature = "groovy")]
10585 #[test]
10586 fn groovy_for_lloc() {
10587 // The classical `for` statement itself counts as one LLOC;
10588 // the body's `println(i)` adds another. The init-slot
10589 // var-decl is suppressed by the LocalVariableDeclaration
10590 // ancestor-check (same rule as `java_for_lloc`).
10591 check_metrics::<GroovyParser>(
10592 "for (int i = 0; i < 100; i++) {
10593 println(i)
10594 }",
10595 "foo.groovy",
10596 |metric| {
10597 // ForStatement(1) + println-expr(1) = 2
10598 assert_eq!(metric.loc.lloc(), 2);
10599 },
10600 );
10601 }
10602
10603 #[cfg(feature = "groovy")]
10604 #[test]
10605 fn groovy_foreach_lloc() {
10606 // `for (item in list)` parses as `enhanced_for_statement` —
10607 // counts as one LLOC.
10608 check_metrics::<GroovyParser>(
10609 "for (item in items) {
10610 println(item)
10611 }",
10612 "foo.groovy",
10613 |metric| {
10614 // EnhancedForStatement(1) + println(1) = 2
10615 assert_eq!(metric.loc.lloc(), 2);
10616 },
10617 );
10618 }
10619
10620 #[cfg(feature = "groovy")]
10621 #[test]
10622 fn groovy_while_lloc() {
10623 // `while` itself is one LLOC; each body statement adds
10624 // another. Mirrors `java_while_lloc`.
10625 check_metrics::<GroovyParser>(
10626 "int i = 0
10627 while (i < 10) {
10628 i++
10629 println(i)
10630 }",
10631 "foo.groovy",
10632 |metric| {
10633 // int i = 0 (1) + while (1) + i++ (1) + println (1) = 4
10634 assert_eq!(metric.loc.lloc(), 4);
10635 },
10636 );
10637 }
10638
10639 #[cfg(feature = "groovy")]
10640 #[test]
10641 fn groovy_do_while_lloc() {
10642 // `do…while` is one LLOC plus its body. Mirrors
10643 // `java_do_while_lloc`.
10644 check_metrics::<GroovyParser>(
10645 "int i = 0
10646 do {
10647 i++
10648 } while (i < 5)",
10649 "foo.groovy",
10650 |metric| {
10651 // int i = 0 (1) + do (1) + i++ (1) = 3
10652 assert_eq!(metric.loc.lloc(), 3);
10653 },
10654 );
10655 }
10656
10657 #[cfg(feature = "groovy")]
10658 #[test]
10659 fn groovy_continue_lloc() {
10660 // `continue` is an LLOC. Same gating as `java_continue_lloc`.
10661 check_metrics::<GroovyParser>(
10662 "for (int i = 0; i < 10; i++) {
10663 if (i == 5) {
10664 continue
10665 }
10666 println(i)
10667 }",
10668 "foo.groovy",
10669 |metric| {
10670 // for(1) + if(1) + continue(1) + println(1) = 4
10671 assert_eq!(metric.loc.lloc(), 4);
10672 },
10673 );
10674 }
10675
10676 #[cfg(feature = "groovy")]
10677 #[test]
10678 fn groovy_expressions_lloc() {
10679 // A bag of expression statements: each independent
10680 // expr-stmt is one LLOC. Mirrors `java_expressions_lloc`.
10681 check_metrics::<GroovyParser>(
10682 "int a = 1
10683 a = 2
10684 a += 3
10685 println(a)
10686 doSomething()",
10687 "foo.groovy",
10688 |metric| {
10689 // 5 expression-statement lines.
10690 assert_eq!(metric.loc.lloc(), 5);
10691 },
10692 );
10693 }
10694
10695 #[cfg(feature = "groovy")]
10696 #[test]
10697 fn groovy_throw_lloc() {
10698 // `throw` is one LLOC via the `ThrowStatement` arm.
10699 check_metrics::<GroovyParser>(
10700 "throw new RuntimeException('bad')",
10701 "foo.groovy",
10702 |metric| {
10703 assert_eq!(metric.loc.lloc(), 1);
10704 },
10705 );
10706 }
10707
10708 #[cfg(feature = "groovy")]
10709 #[test]
10710 fn groovy_general_loc() {
10711 // Comprehensive mix: class + method + control flow.
10712 // Mirrors `java_general_loc`'s coverage shape.
10713 //
10714 // LLOC = 4, fully attributable:
10715 // IfStatement (the outer if/else): +1
10716 // `println(x)` (JuxtFunctionCall): +1
10717 // `println 'neg'` (JuxtFunctionCall): +1
10718 // `return` (ReturnStatement): +1
10719 // The else-branch's `expression_statement (closure)`
10720 // wrapper does NOT count — see the bare-Closure carve-out
10721 // in `impl Loc for GroovyCode::compute`.
10722 check_metrics::<GroovyParser>(
10723 "class A {
10724 void f(int x) {
10725 if (x > 0) {
10726 println(x)
10727 } else {
10728 println 'neg'
10729 }
10730 return
10731 }
10732 }",
10733 "foo.groovy",
10734 |metric| {
10735 assert_eq!(metric.loc.lloc(), 4);
10736 assert_eq!(metric.loc.cloc(), 0);
10737 },
10738 );
10739 }
10740
10741 #[cfg(feature = "csharp")]
10742 #[test]
10743 fn csharp_local_function_in_method_lloc() {
10744 // C# local functions (`int Inner(int x) { ... }` inside `Bar()`)
10745 // open their own function space, so the outer method sees only
10746 // `return Inner(2);` plus the inner body's `return x + 1;`.
10747 check_metrics::<CsharpParser>(
10748 "class Foo {\n int Bar() {\n int Inner(int x) { return x + 1; }\n return Inner(2);\n }\n}\n",
10749 "foo.cs",
10750 |metric| {
10751 assert_eq!(metric.loc.sloc(), 6);
10752 assert_eq!(metric.loc.ploc(), 6);
10753 assert_eq!(metric.loc.lloc(), 2);
10754 assert_eq!(metric.loc.cloc(), 0);
10755 assert_eq!(metric.loc.blank(), 0);
10756 insta::assert_json_snapshot!(metric.loc);
10757 },
10758 );
10759 }
10760
10761 #[cfg(feature = "csharp")]
10762 #[test]
10763 fn csharp_lambda_in_method_lloc() {
10764 // C# lambdas are expressions: the two LLOC come from the
10765 // `Func<int,int> f = x => x + 1;` declaration and the `f(3);` call.
10766 check_metrics::<CsharpParser>(
10767 "class Foo {\n void Bar() {\n System.Func<int, int> f = x => x + 1;\n f(3);\n }\n}\n",
10768 "foo.cs",
10769 |metric| {
10770 assert_eq!(metric.loc.sloc(), 6);
10771 assert_eq!(metric.loc.ploc(), 6);
10772 assert_eq!(metric.loc.lloc(), 2);
10773 assert_eq!(metric.loc.cloc(), 0);
10774 assert_eq!(metric.loc.blank(), 0);
10775 insta::assert_json_snapshot!(metric.loc);
10776 },
10777 );
10778 }
10779
10780 #[cfg(feature = "cpp")]
10781 #[test]
10782 fn cpp_lambda_in_function_lloc() {
10783 // C++11 lambdas are expressions. The outer function `bar()` produces
10784 // two LLOC for the body: `auto f = [](int x) { return x + 1; };` and
10785 // `return f(2);`. The lambda's inner `return x + 1;` is part of the
10786 // lambda body inside the same function space (lambdas do not open a
10787 // new FuncSpace in this implementation), so it adds a third LLOC.
10788 // Closes the parity gap with #195 (which covered 11 other
10789 // languages but omitted C++).
10790 check_metrics::<CppParser>(
10791 "int bar() {\n auto f = [](int x) { return x + 1; };\n return f(2);\n}\n",
10792 "foo.cpp",
10793 |metric| {
10794 assert_eq!(metric.loc.sloc(), 4);
10795 assert_eq!(metric.loc.ploc(), 4);
10796 assert_eq!(metric.loc.lloc(), 3);
10797 assert_eq!(metric.loc.cloc(), 0);
10798 assert_eq!(metric.loc.blank(), 0);
10799 insta::assert_json_snapshot!(metric.loc);
10800 },
10801 );
10802 }
10803
10804 #[cfg(feature = "javascript")]
10805 #[test]
10806 fn javascript_nested_function_lloc() {
10807 // Nested function_declaration: 4 LLOC = outer's `return inner();`,
10808 // inner's `return 1;`, plus the two function declarations
10809 // themselves (the JS Checker counts function declarations as LLOC).
10810 check_metrics::<JavascriptParser>(
10811 "function outer() {\n function inner() {\n return 1;\n }\n return inner();\n}\n",
10812 "foo.js",
10813 |metric| {
10814 assert_eq!(metric.loc.sloc(), 6);
10815 assert_eq!(metric.loc.ploc(), 6);
10816 assert_eq!(metric.loc.lloc(), 2);
10817 assert_eq!(metric.loc.cloc(), 0);
10818 assert_eq!(metric.loc.blank(), 0);
10819 insta::assert_json_snapshot!(metric.loc);
10820 },
10821 );
10822 }
10823
10824 #[cfg(feature = "javascript")]
10825 #[test]
10826 fn javascript_arrow_function_lloc() {
10827 // The arrow function `(x) => x + 1` is an expression: the LLOC
10828 // come from `const inner = ...;` and `return inner(2);`.
10829 check_metrics::<JavascriptParser>(
10830 "function outer() {\n const inner = (x) => x + 1;\n return inner(2);\n}\n",
10831 "foo.js",
10832 |metric| {
10833 assert_eq!(metric.loc.sloc(), 4);
10834 assert_eq!(metric.loc.ploc(), 4);
10835 assert_eq!(metric.loc.lloc(), 2);
10836 assert_eq!(metric.loc.cloc(), 0);
10837 assert_eq!(metric.loc.blank(), 0);
10838 insta::assert_json_snapshot!(metric.loc);
10839 },
10840 );
10841 }
10842
10843 #[cfg(feature = "kotlin")]
10844 #[test]
10845 fn kotlin_lambda_literal_in_fun_lloc() {
10846 // A lambda literal (`{ x -> x + 1 }`) assigned to a `val` plus the
10847 // following call yields two LLOC at the outer function.
10848 check_metrics::<KotlinParser>(
10849 "fun outer() {\n val f: (Int) -> Int = { x -> x + 1 }\n f(3)\n}\n",
10850 "foo.kt",
10851 |metric| {
10852 assert_eq!(metric.loc.sloc(), 4);
10853 assert_eq!(metric.loc.ploc(), 4);
10854 assert_eq!(metric.loc.lloc(), 2);
10855 assert_eq!(metric.loc.cloc(), 0);
10856 assert_eq!(metric.loc.blank(), 0);
10857 insta::assert_json_snapshot!(metric.loc);
10858 },
10859 );
10860 }
10861
10862 #[cfg(feature = "kotlin")]
10863 #[test]
10864 fn kotlin_local_fun_in_fun_lloc() {
10865 // Kotlin's local `fun inner(...)` is also a function_declaration,
10866 // so it opens its own space; the outer LLOC reduces to `inner(3)`,
10867 // and the inner body contributes the second LLOC.
10868 check_metrics::<KotlinParser>(
10869 "fun outer() {\n fun inner(x: Int): Int { return x + 1 }\n inner(3)\n}\n",
10870 "foo.kt",
10871 |metric| {
10872 assert_eq!(metric.loc.sloc(), 4);
10873 assert_eq!(metric.loc.ploc(), 4);
10874 assert_eq!(metric.loc.lloc(), 2);
10875 assert_eq!(metric.loc.cloc(), 0);
10876 assert_eq!(metric.loc.blank(), 0);
10877 insta::assert_json_snapshot!(metric.loc);
10878 },
10879 );
10880 }
10881
10882 #[cfg(feature = "kotlin")]
10883 #[test]
10884 fn kotlin_object_expression_in_fun_lloc() {
10885 // An `object : Runnable { ... }` expression with an overridden
10886 // method whose body invokes `println("hi")`. LLOC: `val r = ...`,
10887 // the override's body call, and the outer `r.run()` call = 3.
10888 check_metrics::<KotlinParser>(
10889 "fun outer() {\n val r = object : Runnable { override fun run() { println(\"hi\") } }\n r.run()\n}\n",
10890 "foo.kt",
10891 |metric| {
10892 assert_eq!(metric.loc.sloc(), 4);
10893 assert_eq!(metric.loc.ploc(), 4);
10894 assert_eq!(metric.loc.lloc(), 3);
10895 assert_eq!(metric.loc.cloc(), 0);
10896 assert_eq!(metric.loc.blank(), 0);
10897 insta::assert_json_snapshot!(metric.loc);
10898 },
10899 );
10900 }
10901
10902 #[cfg(feature = "go")]
10903 #[test]
10904 fn go_function_literal_initializer_lloc() {
10905 // `inner := func(x int) int { return x + 1 }` — the function
10906 // literal opens its own space; LLOC visible on the outer space:
10907 // the assignment + `return inner(2)` = 2, plus the literal's
10908 // `return x + 1` body = 3 aggregated.
10909 check_metrics::<GoParser>(
10910 "package main\nfunc outer() int {\n inner := func(x int) int { return x + 1 }\n return inner(2)\n}\n",
10911 "foo.go",
10912 |metric| {
10913 assert_eq!(metric.loc.sloc(), 5);
10914 assert_eq!(metric.loc.ploc(), 5);
10915 assert_eq!(metric.loc.lloc(), 3);
10916 assert_eq!(metric.loc.cloc(), 0);
10917 assert_eq!(metric.loc.blank(), 0);
10918 insta::assert_json_snapshot!(metric.loc);
10919 },
10920 );
10921 }
10922
10923 #[cfg(feature = "php")]
10924 #[test]
10925 fn php_anonymous_function_in_function_lloc() {
10926 // Anonymous function `function ($x) { return $x + 1; }`: outer
10927 // sees the assignment + `return $f(2);`, the closure body adds
10928 // `return $x + 1;` for 3 LLOC aggregated.
10929 check_metrics::<PhpParser>(
10930 "<?php\nfunction outer() {\n $f = function ($x) { return $x + 1; };\n return $f(2);\n}\n",
10931 "foo.php",
10932 |metric| {
10933 assert_eq!(metric.loc.sloc(), 5);
10934 assert_eq!(metric.loc.ploc(), 5);
10935 assert_eq!(metric.loc.lloc(), 3);
10936 assert_eq!(metric.loc.cloc(), 0);
10937 assert_eq!(metric.loc.blank(), 0);
10938 insta::assert_json_snapshot!(metric.loc);
10939 },
10940 );
10941 }
10942
10943 #[cfg(feature = "php")]
10944 #[test]
10945 fn php_arrow_function_in_function_lloc() {
10946 // The `fn ($x) => $x + 1` arrow function is an expression; the
10947 // outer function sees only its assignment and the `return $f(2);`.
10948 check_metrics::<PhpParser>(
10949 "<?php\nfunction outer() {\n $f = fn ($x) => $x + 1;\n return $f(2);\n}\n",
10950 "foo.php",
10951 |metric| {
10952 assert_eq!(metric.loc.sloc(), 5);
10953 assert_eq!(metric.loc.ploc(), 5);
10954 assert_eq!(metric.loc.lloc(), 2);
10955 assert_eq!(metric.loc.cloc(), 0);
10956 assert_eq!(metric.loc.blank(), 0);
10957 insta::assert_json_snapshot!(metric.loc);
10958 },
10959 );
10960 }
10961
10962 #[cfg(feature = "lua")]
10963 #[test]
10964 fn lua_nested_local_function_lloc() {
10965 // Two nested `local function` declarations: outer + inner both
10966 // count as `function_declaration` LLOC, plus the two `return`
10967 // statements = 4 aggregated.
10968 check_metrics::<LuaParser>(
10969 "local function outer()\n local function inner()\n return 1\n end\n return inner()\nend\n",
10970 "foo.lua",
10971 |metric| {
10972 assert_eq!(metric.loc.sloc(), 6);
10973 assert_eq!(metric.loc.ploc(), 6);
10974 assert_eq!(metric.loc.lloc(), 4);
10975 assert_eq!(metric.loc.cloc(), 0);
10976 assert_eq!(metric.loc.blank(), 0);
10977 insta::assert_json_snapshot!(metric.loc);
10978 },
10979 );
10980 }
10981
10982 #[cfg(feature = "lua")]
10983 #[test]
10984 fn lua_function_expression_in_local_decl_lloc() {
10985 // `local f = function (x) return x + 1 end` — the function
10986 // expression is its own space; aggregated LLOC: outer
10987 // declaration, the inner expression's declaration, the inner
10988 // `return x + 1`, and the outer `return f(2)` = 4.
10989 check_metrics::<LuaParser>(
10990 "local function outer()\n local f = function (x) return x + 1 end\n return f(2)\nend\n",
10991 "foo.lua",
10992 |metric| {
10993 assert_eq!(metric.loc.sloc(), 4);
10994 assert_eq!(metric.loc.ploc(), 4);
10995 assert_eq!(metric.loc.lloc(), 4);
10996 assert_eq!(metric.loc.cloc(), 0);
10997 assert_eq!(metric.loc.blank(), 0);
10998 insta::assert_json_snapshot!(metric.loc);
10999 },
11000 );
11001 }
11002
11003 #[cfg(feature = "tcl")]
11004 #[test]
11005 fn tcl_apply_closure_lloc() {
11006 // `apply $f 2` is a regular Tcl command, not a separate function
11007 // space — tree-sitter-tcl does not model `apply { ... }` as a
11008 // closure construct distinct from any other command. We assert
11009 // the observed LLOC (proc, set, apply, plus the nested `expr`
11010 // command substitution inside the lambda body) so any future
11011 // change to lambda-body counting is caught here.
11012 check_metrics::<TclParser>(
11013 "proc outer {} {\n set f [list x {return [expr {$x + 1}]}]\n apply $f 2\n}\n",
11014 "foo.tcl",
11015 |metric| {
11016 assert_eq!(metric.loc.sloc(), 4);
11017 assert_eq!(metric.loc.ploc(), 4);
11018 assert_eq!(metric.loc.lloc(), 4);
11019 assert_eq!(metric.loc.cloc(), 0);
11020 assert_eq!(metric.loc.blank(), 0);
11021 insta::assert_json_snapshot!(metric.loc);
11022 },
11023 );
11024 }
11025
11026 #[cfg(feature = "perl")]
11027 #[test]
11028 fn perl_anonymous_sub_in_sub_lloc() {
11029 // Anonymous sub `sub { ... }` opens its own function space; the
11030 // outer LLOC counts the `my $f = ...;` declaration plus
11031 // `return $f->(2);`, and the anonymous sub contributes
11032 // `return $_[0] + 1;` for 2 LLOC.
11033 //
11034 // NOTE: a prior LLOC for this construct exists as
11035 // `perl_lloc_anonymous_function` (top-level form) — this test
11036 // asserts the same shape *inside* another sub, exercising space
11037 // nesting.
11038 check_metrics::<PerlParser>(
11039 "sub outer {\n my $f = sub { return $_[0] + 1 };\n return $f->(2);\n}\n",
11040 "foo.pl",
11041 |metric| {
11042 assert_eq!(metric.loc.sloc(), 4);
11043 assert_eq!(metric.loc.ploc(), 4);
11044 assert_eq!(metric.loc.lloc(), 2);
11045 assert_eq!(metric.loc.cloc(), 0);
11046 assert_eq!(metric.loc.blank(), 0);
11047 insta::assert_json_snapshot!(metric.loc);
11048 },
11049 );
11050 }
11051
11052 #[cfg(feature = "perl")]
11053 #[test]
11054 fn perl_named_sub_in_sub_lloc() {
11055 // Perl `sub` declarations are not LLOC (see
11056 // `perl_lloc_function_definition_not_counted`); inside `outer`,
11057 // only `return inner();` is LLOC, and `inner`'s `return 1` is in
11058 // its own space contributing one more aggregated LLOC.
11059 // Total aggregated LLOC: 1.
11060 //
11061 // Observation: lloc=1, not 2. Perl LLOC is anchored on `;`
11062 // tokens whose parent is `SourceFile` or `Block` (see
11063 // `PerlCode::compute` in this file). The bare `return 1` inside
11064 // `sub inner { ... }` has no trailing `;`, so it does not bump
11065 // LLOC. The outer `return inner();` carries the only SEMI.
11066 // This is intentional Perl behaviour and not a bug — Perl
11067 // requires `;` between statements; a single trailing statement
11068 // before `}` is syntactically optional. Asserted as-is.
11069 check_metrics::<PerlParser>(
11070 "sub outer {\n sub inner { return 1 }\n return inner();\n}\n",
11071 "foo.pl",
11072 |metric| {
11073 assert_eq!(metric.loc.sloc(), 4);
11074 assert_eq!(metric.loc.ploc(), 4);
11075 assert_eq!(metric.loc.lloc(), 1);
11076 assert_eq!(metric.loc.cloc(), 0);
11077 assert_eq!(metric.loc.blank(), 0);
11078 insta::assert_json_snapshot!(metric.loc);
11079 },
11080 );
11081 }
11082
11083 #[cfg(feature = "elixir")]
11084 #[test]
11085 fn elixir_fn_inside_def_lloc() {
11086 // `fn x -> x + 1 end` inside a `def`: defmodule + def +
11087 // `f = fn ...` + `f.(2)` = 4 own LLOC for the Unit space, plus
11088 // the anonymous fn body `x + 1` = 1 nested, aggregated 5.
11089 check_metrics::<ElixirParser>(
11090 "defmodule Foo do\n def outer do\n f = fn x -> x + 1 end\n f.(2)\n end\nend\n",
11091 "foo.ex",
11092 |metric| {
11093 assert_eq!(metric.loc.sloc(), 6);
11094 assert_eq!(metric.loc.ploc(), 6);
11095 assert_eq!(metric.loc.lloc(), 5);
11096 assert_eq!(metric.loc.cloc(), 0);
11097 assert_eq!(metric.loc.blank(), 0);
11098 insta::assert_json_snapshot!(metric.loc);
11099 },
11100 );
11101 }
11102
11103 /// Regression for #437: `Loc` min/max must fold each nested function
11104 /// space's *own* min/max, not its aggregate value, so the smallest
11105 /// (and largest) leaf function propagates to the root. Before the fix,
11106 /// merge folded `other.sloc()` and a guarded `compute_minmax` skipped
11107 /// containers, so `sloc_min` reflected only top-level spaces.
11108 ///
11109 /// Layout: Unit (whole file) -> class C -> two methods of *different*
11110 /// sizes. The smaller method is the global minimum; the file/class
11111 /// spans are larger; `sloc_min` must be the small leaf, not the class
11112 /// or unit span. Verified against the pre-fix code by reverting the
11113 /// merge/compute_minmax change (it reports the unit span instead).
11114 #[cfg(feature = "rust")]
11115 #[test]
11116 fn rust_nested_min_max_propagates() {
11117 check_metrics::<RustParser>(
11118 "struct C;\nimpl C {\n fn small(&self) {\n let _ = 1;\n }\n fn big(&self) {\n let _ = 1;\n let _ = 2;\n let _ = 3;\n }\n}\n",
11119 "c.rs",
11120 |metric| {
11121 // Spaces: Unit + impl C + small() + big().
11122 // small() spans 3 rows (signature .. closing brace),
11123 // big() spans 5 rows. The Unit/impl spans are larger
11124 // still. sloc_min must be the smallest leaf (3), not the
11125 // top-level span; sloc_max must be the largest space.
11126 let loc = &metric.loc;
11127 assert!(
11128 loc.sloc_min() <= loc.sloc(),
11129 "sloc_min {} must not exceed unit sloc {}",
11130 loc.sloc_min(),
11131 loc.sloc()
11132 );
11133 assert_eq!(loc.sloc_min(), 3, "smallest leaf method span");
11134 assert_eq!(loc.sloc_max(), loc.sloc(), "largest space is the unit");
11135 // The smallest leaf has one statement; min must reflect it.
11136 assert_eq!(loc.lloc_min(), 1);
11137 },
11138 );
11139 }
11140
11141 /// Java sibling of `rust_nested_min_max_propagates` (#437). The
11142 /// `java_class_loc` snapshot above showed the bug directly: a class
11143 /// with methods reported `sloc_min == sloc` (the unit span). Here we
11144 /// assert the smallest method propagates.
11145 #[cfg(feature = "java")]
11146 #[test]
11147 fn java_nested_min_max_propagates() {
11148 check_metrics::<JavaParser>(
11149 "public class C {\n void small() {\n int x = 1;\n }\n void big() {\n int a = 1;\n int b = 2;\n int c = 3;\n }\n}\n",
11150 "C.java",
11151 |metric| {
11152 // Spaces: Unit + class C + small() + big().
11153 let loc = &metric.loc;
11154 assert!(loc.sloc_min() <= loc.sloc());
11155 assert_eq!(loc.sloc_min(), 3, "smallest leaf method span");
11156 assert_eq!(loc.sloc_max(), loc.sloc());
11157 assert_eq!(loc.lloc_min(), 1);
11158 },
11159 );
11160 }
11161
11162 /// Python sibling of `rust_nested_min_max_propagates` (#437). Python
11163 /// nesting is class -> method, mirroring the worked example in the
11164 /// issue (file -> class C -> method m).
11165 #[cfg(feature = "python")]
11166 #[test]
11167 fn python_nested_min_max_propagates() {
11168 check_metrics::<PythonParser>(
11169 "class C:\n def small(self):\n x = 1\n def big(self):\n a = 1\n b = 2\n c = 3\n",
11170 "c.py",
11171 |metric| {
11172 // Spaces: Unit + class C + small() + big().
11173 let loc = &metric.loc;
11174 assert!(loc.sloc_min() <= loc.sloc());
11175 assert_eq!(loc.sloc_min(), 2, "smallest leaf method span");
11176 assert_eq!(loc.sloc_max(), loc.sloc());
11177 assert_eq!(loc.lloc_min(), 1);
11178 },
11179 );
11180 }
11181
11182 /// `blank()` is `sloc - ploc - only_comment_lines`, an f64 subtraction
11183 /// that can go negative when a space's physical and comment line
11184 /// attribution overlaps its span row count. It must clamp at 0 so the
11185 /// serialized value is never negative (#437).
11186 ///
11187 /// Built from a synthetic [`Stats`] rather than a parsed fixture: the
11188 /// current grammars do not emit a parsed space whose root `sloc` is
11189 /// smaller than `ploc + only_comment_lines`, so a source fixture cannot
11190 /// drive the subtraction negative and would pass against the pre-clamp
11191 /// code (proving nothing). Setting `sloc = ploc = only_comment_lines = 1`
11192 /// yields a pre-clamp `blank` of `1 - 1 - 1 = -1`; reverting the
11193 /// `.max(0.0)` makes this assertion fail with `-1`.
11194 #[test]
11195 fn blank_clamps_negative_to_zero() {
11196 let mut stats = Stats::default();
11197 // A single-row span ending mid-line => sloc() of 1 row.
11198 stats.sloc.start = 0;
11199 // End row 0 ending mid-line, so that row counts: sloc() == 1.
11200 stats.sloc.end_line = 1;
11201 // ploc() is the cardinality of the physical-line set => 1.
11202 stats.ploc.lines.insert(0);
11203 // One comment-only line on the same single row.
11204 stats.cloc.only_comment_line_starts.insert(0);
11205
11206 // Pre-clamp this is 1 - 1 - 1 = -1.
11207 assert_eq!(stats.sloc(), 1);
11208 assert_eq!(stats.ploc(), 1);
11209 assert_eq!(
11210 stats.blank(),
11211 0,
11212 "blank() must clamp the negative subtraction to 0"
11213 );
11214 }
11215
11216 /// A physical line shared by two *sibling* function spaces must
11217 /// count once in the parent that merges them.
11218 ///
11219 /// This is the semantics `Ploc::merge` / `Cloc::merge` encode, and
11220 /// the reason the per-space line stores are sets rather than
11221 /// counters. It is asserted here across four language families —
11222 /// Rust, the C family, the JS family, and Python's indentation-based
11223 /// spaces — because the merge is shared by all of them and each
11224 /// family reaches it through a different `Loc::compute` body.
11225 ///
11226 /// Every fixture puts both spaces on one row, so a merge that summed
11227 /// instead of unioning would report `ploc`/`cloc` of 2 against an
11228 /// `sloc` of 1 — an impossible reading that also drives `blank`
11229 /// negative.
11230 #[cfg(all(
11231 feature = "cpp",
11232 feature = "javascript",
11233 feature = "python",
11234 feature = "rust"
11235 ))]
11236 #[test]
11237 fn sibling_spaces_sharing_a_line_count_it_once() {
11238 check_metrics::<RustParser>(
11239 "fn a() { let x = 1; } fn b() { let y = 2; }",
11240 "foo.rs",
11241 |metric| {
11242 assert_eq!(metric.loc.sloc(), 1);
11243 assert_eq!(metric.loc.ploc(), 1, "one physical row, two spaces");
11244 assert_eq!(metric.loc.blank(), 0);
11245 },
11246 );
11247
11248 check_metrics::<CppParser>(
11249 "int a() { return 1; } int b() { return 2; }",
11250 "foo.cpp",
11251 |metric| {
11252 assert_eq!(metric.loc.sloc(), 1);
11253 assert_eq!(metric.loc.ploc(), 1, "one physical row, two spaces");
11254 assert_eq!(metric.loc.blank(), 0);
11255 },
11256 );
11257
11258 check_metrics::<JavascriptParser>(
11259 "function a() { let x = 1; } function b() { let y = 2; }",
11260 "foo.js",
11261 |metric| {
11262 assert_eq!(metric.loc.sloc(), 1);
11263 assert_eq!(metric.loc.ploc(), 1, "one physical row, two spaces");
11264 assert_eq!(metric.loc.blank(), 0);
11265 },
11266 );
11267
11268 // Python cannot open two spaces on one row, so the shared row is
11269 // the nested `def`'s: it belongs to the inner space's span and to
11270 // the outer space's, and the unit merges both.
11271 check_metrics::<PythonParser>(
11272 "def a():
11273 def b(): return 1",
11274 "foo.py",
11275 |metric| {
11276 assert_eq!(metric.loc.sloc(), 2);
11277 assert_eq!(metric.loc.ploc(), 2, "two physical rows, three spaces");
11278 assert_eq!(metric.loc.blank(), 0);
11279 },
11280 );
11281 }
11282
11283 /// The same union property for comment lines: two sibling spaces that
11284 /// each carry a comment on the one shared row must yield `cloc == 1`.
11285 ///
11286 /// Summing rather than unioning would report `cloc == 2` against
11287 /// `sloc == 1`, the `cloc > sloc` state that pushes MI's
11288 /// comments_percentage above 100% (the failure mode of issue #461,
11289 /// here across the space merge rather than within one space).
11290 #[cfg(all(feature = "cpp", feature = "rust"))]
11291 #[test]
11292 fn sibling_spaces_sharing_a_comment_line_count_it_once() {
11293 check_metrics::<CppParser>(
11294 "int a() { /*x*/ return 1; } int b() { /*y*/ return 2; }",
11295 "foo.cpp",
11296 |metric| {
11297 assert_eq!(metric.loc.sloc(), 1);
11298 assert_eq!(metric.loc.cloc(), 1, "one comment row, two spaces");
11299 assert!(
11300 metric.loc.cloc() <= metric.loc.sloc(),
11301 "cloc must never exceed sloc"
11302 );
11303 },
11304 );
11305
11306 check_metrics::<RustParser>(
11307 "fn a() { /*x*/ let p = 1; } fn b() { /*y*/ let q = 2; }",
11308 "foo.rs",
11309 |metric| {
11310 assert_eq!(metric.loc.sloc(), 1);
11311 assert_eq!(metric.loc.cloc(), 1, "one comment row, two spaces");
11312 assert!(
11313 metric.loc.cloc() <= metric.loc.sloc(),
11314 "cloc must never exceed sloc"
11315 );
11316 },
11317 );
11318 }
11319
11320 /// A row inside a chain of nested spaces is folded upward once per
11321 /// level, and must still count once at the top.
11322 ///
11323 /// Fifteen levels rather than two: at one level a union and a sum
11324 /// agree whenever the sets happen to be disjoint, and the point of
11325 /// #1109 is the repeated fold. The body row belongs to every level's
11326 /// span, so a merge that accumulated would report `ploc == 15`.
11327 #[cfg(feature = "rust")]
11328 #[test]
11329 fn a_row_folded_through_nested_spaces_counts_once() {
11330 const DEPTH: usize = 15;
11331 let source = format!(
11332 "{}let x = 1;{}",
11333 "fn f() { ".repeat(DEPTH),
11334 "} ".repeat(DEPTH)
11335 );
11336
11337 check_metrics::<RustParser>(&source, "foo.rs", |metric| {
11338 assert_eq!(metric.loc.sloc(), 1);
11339 assert_eq!(metric.loc.ploc(), 1, "one physical row, {DEPTH} spaces");
11340 assert_eq!(metric.loc.ploc_max(), 1);
11341 assert_eq!(metric.loc.blank(), 0);
11342 });
11343 }
11344
11345 /// Two inline block comments on a single code line must count as a
11346 /// single comment line, not one per comment node. Pre-fix this
11347 /// reported `cloc = 2` (one increment per node) for a one-line
11348 /// construct, violating `cloc <= sloc`/`cloc <= ploc` and pushing
11349 /// the MI comments_percentage above 100% (issue #461). Reverting
11350 /// the per-line de-dup in `add_code_comment_line` makes the
11351 /// `cloc == 1` assertions fail with `2`.
11352 #[cfg(feature = "cpp")]
11353 #[test]
11354 fn cloc_multiple_block_comments_one_line_cpp() {
11355 check_metrics::<CppParser>(
11356 "int f(int /*a*/, int /*b*/) { return 1; }",
11357 "foo.cpp",
11358 |metric| {
11359 assert_eq!(metric.loc.cloc(), 1, "two inline comments => 1 cloc");
11360 assert!(
11361 metric.loc.cloc() <= metric.loc.sloc(),
11362 "cloc must not exceed sloc"
11363 );
11364 assert!(
11365 metric.loc.cloc() <= metric.loc.ploc(),
11366 "cloc must not exceed ploc for a single-line construct"
11367 );
11368 },
11369 );
11370 }
11371
11372 /// Sibling-language coverage: `add_cloc_lines` is shared across
11373 /// every block-comment language, so the Rust path must behave
11374 /// identically to C++ (issue #461).
11375 #[cfg(feature = "rust")]
11376 #[test]
11377 fn cloc_multiple_block_comments_one_line_rust() {
11378 check_metrics::<RustParser>(
11379 "fn f(/*a*/ x: i32, /*b*/ y: i32) -> i32 { 1 }",
11380 "foo.rs",
11381 |metric| {
11382 assert_eq!(metric.loc.cloc(), 1, "two inline comments => 1 cloc");
11383 assert!(
11384 metric.loc.cloc() <= metric.loc.sloc(),
11385 "cloc must not exceed sloc"
11386 );
11387 },
11388 );
11389 }
11390
11391 /// Guard against over-de-dup: a single multi-line block comment
11392 /// must still contribute one comment line per physical line it
11393 /// spans. The de-dup keys on the start row only, so the three
11394 /// independent continuation lines are unaffected (issue #461).
11395 #[cfg(feature = "cpp")]
11396 #[test]
11397 fn cloc_multiline_block_comment_counts_each_line() {
11398 check_metrics::<CppParser>(
11399 "int g() {\n /* l1\n l2\n l3 */\n return 0;\n}",
11400 "foo.cpp",
11401 |metric| {
11402 assert_eq!(
11403 metric.loc.cloc(),
11404 3,
11405 "a 3-line block comment counts 3 comment lines"
11406 );
11407 },
11408 );
11409 }
11410
11411 /// Two *standalone* block comments on a single physical line (no
11412 /// code) must count as one comment line, not one per node. #461
11413 /// deduped only inline co-located comments via the code-comment
11414 /// path; the standalone path bumped `only_comment_lines` per node,
11415 /// so `/*a*/ /*b*/` reported `cloc = 2` for a single line —
11416 /// violating `cloc <= sloc`. Reverting the per-line set in
11417 /// `add_only_comment_lines` makes the `cloc == 1` assertion fail
11418 /// with `2` (verified by reverting to `only_comment_lines += …`).
11419 #[cfg(feature = "cpp")]
11420 #[test]
11421 fn cloc_multiple_standalone_block_comments_one_line_cpp() {
11422 check_metrics::<CppParser>("/*a*/ /*b*/", "foo.cpp", |metric| {
11423 assert_eq!(
11424 metric.loc.cloc(),
11425 1,
11426 "two standalone comments on one line => 1 cloc"
11427 );
11428 assert_eq!(metric.loc.sloc(), 1);
11429 assert!(
11430 metric.loc.cloc() <= metric.loc.sloc(),
11431 "cloc must not exceed sloc"
11432 );
11433 });
11434 }
11435
11436 /// Sibling-language coverage: the standalone de-dup lives in the
11437 /// shared `add_only_comment_lines` helper, so Rust must match C++
11438 /// (issue #461 follow-up).
11439 #[cfg(feature = "rust")]
11440 #[test]
11441 fn cloc_multiple_standalone_block_comments_one_line_rust() {
11442 check_metrics::<RustParser>("/*a*/ /*b*/", "foo.rs", |metric| {
11443 assert_eq!(
11444 metric.loc.cloc(),
11445 1,
11446 "two standalone comments on one line => 1 cloc"
11447 );
11448 assert_eq!(metric.loc.sloc(), 1);
11449 assert!(
11450 metric.loc.cloc() <= metric.loc.sloc(),
11451 "cloc must not exceed sloc"
11452 );
11453 });
11454 }
11455
11456 /// File-level guard: a comment-only physical line followed by a
11457 /// real code line still counts the comment line once and never
11458 /// exceeds sloc, even after the per-space merge that previously
11459 /// summed `code_comment_lines` (and double-counted a boundary line)
11460 /// rather than reading the per-line set. Three standalone comments
11461 /// share line 1, so the whole file has exactly one comment line.
11462 #[cfg(feature = "cpp")]
11463 #[test]
11464 fn cloc_standalone_comments_then_code_no_double_count() {
11465 check_metrics::<CppParser>("/*a*/ /*b*/ /*c*/\nint x = 1;\n", "foo.cpp", |metric| {
11466 assert_eq!(metric.loc.sloc(), 2);
11467 assert_eq!(
11468 metric.loc.cloc(),
11469 1,
11470 "only line 1 carries comments => 1 cloc"
11471 );
11472 assert!(
11473 metric.loc.cloc() <= metric.loc.sloc(),
11474 "cloc must not exceed sloc"
11475 );
11476 });
11477 }
11478
11479 #[cfg(feature = "irules")]
11480 #[test]
11481 fn irules_multiline_quoted_word_credits_every_row_to_ploc() {
11482 // Regression test for #1260, the dialect half of the Tcl fix: a
11483 // `quoted_word` carries no child per row, so its interior rows
11484 // reached neither PLOC nor CLOC and
11485 // `blank = sloc - ploc - cloc` claimed them.
11486 //
11487 // expected: 6 rows — `when HTTP_REQUEST {`,
11488 // ` set msg "line one`, `line two`, `line three"`,
11489 // ` log local0. $msg`, `}` — all code, none blank. lloc 3 for
11490 // the handler, the `set` and the `log` command.
11491 check_metrics::<IrulesParser>(
11492 "when HTTP_REQUEST {\n set msg \"line one\nline two\nline three\"\n log local0. $msg\n}",
11493 "foo.irule",
11494 |metric| {
11495 assert_eq!(metric.loc.sloc(), 6);
11496 assert_eq!(metric.loc.ploc(), 6);
11497 assert_eq!(metric.loc.lloc(), 3);
11498 assert_eq!(metric.loc.cloc(), 0);
11499 assert_eq!(metric.loc.blank(), 0);
11500 },
11501 );
11502
11503 // expected: 3 rows whose middle row is empty *inside* the literal —
11504 // `set msg "one`, ``, `three"` — code, not blank.
11505 check_metrics::<IrulesParser>("set msg \"one\n\nthree\"", "foo.irule", |metric| {
11506 assert_eq!(metric.loc.sloc(), 3);
11507 assert_eq!(metric.loc.ploc(), 3);
11508 assert_eq!(metric.loc.cloc(), 0);
11509 assert_eq!(metric.loc.blank(), 0);
11510 });
11511 }
11512
11513 /// Interior blank lines are counted as BLANK and excluded from PLOC.
11514 /// sloc 6 (every line) / ploc 4 (4 code lines) / lloc 3 (handler + 2
11515 /// `set`s) / cloc 0 / blank 2.
11516 ///
11517 /// Also the dialect's half of the #1260 carve-out: the handler body is
11518 /// a `braced_word`, which the grammar parses as a script, so its blank
11519 /// rows stay blank — only `quoted_word` is routed to PLOC.
11520 #[cfg(feature = "irules")]
11521 #[test]
11522 fn irules_blank() {
11523 check_metrics::<IrulesParser>(
11524 "when X {\n\n set x 1\n\n set y 2\n}\n",
11525 "foo.irule",
11526 |metric| {
11527 assert_eq!(metric.loc.sloc(), 6);
11528 assert_eq!(metric.loc.ploc(), 4);
11529 assert_eq!(metric.loc.lloc(), 3);
11530 assert_eq!(metric.loc.cloc(), 0);
11531 assert_eq!(metric.loc.blank(), 2);
11532 },
11533 );
11534 }
11535
11536 /// A handler body with no blank lines reports zero BLANK. lloc 3 =
11537 /// handler + `set` + `log` command.
11538 #[cfg(feature = "irules")]
11539 #[test]
11540 fn irules_no_zero_blank() {
11541 check_metrics::<IrulesParser>(
11542 "when HTTP_REQUEST {\n set x 1\n log local0. $x\n}\n",
11543 "foo.irule",
11544 |metric| {
11545 assert_eq!(metric.loc.sloc(), 4);
11546 assert_eq!(metric.loc.ploc(), 4);
11547 assert_eq!(metric.loc.lloc(), 3);
11548 assert_eq!(metric.loc.blank(), 0);
11549 },
11550 );
11551 }
11552
11553 /// `#`-prefixed comment lines are counted as CLOC (iRules has no block
11554 /// comments, so each comment node spans exactly one line).
11555 ///
11556 /// expected: rows 0-2 are comment-only, row 3 is the sole code row.
11557 /// `ploc` and `sloc` are asserted, not just `cloc`/`blank`: without
11558 /// them this test passed all the way through #1135, which credited
11559 /// each of the three comment rows to PLOC as well (`ploc == 4`,
11560 /// `cloc + ploc == 7` against `sloc == 4`).
11561 #[cfg(feature = "irules")]
11562 #[test]
11563 fn irules_cloc() {
11564 check_metrics::<IrulesParser>(
11565 "# a\n# b\n# c\nwhen X { set x 1 }\n",
11566 "foo.irule",
11567 |metric| {
11568 assert_eq!(metric.loc.sloc(), 4);
11569 assert_eq!(metric.loc.ploc(), 1);
11570 assert_eq!(metric.loc.cloc(), 3);
11571 assert_eq!(metric.loc.blank(), 0);
11572 },
11573 );
11574 }
11575
11576 /// LLOC counts each statement once: handler header, `if`, `set`, and the
11577 /// generic `log` command = 4. The `switch_arm` headers are not counted
11578 /// (their bodies' commands are), verified in `irules_switch_lloc`.
11579 #[cfg(feature = "irules")]
11580 #[test]
11581 fn irules_lloc() {
11582 check_metrics::<IrulesParser>(
11583 "when X {\n if { $a } {\n set x 1\n }\n log local0. done\n}\n",
11584 "foo.irule",
11585 |metric| {
11586 assert_eq!(metric.loc.lloc(), 4);
11587 },
11588 );
11589 }
11590
11591 /// A command inside `[...]` (`command_substitution`) is a sub-expression,
11592 /// not a top-level statement, so it does not add to LLOC. Here lloc 2 =
11593 /// handler + `set`; the inner `expr` is NOT counted. Removing the
11594 /// `CommandSubstitution` guard would push lloc to 3 — this is the loc
11595 /// gating-decision regression test.
11596 #[cfg(feature = "irules")]
11597 #[test]
11598 fn irules_no_command_substitution_lloc() {
11599 check_metrics::<IrulesParser>(
11600 "when X {\n set y [expr { 1 + 2 }]\n}\n",
11601 "foo.irule",
11602 |metric| {
11603 assert_eq!(metric.loc.lloc(), 2);
11604 },
11605 );
11606 }
11607
11608 /// `switch` counts once; each arm's *body* command counts, but the
11609 /// `switch_arm` pattern/body pair itself is not a logical line. lloc 4 =
11610 /// handler + `switch` + two `set`s (one per arm body).
11611 #[cfg(feature = "irules")]
11612 #[test]
11613 fn irules_switch_lloc() {
11614 check_metrics::<IrulesParser>(
11615 "when X {\n switch $h {\n a { set r 1 }\n b { set r 2 }\n }\n}\n",
11616 "foo.irule",
11617 |metric| {
11618 assert_eq!(metric.loc.lloc(), 4);
11619 },
11620 );
11621 }
11622
11623 /// A `proc` definition and its `return` command are each one logical
11624 /// line: lloc 2.
11625 #[cfg(feature = "irules")]
11626 #[test]
11627 fn irules_proc_lloc() {
11628 check_metrics::<IrulesParser>(
11629 "proc f { a } {\n return $a\n}\n",
11630 "foo.irule",
11631 |metric| {
11632 assert_eq!(metric.loc.lloc(), 2);
11633 },
11634 );
11635 }
11636
11637 /// Objective-C blank-line accounting: two code lines separated by
11638 /// blank lines.
11639 #[cfg(feature = "objc")]
11640 #[test]
11641 fn objc_blank() {
11642 check_metrics::<ObjcParser>(
11643 "
11644
11645 int a = 42;
11646
11647 int b = 43;
11648
11649 ",
11650 "foo.m",
11651 |metric| {
11652 assert_eq!(metric.loc.blank(), 1);
11653 insta::assert_json_snapshot!(metric.loc, @r#"
11654 {
11655 "sloc": 3,
11656 "ploc": 2,
11657 "lloc": 2,
11658 "cloc": 0,
11659 "blank": 1,
11660 "sloc_average": 3.0,
11661 "ploc_average": 2.0,
11662 "lloc_average": 2.0,
11663 "cloc_average": 0.0,
11664 "blank_average": 1.0,
11665 "sloc_min": 3,
11666 "sloc_max": 3,
11667 "cloc_min": 0,
11668 "cloc_max": 0,
11669 "ploc_min": 2,
11670 "ploc_max": 2,
11671 "lloc_min": 2,
11672 "lloc_max": 2,
11673 "blank_min": 1,
11674 "blank_max": 1
11675 }
11676 "#);
11677 },
11678 );
11679 }
11680
11681 /// Objective-C comment accounting: a block comment and a line
11682 /// comment each contribute to `cloc`.
11683 #[cfg(feature = "objc")]
11684 #[test]
11685 fn objc_cloc() {
11686 check_metrics::<ObjcParser>(
11687 "/* Block comment
11688 still the block */
11689 // Line comment
11690 int a = 42; // trailing",
11691 "foo.m",
11692 |metric| {
11693 insta::assert_json_snapshot!(metric.loc, @r#"
11694 {
11695 "sloc": 4,
11696 "ploc": 1,
11697 "lloc": 1,
11698 "cloc": 4,
11699 "blank": 0,
11700 "sloc_average": 4.0,
11701 "ploc_average": 1.0,
11702 "lloc_average": 1.0,
11703 "cloc_average": 4.0,
11704 "blank_average": 0.0,
11705 "sloc_min": 4,
11706 "sloc_max": 4,
11707 "cloc_min": 4,
11708 "cloc_max": 4,
11709 "ploc_min": 1,
11710 "ploc_max": 1,
11711 "lloc_min": 1,
11712 "lloc_max": 1,
11713 "blank_min": 0,
11714 "blank_max": 0
11715 }
11716 "#);
11717 },
11718 );
11719 }
11720
11721 /// Objective-C logical-line accounting: a method whose body has three
11722 /// statements. The `method_definition` opens a function space but is
11723 /// not itself a logical line; each statement adds one.
11724 #[cfg(feature = "objc")]
11725 #[test]
11726 fn objc_lloc() {
11727 check_metrics::<ObjcParser>(
11728 "@implementation Foo
11729- (int)bar {
11730 int a = 1;
11731 int b = 2;
11732 return a + b;
11733}
11734@end
11735",
11736 "foo.m",
11737 |metric| {
11738 // expected: decl `int a` (1) + decl `int b` (1) +
11739 // `return` (1) = 3.
11740 assert_eq!(metric.loc.lloc(), 3);
11741 insta::assert_json_snapshot!(metric.loc, @r#"
11742 {
11743 "sloc": 7,
11744 "ploc": 7,
11745 "lloc": 3,
11746 "cloc": 0,
11747 "blank": 0,
11748 "sloc_average": 2.3333333333333335,
11749 "ploc_average": 2.3333333333333335,
11750 "lloc_average": 1.0,
11751 "cloc_average": 0.0,
11752 "blank_average": 0.0,
11753 "sloc_min": 5,
11754 "sloc_max": 7,
11755 "cloc_min": 0,
11756 "cloc_max": 0,
11757 "ploc_min": 5,
11758 "ploc_max": 7,
11759 "lloc_min": 3,
11760 "lloc_max": 3,
11761 "blank_min": 0,
11762 "blank_max": 0
11763 }
11764 "#);
11765 },
11766 );
11767 }
11768
11769 /// Objective-C for-header gating: the `int i = 0` declaration in a
11770 /// classic `for` init slot is part of the `for` statement's logical
11771 /// line and must NOT add a second one (mirrors the C / C++ gate).
11772 /// Reverting the `count_specific_ancestors` gate would push lloc from
11773 /// 2 to 3.
11774 #[cfg(feature = "objc")]
11775 #[test]
11776 fn objc_no_declaration_in_for_header_lloc() {
11777 check_metrics::<ObjcParser>(
11778 "@implementation Foo
11779- (void)bar {
11780 for (int i = 0; i < 10; ++i) {
11781 [self use:i];
11782 }
11783}
11784@end
11785",
11786 "foo.m",
11787 |metric| {
11788 // expected: for-statement (1) + body expression
11789 // `[self use:i]` (1) = 2. The header `int i = 0`
11790 // declaration is gated out; without the gate this is 3.
11791 assert_eq!(metric.loc.lloc(), 2);
11792 insta::assert_json_snapshot!(metric.loc, @r#"
11793 {
11794 "sloc": 7,
11795 "ploc": 7,
11796 "lloc": 2,
11797 "cloc": 0,
11798 "blank": 0,
11799 "sloc_average": 2.3333333333333335,
11800 "ploc_average": 2.3333333333333335,
11801 "lloc_average": 0.6666666666666666,
11802 "cloc_average": 0.0,
11803 "blank_average": 0.0,
11804 "sloc_min": 5,
11805 "sloc_max": 7,
11806 "cloc_min": 0,
11807 "cloc_max": 0,
11808 "ploc_min": 5,
11809 "ploc_max": 7,
11810 "lloc_min": 2,
11811 "lloc_max": 2,
11812 "blank_min": 0,
11813 "blank_max": 0
11814 }
11815 "#);
11816 },
11817 );
11818 }
11819
11820 #[cfg(feature = "objc")]
11821 #[test]
11822 fn objc_at_directives_lloc() {
11823 // The only ObjC-specific LLOC work the impl does beyond the C
11824 // inheritance: `@synchronized` is a dedicated `synchronized_statement`
11825 // node (counts as a logical line), but `@autoreleasepool` emits
11826 // only a keyword token with no wrapping node, so its *header* adds
11827 // nothing and only its inner statements count.
11828 check_metrics::<ObjcParser>(
11829 "@implementation Foo
11830- (void)bar {
11831 @synchronized (self) {
11832 [self use];
11833 }
11834 @autoreleasepool {
11835 [self use];
11836 }
11837}
11838@end
11839",
11840 "foo.m",
11841 |metric| {
11842 // expected: @synchronized statement (1) + its body
11843 // `[self use]` (1) + the @autoreleasepool body `[self use]`
11844 // (1) = 3. The `@autoreleasepool` header contributes no
11845 // logical line (it has no node); if it did, this would be 4.
11846 assert_eq!(metric.loc.lloc(), 3);
11847 insta::assert_json_snapshot!(metric.loc, @r#"
11848 {
11849 "sloc": 10,
11850 "ploc": 10,
11851 "lloc": 3,
11852 "cloc": 0,
11853 "blank": 0,
11854 "sloc_average": 3.3333333333333335,
11855 "ploc_average": 3.3333333333333335,
11856 "lloc_average": 1.0,
11857 "cloc_average": 0.0,
11858 "blank_average": 0.0,
11859 "sloc_min": 8,
11860 "sloc_max": 10,
11861 "cloc_min": 0,
11862 "cloc_max": 0,
11863 "ploc_min": 8,
11864 "ploc_max": 10,
11865 "lloc_min": 3,
11866 "lloc_max": 3,
11867 "blank_min": 0,
11868 "blank_max": 0
11869 }
11870 "#);
11871 },
11872 );
11873 }
11874
11875 /// Analyses `source` byte-for-byte as Rust.
11876 ///
11877 /// Goes through `metrics_verbatim` rather than `check_metrics`
11878 /// because the #1051 cases end at EOF; see that helper for why.
11879 #[cfg(feature = "rust")]
11880 fn rust_loc(source: &[u8]) -> Stats {
11881 metrics_verbatim(crate::LANG::Rust, source, crate::MetricsOptions::default()).loc
11882 }
11883
11884 /// #1051: a Rust doc comment ending at EOF has no trailing newline for
11885 /// the scanner to consume, so its `LineComment` node ends on its own
11886 /// start row and discounting a row underflowed. On row 0 that panicked
11887 /// (debug: the subtraction; release: a hash-table capacity overflow in
11888 /// `add_only_comment_lines`). On any later row release did not crash —
11889 /// it silently reported one `cloc` too few.
11890 #[cfg(feature = "rust")]
11891 #[test]
11892 fn rust_doc_comment_at_eof_does_not_underflow() {
11893 // `end == start == 0` — underflowed at the subtraction itself.
11894 // expected: the sole row is one comment-only line, no code.
11895 let outer = rust_loc(b"/// x");
11896 assert_eq!(outer.cloc(), 1);
11897 assert_eq!(outer.ploc(), 0);
11898
11899 let inner = rust_loc(b"//! x");
11900 assert_eq!(inner.cloc(), 1);
11901 assert_eq!(inner.ploc(), 0);
11902
11903 // `end == start > 0` — the subtraction succeeded but drove `end`
11904 // below `start`. Release silently counted `cloc == 0` here.
11905 // expected: row 0 is code, row 1 is comment-only.
11906 let after_code = rust_loc(b"fn f(){}\n/// x");
11907 assert_eq!(after_code.cloc(), 1);
11908 assert_eq!(after_code.ploc(), 1);
11909
11910 // Two doc comments, the second at EOF: both rows are comment-only.
11911 // Pre-fix release reported 1 here, not 2.
11912 assert_eq!(rust_loc(b"/// a\n/// b").cloc(), 2);
11913
11914 // A doc comment sharing its row with code. `let` is not valid at
11915 // file scope, but tree-sitter parses it as a clean `let_declaration`
11916 // + `line_comment`, which is the only way to reach the
11917 // comment-after-code branch (no *valid* Rust puts `///` after code).
11918 let trailing = rust_loc(b"let x = 1; /// d");
11919 assert_eq!(trailing.cloc(), 1);
11920 assert_eq!(trailing.ploc(), 1);
11921 }
11922
11923 /// A doc comment at EOF must count exactly like a plain line comment at
11924 /// EOF. The `DocComment` adjustment exists only to discount the newline
11925 /// the scanner consumes; at EOF there is none to discount, so the two
11926 /// shapes are indistinguishable for LOC purposes.
11927 #[cfg(feature = "rust")]
11928 #[test]
11929 fn rust_doc_comment_at_eof_matches_plain_comment() {
11930 let plain = rust_loc(b"// x");
11931 // Pin the baseline absolutely too: parity alone would still hold if
11932 // both sides moved together, and would then read as a regression
11933 // when the un-newline-terminated `sloc` accounting is corrected.
11934 assert_eq!(plain.cloc(), 1);
11935 assert_eq!(plain.ploc(), 0);
11936
11937 for doc in [&b"/// x"[..], &b"//! x"[..]] {
11938 let doc = rust_loc(doc);
11939 assert_eq!(doc.cloc(), plain.cloc());
11940 assert_eq!(doc.ploc(), plain.ploc());
11941 assert_eq!(doc.sloc(), plain.sloc());
11942 assert_eq!(doc.blank(), plain.blank());
11943 }
11944 }
11945
11946 /// The newline-terminated path must stay unchanged by the #1051 guard.
11947 /// A `DocComment` node really does span one row more than it renders
11948 /// whenever the scanner consumed a newline, and that row must still be
11949 /// excluded — otherwise the guard would silently become a no-op and
11950 /// inflate CLOC for every doc-commented Rust file.
11951 #[cfg(feature = "rust")]
11952 #[test]
11953 fn rust_doc_comment_with_trailing_newline_still_discounts_the_row() {
11954 // expected: one rendered comment row, not two.
11955 assert_eq!(rust_loc(b"/// x\n").cloc(), 1);
11956 // expected: two consecutive doc comments are two rows, not four.
11957 assert_eq!(rust_loc(b"/// a\n/// b\n").cloc(), 2);
11958
11959 // The common real-world shape: doc comment attached to an item.
11960 // expected: row 0 comment-only, row 1 code.
11961 let documented = rust_loc(b"/// doc\nfn f() {}\n");
11962 assert_eq!(documented.cloc(), 1);
11963 assert_eq!(documented.ploc(), 1);
11964 }
11965
11966 /// CRLF is the boundary the guard must *not* fire on. `\r` is ordinary
11967 /// content to `process_line_doc_content`, so it consumes the following
11968 /// `\n` and the node does span an extra row — the discount is still
11969 /// owed. A lone trailing `\r` at EOF is the opposite case. Without this,
11970 /// a future grammar bump that stops consuming the newline would leave
11971 /// every LF test passing while the discount silently became dead code.
11972 #[cfg(feature = "rust")]
11973 #[test]
11974 fn rust_doc_comment_crlf_still_discounts_the_row() {
11975 // Newline consumed despite the `\r`: discount applies.
11976 assert_eq!(rust_loc(b"/// x\r\n").cloc(), 1);
11977 // expected: row 0 code, row 1 comment-only.
11978 let after_code = rust_loc(b"fn f(){}\r\n/// x");
11979 assert_eq!(after_code.cloc(), 1);
11980 assert_eq!(after_code.ploc(), 1);
11981 // Lone `\r`, then EOF: no newline consumed, so no discount is owed.
11982 assert_eq!(rust_loc(b"/// x\r").cloc(), 1);
11983 }
11984
11985 /// One-line, deliberately un-newline-terminated source, one entry per
11986 /// language whose `Loc` implementation is not a documented no-op.
11987 ///
11988 /// `sloc` is computed once, in the shared [`Sloc`], from the unit
11989 /// span the grammar hands us — so the only per-language variable is
11990 /// where each grammar puts the root node's end position. #1067 was
11991 /// possible precisely because that position was assumed rather than
11992 /// read, so the sweep is exhaustive rather than a sample: a future
11993 /// grammar whose root does *not* run to end-of-input has to show up
11994 /// here rather than silently lose a row.
11995 ///
11996 /// `Preproc` and `Ccomment` are excluded, but not because they are
11997 /// exempt from the rule. Their `Loc` impls are no-ops
11998 /// (`implement_metric_trait!(Loc, PreprocCode, CcommentCode)`, #188),
11999 /// so the *node-accumulated* sub-metrics are 0 by design — yet
12000 /// `sloc` is not node-accumulated: the walker anchors every Unit's
12001 /// row span at finalization, their synthetic Unit root included, so
12002 /// they carry a real span and drift with #1067 exactly as the
12003 /// languages below do. What they cannot join is the second sweep, whose final
12004 /// `mi != 0` assertion is unreachable with `ploc == 0`. They get
12005 /// their own check in
12006 /// `no_op_loc_grammars_still_count_their_unterminated_row`.
12007 #[cfg(any(
12008 feature = "bash",
12009 feature = "c",
12010 feature = "c-family-helpers",
12011 feature = "cpp",
12012 feature = "csharp",
12013 feature = "elixir",
12014 feature = "go",
12015 feature = "groovy",
12016 feature = "irules",
12017 feature = "java",
12018 feature = "javascript",
12019 feature = "kotlin",
12020 feature = "lua",
12021 feature = "mozcpp",
12022 feature = "mozjs",
12023 feature = "objc",
12024 feature = "perl",
12025 feature = "php",
12026 feature = "python",
12027 feature = "ruby",
12028 feature = "rust",
12029 feature = "tcl",
12030 feature = "typescript",
12031 ))]
12032 const UNTERMINATED_ONE_LINERS: &[(crate::LANG, &[u8])] = &[
12033 (crate::LANG::Rust, b"fn main() {}"),
12034 (crate::LANG::C, b"int main(void) { return 0; }"),
12035 (crate::LANG::Cpp, b"int main() { return 0; }"),
12036 (crate::LANG::Mozcpp, b"int main() { return 0; }"),
12037 (crate::LANG::Objc, b"int main(void) { return 0; }"),
12038 (crate::LANG::Csharp, b"class C { void M() {} }"),
12039 (crate::LANG::Java, b"class C { void m() {} }"),
12040 (crate::LANG::Kotlin, b"fun main() {}"),
12041 (crate::LANG::Groovy, b"def f() {}"),
12042 (crate::LANG::Go, b"package main"),
12043 (crate::LANG::Javascript, b"function f() {}"),
12044 (crate::LANG::Mozjs, b"function f() {}"),
12045 (crate::LANG::Typescript, b"function f(): void {}"),
12046 (crate::LANG::Tsx, b"function f() {}"),
12047 (crate::LANG::Python, b"def f(): pass"),
12048 (crate::LANG::Ruby, b"def f; end"),
12049 (crate::LANG::Php, b"<?php function f() {}"),
12050 (crate::LANG::Perl, b"sub f { return 1; }"),
12051 (crate::LANG::Bash, b"f() { echo hi; }"),
12052 (crate::LANG::Lua, b"function f() end"),
12053 (crate::LANG::Tcl, b"proc f {} {}"),
12054 (crate::LANG::Irules, b"proc f {} {}"),
12055 (crate::LANG::Elixir, b"defmodule M do end"),
12056 ];
12057
12058 /// #1067: `Sloc::sloc()` derived the unit's row count as `end - start`,
12059 /// which is only right when a trailing newline pushes the root node's
12060 /// end onto a phantom extra row. Source that stops mid-line — anything
12061 /// not newline-terminated — lost its final row, so a one-line file
12062 /// reported `sloc == 0`.
12063 ///
12064 /// Uses [`metrics_verbatim`], not `check_metrics`: the latter trims and
12065 /// re-appends a trailing newline, which makes this entire input class
12066 /// unreachable and the test vacuous (the same blind spot that hid
12067 /// #1051).
12068 #[cfg(all(
12069 feature = "bash",
12070 feature = "c",
12071 feature = "cpp",
12072 feature = "csharp",
12073 feature = "elixir",
12074 feature = "go",
12075 feature = "groovy",
12076 feature = "irules",
12077 feature = "java",
12078 feature = "javascript",
12079 feature = "kotlin",
12080 feature = "lua",
12081 feature = "mozcpp",
12082 feature = "mozjs",
12083 feature = "objc",
12084 feature = "perl",
12085 feature = "php",
12086 feature = "python",
12087 feature = "ruby",
12088 feature = "rust",
12089 feature = "tcl",
12090 feature = "typescript",
12091 ))]
12092 #[test]
12093 fn unterminated_one_line_file_reports_one_source_line() {
12094 for (lang, source) in UNTERMINATED_ONE_LINERS {
12095 let text = String::from_utf8_lossy(source);
12096 let loc = metrics_verbatim(*lang, source, MetricsOptions::default()).loc;
12097 assert_eq!(loc.sloc(), 1, "{lang:?} sloc for {text:?}");
12098 // The largest space can never be bigger than the file itself.
12099 assert_eq!(loc.sloc_max(), 1, "{lang:?} sloc_max for {text:?}");
12100 // The invariant documented on `Stats::with_cloc_sloc`: every
12101 // physical line is code, comment, both, or blank, so the code
12102 // and comment-only tallies cannot together exceed the row
12103 // count. `sloc == 0` broke it for any unterminated file whose
12104 // last line carried content.
12105 assert!(
12106 loc.cloc() + loc.ploc() <= loc.sloc(),
12107 "{lang:?}: cloc {} + ploc {} exceeds sloc {} for {text:?}",
12108 loc.cloc(),
12109 loc.ploc(),
12110 loc.sloc(),
12111 );
12112 }
12113 }
12114
12115 /// The two grammars whose `Loc` impl is the macro's no-op still get a
12116 /// `sloc`, so they drift with #1067 like everything else.
12117 ///
12118 /// Their root is not a `SpaceKind::Unit`, so `metrics_inner` pushes a
12119 /// synthetic Unit, whose row span `anchor_unit_sloc_span` fills in at
12120 /// finalization — a span the no-op `compute` never touches but
12121 /// `Sloc::sloc()` still measures.
12122 /// Before #1067 an unterminated one-liner measured `0` rows here too.
12123 /// Kept apart from [`UNTERMINATED_ONE_LINERS`] only because the
12124 /// `mi != 0` half of the sweep below cannot hold with `ploc == 0`.
12125 #[cfg(any(
12126 feature = "c",
12127 feature = "c-family-helpers",
12128 feature = "cpp",
12129 feature = "mozcpp"
12130 ))]
12131 #[test]
12132 fn no_op_loc_grammars_still_count_their_unterminated_row() {
12133 for (lang, source) in [
12134 (crate::LANG::Preproc, &b"#define A 1"[..]),
12135 (crate::LANG::Ccomment, &b"/* c */"[..]),
12136 ] {
12137 let bare = metrics_verbatim(lang, source, MetricsOptions::default()).loc;
12138 assert_eq!(bare.sloc(), 1, "{lang:?} unterminated sloc");
12139 let mut terminated = source.to_vec();
12140 terminated.push(b'\n');
12141 let terminated = metrics_verbatim(lang, &terminated, MetricsOptions::default()).loc;
12142 assert_eq!(
12143 bare.sloc(),
12144 terminated.sloc(),
12145 "{lang:?} sloc must not depend on the trailing newline"
12146 );
12147 // The node-accumulated sub-metrics are the ones #188 zeroes.
12148 assert_eq!((bare.ploc(), bare.cloc(), bare.lloc()), (0, 0, 0));
12149 }
12150 }
12151
12152 /// A backslash-continued `#define` body is one `PreprocArg` node
12153 /// spanning every continuation row, so each of those rows is PLOC.
12154 ///
12155 /// The four C-family `Loc` impls carry an identical arm for this
12156 /// (`tree-sitter-cpp` does not expand macros — see the comment at
12157 /// each site), and until #1229 only C++'s copy was exercised: the
12158 /// other three were the sole uncovered lines in that PR. They are
12159 /// deliberate clones, so a fixture for one is a fixture for all
12160 /// four, and `Mozcpp` in particular owns no file extension and can
12161 /// only be reached by naming the language.
12162 ///
12163 /// Measured, and confirmed discriminating by deleting the arm from
12164 /// all four modules: `ploc` is 4 with it and 3 without, the lost row
12165 /// being the macro's last continuation line. `sloc` is 5 (three
12166 /// macro rows, one blank, one `main`) and `lloc` is 1 — the single
12167 /// `return` statement — since a `#define` declares no statement.
12168 ///
12169 /// This fixture pins how *many* rows the arm credits, not where it
12170 /// stops: its body closes on `(b))`, so the node's end column is
12171 /// above 0 and the raw end row and `Node::end_line() - 1` name the
12172 /// same last row. The bound is pinned separately by
12173 /// [`a_dangling_macro_continuation_does_not_credit_the_row_below`].
12174 #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12175 #[test]
12176 fn a_continued_macro_body_counts_every_row_it_spans() {
12177 // Rows: 0-2 are the macro, 3 is blank, 4 is `main`.
12178 const CONTINUED_MACRO: &[u8] =
12179 b"#define SUM(a, b) \\\n ((a) + \\\n (b))\n\nint main(void) { return SUM(1, 2); }\n";
12180
12181 // One row per language, each gated on its own feature: an
12182 // ungated table constructs a parser for a language the build
12183 // disabled and panics `LanguageDisabled`, which reads as a
12184 // defect in whatever was being changed rather than as a
12185 // configuration that cannot run this test (#1446).
12186 const LANGS: &[crate::LANG] = &[
12187 #[cfg(feature = "c")]
12188 crate::LANG::C,
12189 #[cfg(feature = "cpp")]
12190 crate::LANG::Cpp,
12191 #[cfg(feature = "mozcpp")]
12192 crate::LANG::Mozcpp,
12193 #[cfg(feature = "objc")]
12194 crate::LANG::Objc,
12195 ];
12196 crate::test_support::assert_fixtures_present(LANGS);
12197
12198 for &lang in LANGS {
12199 let loc = metrics_verbatim(lang, CONTINUED_MACRO, MetricsOptions::default()).loc;
12200 assert_eq!(
12201 loc.ploc(),
12202 4,
12203 "{lang:?}: every continuation row of the macro body is code"
12204 );
12205 assert_eq!(loc.sloc(), 5, "{lang:?} sloc");
12206 assert_eq!(loc.lloc(), 1, "{lang:?} lloc");
12207 assert_eq!(loc.cloc(), 0, "{lang:?} cloc");
12208 assert_eq!(loc.blank(), 1, "{lang:?} blank");
12209 }
12210 }
12211
12212 /// Where the `PreprocArg` arm stops: a macro body whose last
12213 /// continuation is a *dangling* backslash produces a node ending at
12214 /// column 0 of the row below, and that row is not part of the span.
12215 ///
12216 /// `Node::end_line` is what encodes the rule, so the arm derives its
12217 /// last row from `add_string_interior_ploc` rather than from the raw
12218 /// `end_row()`, which over-reads by one on exactly this shape
12219 /// (#1423). Every other multi-row PLOC path already went through the
12220 /// helper; these four arms were the last open-coded copies.
12221 ///
12222 /// Measured with `bca dump`, not read off the source text: the
12223 /// `preproc_arg` runs from row 0 column 10 to row 2 column 0. Rows
12224 /// are 0 `#define`, 1 the continuation, 2 the blank row the trailing
12225 /// backslash runs into, 3 `int x;`. So `ploc` is {0, 1, 3} = 3 and
12226 /// `blank` is 1. The raw end row credited row 2 as well, reporting
12227 /// `ploc 4, blank 0`.
12228 ///
12229 /// Two parts of the fixture are load-bearing rather than scenery:
12230 ///
12231 /// - **`int x;` after the macro.** `clamp_line_sets_to_span` already
12232 /// drops a PLOC row past the space's own last row, so a fixture
12233 /// ending on the macro reports the corrected numbers under either
12234 /// spelling and discriminates nothing.
12235 /// - **The ` 2 \` continuation row.** Nothing but the arm's
12236 /// interior insert credits row 1 — `preproc_def` contributes only
12237 /// its own start row — so `ploc == 3` fails if that row is ever
12238 /// trimmed out of the fixture, per the fixture-decay rule in
12239 /// `.claude/rules/testing.md`. The `blank == 1` half has no such
12240 /// anchor available: deleting the dangling backslash also stops
12241 /// row 2 being credited, so a revert of the bound is the only
12242 /// evidence that half discriminates, and it was run.
12243 #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12244 #[test]
12245 fn a_dangling_macro_continuation_does_not_credit_the_row_below() {
12246 // Rows: 0-1 are the macro, 2 is the blank row its trailing
12247 // backslash runs into, 3 is `int x;`.
12248 const DANGLING_CONTINUATION: &[u8] = b"#define A 1 \\\n 2 \\\n\nint x;\n";
12249
12250 // Gated per row for the reason
12251 // `a_continued_macro_body_counts_every_row_it_spans` gives.
12252 const LANGS: &[crate::LANG] = &[
12253 #[cfg(feature = "c")]
12254 crate::LANG::C,
12255 #[cfg(feature = "cpp")]
12256 crate::LANG::Cpp,
12257 #[cfg(feature = "mozcpp")]
12258 crate::LANG::Mozcpp,
12259 #[cfg(feature = "objc")]
12260 crate::LANG::Objc,
12261 ];
12262 crate::test_support::assert_fixtures_present(LANGS);
12263
12264 for &lang in LANGS {
12265 let loc = metrics_verbatim(lang, DANGLING_CONTINUATION, MetricsOptions::default()).loc;
12266 assert_eq!(
12267 loc.ploc(),
12268 3,
12269 "{lang:?}: the row a dangling continuation ends *at* is not code"
12270 );
12271 assert_eq!(
12272 loc.blank(),
12273 1,
12274 "{lang:?}: that row is blank, not part of the macro body"
12275 );
12276 assert_eq!(loc.sloc(), 4, "{lang:?} sloc");
12277 assert_eq!(loc.lloc(), 1, "{lang:?} lloc");
12278 assert_eq!(loc.cloc(), 0, "{lang:?} cloc");
12279 }
12280 }
12281
12282 /// Whether the last line ends in a newline is a formatting detail, not
12283 /// a property of the code — no LOC sub-metric, and therefore no MI
12284 /// value, may depend on it. This is the invariant #1067 violated, and
12285 /// it pins the fix from both sides: the newline-terminated path (the
12286 /// one every in-tree harness exercises) must not move either.
12287 ///
12288 /// **The invariant is now unconditional.** It used to be scoped to
12289 /// source containing a token: whitespace-only input collapsed most
12290 /// grammars' roots to a zero-width node at end-of-input, leaving
12291 /// `sloc` no span to measure, so `b" "` reported one row and
12292 /// `b" \n"` reported none (#1087). #1247 anchored the unit's `sloc`
12293 /// span to the span the unit reports, which removed that dependence
12294 /// on where the root node happens to start. The whitespace-only class
12295 /// is swept separately — it cannot ride this test, whose closing
12296 /// `assert_ne!` requires a non-zero MI — in
12297 /// [`whitespace_only_input_is_uniform_across_grammars`].
12298 #[cfg(all(
12299 feature = "bash",
12300 feature = "c",
12301 feature = "cpp",
12302 feature = "csharp",
12303 feature = "elixir",
12304 feature = "go",
12305 feature = "groovy",
12306 feature = "irules",
12307 feature = "java",
12308 feature = "javascript",
12309 feature = "kotlin",
12310 feature = "lua",
12311 feature = "mozcpp",
12312 feature = "mozjs",
12313 feature = "objc",
12314 feature = "perl",
12315 feature = "php",
12316 feature = "python",
12317 feature = "ruby",
12318 feature = "rust",
12319 feature = "tcl",
12320 feature = "typescript",
12321 ))]
12322 #[test]
12323 fn trailing_newline_does_not_change_loc_or_mi() {
12324 for (lang, source) in UNTERMINATED_ONE_LINERS {
12325 let text = String::from_utf8_lossy(source);
12326 let bare = metrics_verbatim(*lang, source, MetricsOptions::default());
12327 let mut newline_terminated = source.to_vec();
12328 newline_terminated.push(b'\n');
12329 let terminated =
12330 metrics_verbatim(*lang, &newline_terminated, MetricsOptions::default());
12331
12332 assert_eq!(
12333 bare.loc.sloc(),
12334 terminated.loc.sloc(),
12335 "{lang:?} sloc {text:?}"
12336 );
12337 assert_eq!(
12338 bare.loc.ploc(),
12339 terminated.loc.ploc(),
12340 "{lang:?} ploc {text:?}"
12341 );
12342 assert_eq!(
12343 bare.loc.cloc(),
12344 terminated.loc.cloc(),
12345 "{lang:?} cloc {text:?}"
12346 );
12347 assert_eq!(
12348 bare.loc.lloc(),
12349 terminated.loc.lloc(),
12350 "{lang:?} lloc {text:?}"
12351 );
12352 assert_eq!(
12353 bare.loc.blank(),
12354 terminated.loc.blank(),
12355 "{lang:?} blank {text:?}"
12356 );
12357 // The MI knock-on: `mi::inputs_are_empty` short-circuits to
12358 // 0.0 on `sloc <= 0`, so before the fix every unterminated
12359 // one-liner reported MI 0 while its newline-terminated twin
12360 // reported a real score. Inputs are now identical, so the
12361 // three formulas agree bit-for-bit.
12362 assert_eq!(
12363 bare.mi.original(),
12364 terminated.mi.original(),
12365 "{lang:?} mi {text:?}"
12366 );
12367 assert_eq!(
12368 bare.mi.sei(),
12369 terminated.mi.sei(),
12370 "{lang:?} mi.sei {text:?}"
12371 );
12372 assert_eq!(
12373 bare.mi.visual_studio(),
12374 terminated.mi.visual_studio(),
12375 "{lang:?} mi.visual_studio {text:?}",
12376 );
12377 assert_ne!(bare.mi.original(), 0.0, "{lang:?} mi must not be zeroed");
12378 }
12379 }
12380
12381 /// The second #1067 symptom: `b"fn f(){}\n/// x"` reported `sloc == 1`
12382 /// with `ploc == 1` *and* `cloc == 1`, so `cloc + ploc > sloc`. The
12383 /// file has two rows; only the missing one made the sums disagree.
12384 #[cfg(feature = "rust")]
12385 #[test]
12386 fn unterminated_trailing_comment_upholds_the_cloc_ploc_invariant() {
12387 // expected: row 0 is code, row 1 is comment-only, nothing blank.
12388 let loc = rust_loc(b"fn f(){}\n/// x");
12389 assert_eq!(loc.sloc(), 2);
12390 assert_eq!(loc.ploc(), 1);
12391 assert_eq!(loc.cloc(), 1);
12392 assert_eq!(loc.blank(), 0);
12393 }
12394
12395 /// Degenerate inputs, pinned so the end-column rule in
12396 /// `Node::end_line` cannot drift into fabricating rows for files
12397 /// that have none.
12398 #[cfg(feature = "rust")]
12399 #[test]
12400 fn degenerate_inputs_report_their_real_row_count() {
12401 // No bytes, no rows.
12402 assert_eq!(rust_loc(b"").sloc(), 0);
12403 // One row of whitespace, unterminated: the root node is empty but
12404 // sits at column 3, so the row is real and counts as blank.
12405 let spaces = rust_loc(b" ");
12406 assert_eq!(spaces.sloc(), 1);
12407 assert_eq!(spaces.blank(), 1);
12408 // Newline-terminated whitespace was the #1087 carve-out: most
12409 // grammars collapse the root to a zero-width node at end-of-input
12410 // (`(1, 0)..(1, 0)` for `"\n"`), so the measured span had no rows
12411 // left to attribute and `sloc` was 0 for a file that plainly has
12412 // one. #1247 retired that: the unit's `sloc` span is now anchored
12413 // to the span the unit *reports*, which #1195 already anchored at
12414 // line 1, so a collapsed root no longer costs the file its rows.
12415 let one = rust_loc(b"\n");
12416 assert_eq!((one.sloc(), one.ploc(), one.blank()), (1, 0, 1));
12417 let two = rust_loc(b"\n\n");
12418 assert_eq!((two.sloc(), two.ploc(), two.blank()), (2, 0, 2));
12419 }
12420
12421 /// Every grammar whose `Loc` behaviour this module owns, including the
12422 /// two whose `compute` is the `implement_metric_trait!` no-op.
12423 ///
12424 /// [`UNTERMINATED_ONE_LINERS`] carries a fixture per language because
12425 /// its sweeps need parseable code; the whitespace-only sweep needs
12426 /// only the language, and must not omit `Preproc`/`Ccomment` — their
12427 /// synthetic Unit root is anchored like any other, so they carry a
12428 /// real span and answer the #1087/#1247 question too.
12429 #[cfg(any(
12430 feature = "bash",
12431 feature = "c",
12432 feature = "c-family-helpers",
12433 feature = "cpp",
12434 feature = "csharp",
12435 feature = "elixir",
12436 feature = "go",
12437 feature = "groovy",
12438 feature = "irules",
12439 feature = "java",
12440 feature = "javascript",
12441 feature = "kotlin",
12442 feature = "lua",
12443 feature = "mozcpp",
12444 feature = "mozjs",
12445 feature = "objc",
12446 feature = "perl",
12447 feature = "php",
12448 feature = "python",
12449 feature = "ruby",
12450 feature = "rust",
12451 feature = "tcl",
12452 feature = "typescript",
12453 ))]
12454 fn all_loc_grammars() -> impl Iterator<Item = crate::LANG> {
12455 UNTERMINATED_ONE_LINERS
12456 .iter()
12457 .map(|(lang, _)| *lang)
12458 .chain([crate::LANG::Preproc, crate::LANG::Ccomment])
12459 }
12460
12461 /// #1087 accepted whitespace-only source as the one input class where
12462 /// a trailing newline moved `sloc` — twenty grammars collapse the root
12463 /// to a zero-width node at end-of-input, so `" "` reported one row
12464 /// and `" \n"` reported none, while five grammars (Elixir, Tcl,
12465 /// iRules, Preproc, Ccomment) kept the span and were newline-
12466 /// independent already. This sweep was written to pin both halves.
12467 ///
12468 /// #1247 removed the premise. The carve-out was a consequence of
12469 /// measuring the unit's `sloc` span from the root node's first token;
12470 /// once that span is anchored to the one the unit *reports* (#1195
12471 /// anchored the reported span at line 1), a collapsed root no longer
12472 /// erases the file's rows and every grammar answers alike. The sweep
12473 /// stays, with the split list retired: it now pins the *absence* of a
12474 /// per-grammar difference, which is the property a future grammar bump
12475 /// or walker change could still break.
12476 ///
12477 /// The unterminated side is unchanged and was always uniform: every
12478 /// grammar reports the row, as one blank line.
12479 #[cfg(all(
12480 feature = "bash",
12481 feature = "c",
12482 any(
12483 feature = "c",
12484 feature = "c-family-helpers",
12485 feature = "cpp",
12486 feature = "mozcpp"
12487 ),
12488 feature = "cpp",
12489 feature = "csharp",
12490 feature = "elixir",
12491 feature = "go",
12492 feature = "groovy",
12493 feature = "irules",
12494 feature = "java",
12495 feature = "javascript",
12496 feature = "kotlin",
12497 feature = "lua",
12498 feature = "mozcpp",
12499 feature = "mozjs",
12500 feature = "objc",
12501 feature = "perl",
12502 feature = "php",
12503 feature = "python",
12504 feature = "ruby",
12505 feature = "rust",
12506 feature = "tcl",
12507 feature = "typescript",
12508 ))]
12509 #[test]
12510 fn whitespace_only_input_is_uniform_across_grammars() {
12511 // Spaces and tabs both, so a grammar that lexes one as extra and
12512 // the other as an error token cannot hide behind the sweep.
12513 for bare in [&b" "[..], b"\t\t"] {
12514 let mut newline_terminated = bare.to_vec();
12515 newline_terminated.push(b'\n');
12516 for lang in all_loc_grammars() {
12517 let text = String::from_utf8_lossy(bare);
12518 let unterminated = metrics_verbatim(lang, bare, MetricsOptions::default()).loc;
12519 let terminated =
12520 metrics_verbatim(lang, &newline_terminated, MetricsOptions::default()).loc;
12521
12522 assert_eq!(
12523 (unterminated.sloc(), unterminated.blank()),
12524 (1, 1),
12525 "{lang:?}: unterminated {text:?} is one blank row for every grammar"
12526 );
12527
12528 assert_eq!(
12529 (terminated.sloc(), terminated.blank()),
12530 (1, 1),
12531 "{lang:?}: newline-terminated {text:?} is the same one blank \
12532 row — whether the grammar collapses its root at \
12533 end-of-input is no longer observable in loc (#1247)"
12534 );
12535
12536 // Whitespace is never code and never a comment, whichever
12537 // side of the carve-out the grammar sits on. This is the
12538 // assertion #1135 broke for Tcl and iRules, whose row
12539 // terminator used to land in their PLOC catch-all.
12540 for loc in [&unterminated, &terminated] {
12541 assert_eq!(
12542 (loc.ploc(), loc.cloc(), loc.lloc()),
12543 (0, 0, 0),
12544 "{lang:?}: whitespace is neither code nor comment"
12545 );
12546 }
12547 }
12548 }
12549 }
12550
12551 /// #1247: the unit anchors its *reported* span at line 1 (#1195) but
12552 /// measured its `sloc` span from the root node's first token, so blank
12553 /// rows above that token counted in neither `sloc` nor `blank` — while
12554 /// byte-identical rows one line lower counted in both.
12555 ///
12556 /// Swept across four grammars because the per-language `Loc` impls
12557 /// mirror each other. Tcl is in the list because it was already
12558 /// *right*: its row terminator is a token child of the root, so the
12559 /// root already started at row 0 and Tcl reported `sloc 4` for the
12560 /// Rust fixture's `sloc 1`. A per-language fix would have had to know
12561 /// which grammars were which; the walker-level anchor does not, and
12562 /// Tcl is the case that catches one being applied twice.
12563 ///
12564 /// `space_verbatim`, not `check_metrics`: the shim trims leading and
12565 /// trailing newlines, which deletes this test's entire subject.
12566 #[cfg(all(feature = "c", feature = "python", feature = "rust", feature = "tcl"))]
12567 #[test]
12568 fn leading_blank_rows_count_in_the_units_sloc_and_blank() {
12569 #[cfg(any(feature = "c", feature = "python", feature = "rust", feature = "tcl"))]
12570 const LEADING_BLANKS: u64 = 3;
12571 for (lang, body, ploc) in [
12572 (crate::LANG::Rust, &b"fn a() {}\n"[..], 1),
12573 (crate::LANG::Python, b"def a():\n pass\n", 2),
12574 (crate::LANG::C, b"int f() { return 0; }\n", 1),
12575 (crate::LANG::Tcl, b"puts hi\n", 1),
12576 ] {
12577 let mut source = vec![b'\n'; LEADING_BLANKS as usize];
12578 source.extend_from_slice(body);
12579 let space = space_verbatim(lang, &source, MetricsOptions::default());
12580 let loc = &space.metrics.loc;
12581 assert_eq!(
12582 (loc.sloc(), loc.ploc(), loc.cloc(), loc.blank()),
12583 (LEADING_BLANKS + ploc, ploc, 0, LEADING_BLANKS),
12584 "{lang:?}: the leading rows are blank, not absent"
12585 );
12586 // The disagreement the issue is named for: the unit's own
12587 // reported span and its `sloc` are two spellings of one
12588 // number, and were not before.
12589 assert_eq!(
12590 loc.sloc() as usize,
12591 space.end_line - space.start_line + 1,
12592 "{lang:?}: sloc equals the rows of the unit's reported span"
12593 );
12594 }
12595 }
12596
12597 /// The two controls from #1247's evidence table. Both were already
12598 /// correct, and both are how the inconsistency was visible at all: a
12599 /// comment on line 1 flipped a byte-identical file from `sloc 1` to
12600 /// `sloc 4`, because comments are in the tree and blank rows are not.
12601 /// A fix that reached past the unit would move one of these.
12602 #[cfg(feature = "rust")]
12603 #[test]
12604 fn interior_blanks_and_leading_comments_are_unmoved_by_the_anchor() {
12605 // expected: rows 1 and 3 are code, row 2 is blank.
12606 let interior = rust_loc(b"fn a() {}\n\nfn b() {}\n");
12607 assert_eq!(
12608 (
12609 interior.sloc(),
12610 interior.ploc(),
12611 interior.cloc(),
12612 interior.blank()
12613 ),
12614 (3, 2, 0, 1)
12615 );
12616 // expected: row 1 is comment-only, rows 2-3 blank, row 4 code.
12617 let leading_comment = rust_loc(b"// c\n\n\nfn a() {}\n");
12618 assert_eq!(
12619 (
12620 leading_comment.sloc(),
12621 leading_comment.ploc(),
12622 leading_comment.cloc(),
12623 leading_comment.blank()
12624 ),
12625 (4, 1, 1, 2)
12626 );
12627 }
12628
12629 /// The anchor is gated on `SpaceKind::Unit`, and that gate is the
12630 /// entire separation between "the file starts at line 1" and "every
12631 /// space starts at line 1". Asserted on the nested space's `sloc` as
12632 /// well as its span, because only the `sloc` half is new.
12633 #[cfg(feature = "rust")]
12634 #[test]
12635 fn the_unit_anchor_does_not_reach_nested_spaces() {
12636 let space = space_verbatim(
12637 crate::LANG::Rust,
12638 b"\n\n\nfn a() {\n let x = 1;\n}\n",
12639 MetricsOptions::default(),
12640 );
12641 assert_eq!((space.start_line, space.end_line), (1, 6));
12642 assert_eq!(space.metrics.loc.sloc(), 6, "the file has six rows");
12643 let nested = &space.spaces[0];
12644 assert_eq!((nested.start_line, nested.end_line), (4, 6));
12645 assert_eq!(
12646 nested.metrics.loc.sloc(),
12647 3,
12648 "the function's own three rows, not the file's six"
12649 );
12650 assert_eq!(nested.metrics.loc.blank(), 0, "the function has no blanks");
12651 }
12652
12653 /// `Sloc::exclude_span` records each pruned subtree's rows and
12654 /// `sloc()` subtracts their cardinality from the enclosing span, so
12655 /// widening that span at the top could in principle desynchronise
12656 /// the two. It cannot: the rows the anchor adds are above the first
12657 /// token, and no pruned subtree can overlap them. Pinned rather than
12658 /// argued, since the failure mode is a silent `saturating_sub` clamp
12659 /// to 0 rather than a panic (#722, #1247, #1417).
12660 #[cfg(feature = "rust")]
12661 #[test]
12662 fn exclude_tests_pruning_composes_with_the_unit_anchor() {
12663 // Rows 1-3 blank, 4 `fn a`, 5 blank, 6 `#[test]`, 7-9 `fn t`.
12664 let source = b"\n\n\nfn a() {}\n\n#[test]\nfn t() {\n assert!(true);\n}\n";
12665 let kept = metrics_verbatim(
12666 crate::LANG::Rust,
12667 source,
12668 MetricsOptions::default().with_exclude_tests(true),
12669 )
12670 .loc;
12671 // The prune takes the `fn t` item's rows 7-9 and, since #1431,
12672 // the `#[test]` sibling on row 6 that marks it. What the anchor
12673 // decides is the other end: `blank` is 4 rather than 1, because
12674 // rows 1-3 are now inside the span the pruning subtracts from.
12675 assert_eq!(
12676 (kept.sloc(), kept.ploc(), kept.cloc(), kept.blank()),
12677 (5, 1, 0, 4),
12678 "the four pruned rows leave; the three leading blanks stay"
12679 );
12680
12681 let unpruned = rust_loc(source);
12682 assert_eq!(
12683 (unpruned.sloc(), unpruned.ploc(), unpruned.blank()),
12684 (9, 5, 4),
12685 "the same file unpruned — the anchor is what makes both blank counts 4"
12686 );
12687 }
12688
12689 /// Analyses `source` as Rust with `exclude_tests` on, byte-for-byte.
12690 /// `check_metrics` cannot express this: it trims and re-appends the
12691 /// trailing newline (destroying the row structure these fixtures are
12692 /// about) and its macro hard-binds `MetricsOptions`.
12693 ///
12694 /// Gated to match its three callers below, so a build without the
12695 /// Rust grammar has neither the tests nor an unused helper (#1446).
12696 #[cfg(feature = "rust")]
12697 fn rust_loc_pruned(source: &[u8]) -> Stats {
12698 metrics_verbatim(
12699 crate::LANG::Rust,
12700 source,
12701 crate::MetricsOptions::default().with_exclude_tests(true),
12702 )
12703 .loc
12704 }
12705
12706 /// #1417: `Sloc::exclude_span` recorded each pruned subtree's whole
12707 /// span as a *count*, so a row a retained sibling also occupies was
12708 /// subtracted anyway and `sloc` fell below `ploc`. Both one-liner
12709 /// spellings from the issue reported `sloc 0, ploc 1` — one row of
12710 /// code in a zero-row file.
12711 ///
12712 /// The pruned and unpruned readings are identical here, and that is
12713 /// the clearest statement of why: the whole row survives the prune,
12714 /// because `fn a()` is on it. `sloc 1` for a file that is more than
12715 /// half test code reads as under-subtraction and is not — nothing
12716 /// on that row can be removed without removing `fn a()` with it.
12717 ///
12718 /// A second, separate cause used to add a row here and is worth not
12719 /// confusing with this one: `should_skip_subtree` matched only the
12720 /// *item*, and a `#[cfg(test)]` / `#[test]` attribute is an
12721 /// `AttributeItem` sibling of it, so the attribute was walked
12722 /// normally and Rust's catch-all credited its start row to PLOC — a
12723 /// file that was 100% test code still reported `ploc 1` (#1431, now
12724 /// fixed). These fixtures put the attribute on the shared row, so
12725 /// that fix changes neither reading: what holds row 0 here is
12726 /// `fn a()`, not the attribute.
12727 #[cfg(feature = "rust")]
12728 #[test]
12729 fn a_pruned_item_sharing_a_row_with_retained_code_keeps_the_row() {
12730 for source in [
12731 &b"fn a() {} #[cfg(test)] mod t { #[test] fn x() {} }\n"[..],
12732 &b"#[cfg(test)] mod t { #[test] fn x() {} } fn a() {}\n"[..],
12733 ] {
12734 let text = String::from_utf8_lossy(source);
12735 let pruned = rust_loc_pruned(source);
12736 assert_eq!(
12737 (pruned.sloc(), pruned.ploc(), pruned.cloc(), pruned.blank()),
12738 (1, 1, 0, 0),
12739 "pruned {text:?}"
12740 );
12741
12742 let kept = rust_loc(source);
12743 assert_eq!(
12744 (kept.sloc(), kept.ploc(), kept.cloc(), kept.blank()),
12745 (1, 1, 0, 0),
12746 "unpruned {text:?} — the prune can remove no row here"
12747 );
12748 }
12749 }
12750
12751 /// The discriminator against the alternative fix #1417 rejected:
12752 /// clamping the excluded *count* to `span - |retained rows|`.
12753 ///
12754 /// On `shared_row` the truth is `sloc 3, ploc 2, blank 1`; before
12755 /// the fix it read `sloc 2, blank 0`; and the clamp reads
12756 /// `min(1, 3 - 2) = 1`, so `sloc 2, blank 0` — no improvement at
12757 /// all. It buys `ploc <= sloc` by moving the error into `blank`,
12758 /// which is the outcome #1398's rationale warns against. Measured
12759 /// against the built binary before the set-based fix landed, not
12760 /// reasoned about. Without this fixture the fix is indistinguishable
12761 /// from that alternative.
12762 ///
12763 /// `own_rows` is the control, in the same test because the two must
12764 /// not be confusable: it genuinely excludes rows — both of the
12765 /// pruned item's own, since #1431 took the attribute with it — where
12766 /// `shared_row` excludes none, so a fix cannot buy the invariant by
12767 /// under-subtracting in the ordinary rustfmt layout.
12768 ///
12769 /// **`shared_row` has no fixture-decay anchor, and none exists.**
12770 /// The rule in `.claude/rules/testing.md` asks for a second axis
12771 /// only the excluded construct contributes to, so that trimming it
12772 /// out of the fixture fails this test rather than quietly hollowing
12773 /// it. Deleting `#[cfg(test)] mod t {}` from row 0 leaves all four
12774 /// numbers at `(3, 2, 0, 1)` — measured, 0 of 3,390 lib tests fail
12775 /// — and that is not an oversight but the claim restated: a pruned
12776 /// item whose only row a retained sibling also occupies is *by
12777 /// definition* metrically indistinguishable from not being there.
12778 /// Every candidate axis collapses the same way. The unpruned
12779 /// reading is also `(3, 2, 0, 1)`, because `mod t {}` adds no row
12780 /// of its own; the space tree matches too, because the prune
12781 /// removes the space the construct would have opened. The coverage
12782 /// here is the production perturbation, which was run: this test is
12783 /// among the failures for dropping the `ploc` subtraction, for
12784 /// widening `exclude_span`'s bound by one, and for both `subtract`
12785 /// mutations. `own_rows` is anchored in the ordinary way — trim its
12786 /// pruned `mod` and `sloc 2` fails.
12787 #[cfg(feature = "rust")]
12788 #[test]
12789 fn a_blank_row_does_not_absorb_a_phantom_exclusion() {
12790 // Row 0 `fn a` and the pruned `mod t`, row 1 blank, row 2 `fn b`.
12791 let shared_row = rust_loc_pruned(b"fn a() {} #[cfg(test)] mod t {}\n\nfn b() {}\n");
12792 assert_eq!(
12793 (
12794 shared_row.sloc(),
12795 shared_row.ploc(),
12796 shared_row.cloc(),
12797 shared_row.blank()
12798 ),
12799 (3, 2, 0, 1),
12800 "nothing is excluded: row 0 carries `fn a`"
12801 );
12802
12803 // Row 0 `fn a`, row 1 blank, row 2 `#[cfg(test)]`, row 3 the
12804 // pruned `mod t` — rows 2 and 3 are both the prune's to take,
12805 // the attribute along with the item it marks (#1431).
12806 let own_rows = rust_loc_pruned(b"fn a() {}\n\n#[cfg(test)]\nmod t { #[test] fn x() {} }\n");
12807 assert_eq!(
12808 (
12809 own_rows.sloc(),
12810 own_rows.ploc(),
12811 own_rows.cloc(),
12812 own_rows.blank()
12813 ),
12814 (2, 1, 0, 1),
12815 "two of four rows excluded; only `fn a` and the blank stay"
12816 );
12817 }
12818
12819 /// The *comment* half of the settle, which the fixtures above cannot
12820 /// reach because every one of them has `cloc 0`.
12821 ///
12822 /// `Stats::settle_excluded_rows` subtracts three sets from the
12823 /// pruned rows — `ploc.lines` and both `Cloc` sets — and only the
12824 /// first was covered: deleting **both** comment subtractions failed
12825 /// 0 of 3,388 lib tests, while deleting the `ploc` one failed 3. A
12826 /// comment sharing a row with a pruned item is retained text on that
12827 /// row exactly as code would be, so the row must survive the prune.
12828 #[cfg(feature = "rust")]
12829 #[test]
12830 fn a_comment_sharing_a_pruned_items_row_keeps_that_row() {
12831 // Row 0 `#[cfg(test)]`, row 1 the pruned `mod t` and a trailing
12832 // comment. Both rows are pruned — the attribute since #1431 —
12833 // but row 1 holds retained text, so only row 0 is the prune's to
12834 // take. `sloc 1` against `sloc 0` is exactly the comment
12835 // subtraction: without it both rows go and a file with a
12836 // surviving comment reports no lines at all.
12837 let loc = rust_loc_pruned(b"#[cfg(test)]\nmod t {} // note\n");
12838 assert_eq!(loc.sloc(), 1, "the comment row survives the prune");
12839 assert_eq!(loc.cloc(), 1, "the trailing comment is still counted");
12840 assert_eq!(loc.ploc(), 0, "no production code is left on either row");
12841 assert!(
12842 loc.ploc() <= loc.sloc(),
12843 "ploc {} must not exceed sloc {}",
12844 loc.ploc(),
12845 loc.sloc()
12846 );
12847 }
12848
12849 /// The non-unit half of the same off-by-one. tree-sitter-perl's
12850 /// `function_definition` swallows the newline after the closing brace
12851 /// of a file's **last** `sub`, so that node's span ends at column 0 of
12852 /// a row it does not occupy. The old unconditional `+ 1` credited that
12853 /// row, inflating the last sub of every Perl file by one line — here,
12854 /// reporting a 3-row `sub` as 4.
12855 #[cfg(feature = "perl")]
12856 #[test]
12857 fn perl_last_sub_does_not_absorb_the_trailing_newline() {
12858 // Two identical 3-row subs; only the second hits the quirk.
12859 let space = space_verbatim(
12860 crate::LANG::Perl,
12861 b"sub f {\n return 1;\n}\nsub g {\n return 2;\n}\n",
12862 MetricsOptions::default(),
12863 );
12864 assert_eq!(space.metrics.loc.sloc(), 6, "the file has six rows");
12865 let subs: Vec<u64> = space
12866 .spaces
12867 .iter()
12868 .map(|child| child.metrics.loc.sloc())
12869 .collect();
12870 assert_eq!(subs, vec![3, 3], "both subs occupy three rows");
12871 }
12872
12873 /// #1398, whose mechanism [`Stats::clamp_line_sets_to_span`]
12874 /// documents: a recovery token starting one row past end-of-input
12875 /// reached a catch-all's leaf branch and became a line of code, so
12876 /// `ploc` came out one above `sloc`.
12877 ///
12878 /// The assertion is exact equality with the *unchanged* `sloc`, in
12879 /// both directions. `ploc <= sloc` alone would also be satisfied by
12880 /// a clamp wide enough to delete real code rows, and every fixture
12881 /// here is all-code, so `ploc == sloc` is the only passing value.
12882 ///
12883 /// Each row additionally asserts the tree still holds a node past
12884 /// the file's last row. Without that anchor every fixture decays
12885 /// into an ordinary all-code file the moment someone edits the
12886 /// source string, and `ploc == sloc` is then true of any
12887 /// well-formed input — measured: replacing all fifteen fixtures
12888 /// with well-formed sources of the same row count leaves this test
12889 /// green. The phantom node *is* the subject, so it is the axis to
12890 /// anchor on (`.claude/rules/testing.md`, "Perturb the fixture as
12891 /// well as the production line").
12892 ///
12893 /// Writing that anchor as `has_error()` — the obvious choice — is
12894 /// wrong, and finding out why is worth the paragraph: Ruby's
12895 /// `x = <<~DOC\na\n` parses with **no error node at all**, and
12896 /// still gets a zero-width `heredoc_end` at `(3, 1)` of a two-row
12897 /// file. A grammar need not report failure to hand back a span it
12898 /// cannot honour, which is half of why this defect survived.
12899 ///
12900 /// `metrics_verbatim`, not `check_metrics`, for a duller reason
12901 /// than #1051's: a `LANG`-keyed table cannot dispatch the generic
12902 /// `check_metrics::<T>` shim, and that shim's `fn` callback returns
12903 /// nothing. Every fixture here is already a fixed point of the
12904 /// trailing-newline normalisation, so — measured — a
12905 /// `check_metrics` twin of any of these rows fails against unfixed
12906 /// code too. This is *not* the EOF class both harnesses hide.
12907 #[cfg(any(
12908 feature = "bash",
12909 feature = "c",
12910 feature = "cpp",
12911 feature = "elixir",
12912 feature = "groovy",
12913 feature = "lua",
12914 feature = "mozcpp",
12915 feature = "objc",
12916 feature = "perl",
12917 feature = "ruby",
12918 ))]
12919 #[test]
12920 fn a_recovery_token_past_the_span_is_not_a_code_row() {
12921 // Every fixture that reproduced the defect, one row per
12922 // language, as `(language, source, rows the file has, ploc
12923 // before #1398)`. That last column is what makes this a
12924 // regression test rather than a restatement of the fixed
12925 // behaviour: a reviewer can see which row the assertion is
12926 // about, and it is one above `sloc` in every case.
12927 //
12928 // Four unrelated recovery shapes are represented, which is the
12929 // argument for one clamp over ten per-arm skips: a heredoc
12930 // whose terminator never arrives (Bash, Perl, Ruby), an
12931 // unterminated string delimiter (Elixir, Lua, Groovy), an
12932 // unclosed bracket (Ruby's `%w[`), and a dangling line
12933 // continuation (the C family, where the `\` splices a row that
12934 // is not there).
12935 const FIXTURES: &[(crate::LANG, &[u8], u64, u64)] = &[
12936 #[cfg(feature = "bash")]
12937 (crate::LANG::Bash, b"cat <<DOC\na\n", 2, 3),
12938 #[cfg(feature = "c")]
12939 (crate::LANG::C, b"int x = \\\n", 1, 2),
12940 #[cfg(feature = "cpp")]
12941 (crate::LANG::Cpp, b"int x = \\\n", 1, 2),
12942 #[cfg(feature = "mozcpp")]
12943 (crate::LANG::Mozcpp, b"int x = \\\n", 1, 2),
12944 #[cfg(feature = "objc")]
12945 (crate::LANG::Objc, b"int x = \\\n", 1, 2),
12946 // The issue's reproduction spells this `x = "abc\n\n\n`;
12947 // the trailing blank rows never reach the parser, because
12948 // `normalize_line_endings` collapses a run of trailing
12949 // newlines to one before any file-based caller sees it.
12950 #[cfg(feature = "elixir")]
12951 (crate::LANG::Elixir, b"x = \"abc\n", 1, 2),
12952 #[cfg(feature = "elixir")]
12953 (crate::LANG::Elixir, b"x = \"\"\"\nabc\n", 2, 3),
12954 #[cfg(feature = "elixir")]
12955 (crate::LANG::Elixir, b"x = ~s(\n", 1, 2),
12956 // Not a truncation, and the only row here reachable from a
12957 // file nobody cut short: Elixir comments with `#`, so a
12958 // C-style block comment is a whole-line parse failure. The
12959 // shape reaches the same phantom row, which is the point —
12960 // "malformed" is not a synonym for "truncated".
12961 #[cfg(feature = "elixir")]
12962 (crate::LANG::Elixir, b"/* block */\n", 1, 2),
12963 #[cfg(feature = "groovy")]
12964 (crate::LANG::Groovy, b"def x = \"\"\"abc\n", 1, 2),
12965 #[cfg(feature = "lua")]
12966 (crate::LANG::Lua, b"x = \"abc\n", 1, 2),
12967 #[cfg(feature = "lua")]
12968 (crate::LANG::Lua, b"x = 'abc\n", 1, 2),
12969 #[cfg(feature = "perl")]
12970 (crate::LANG::Perl, b"my $x = <<DOC;\na\n", 2, 3),
12971 #[cfg(feature = "ruby")]
12972 (crate::LANG::Ruby, b"x = <<~DOC\na\n", 2, 3),
12973 #[cfg(feature = "ruby")]
12974 (crate::LANG::Ruby, b"x = %w[\n", 1, 2),
12975 ];
12976
12977 crate::test_support::assert_fixtures_present(FIXTURES);
12978 for &(lang, source, rows, ploc_before_1398) in FIXTURES {
12979 let text = String::from_utf8_lossy(source);
12980 // The anchor: the tree still contains a node starting past
12981 // the file's last row. That node *is* this test's subject,
12982 // so an edit that repaired the fixture — or a grammar bump
12983 // that stopped emitting the phantom — fails here rather
12984 // than leaving the metric assertions true for an unrelated
12985 // reason.
12986 //
12987 // Not `has_error()`, which would be the obvious anchor and
12988 // is wrong: `x = <<~DOC\na\n` parses **cleanly** in Ruby,
12989 // phantom `heredoc_end` at `(3, 1)` and all. A clean parse
12990 // is not evidence of an in-span tree, which is half of why
12991 // this defect went unnoticed.
12992 let ast = crate::Ast::parse(crate::Source::new(lang, source))
12993 .expect("an enabled language parses, error tree or not");
12994 let mut stack = vec![ast.root_node()];
12995 let mut phantom = None;
12996 while let Some(node) = stack.pop() {
12997 if node.start_row() as u64 >= rows {
12998 phantom = Some(node.start_row());
12999 break;
13000 }
13001 stack.extend(node.children());
13002 }
13003 assert!(
13004 phantom.is_some(),
13005 "{lang:?}: {text:?} no longer produces a node past row {rows}, \
13006 so it no longer exercises #1398"
13007 );
13008
13009 let loc = metrics_verbatim(lang, source, MetricsOptions::default()).loc;
13010 assert_eq!(loc.sloc(), rows, "{lang:?}: {text:?} occupies {rows} rows");
13011 assert_eq!(
13012 loc.ploc(),
13013 rows,
13014 "{lang:?}: every row of {text:?} is code and no row past it is \
13015 (was {ploc_before_1398} before #1398)"
13016 );
13017 // One-sided, unlike the two above: every fixture is all
13018 // code, so this cannot catch a clamp that over-reaches into
13019 // the comment sets — an over-reach can only leave 0 at 0.
13020 // What it does pin is the other direction, a future change
13021 // that files the phantom row as a *comment* instead. The
13022 // CLOC half of the clamp is covered by
13023 // `an_unterminated_pod_block_does_not_comment_a_row_past_eof`,
13024 // which is its only guard.
13025 assert_eq!((loc.cloc(), loc.blank()), (0, 0), "{lang:?}: {text:?}");
13026 }
13027 }
13028
13029 /// The `span == 0` arm of [`Stats::clamp_line_sets_to_span`] clears
13030 /// both line sets, so its precondition — that no walk arrives here
13031 /// holding a row — is the thing worth pinning. Nothing else can:
13032 /// the two assertions at the end of that function compare against
13033 /// `span`, which the clear forces to `0 <= 0` whatever it destroyed.
13034 ///
13035 /// This test used to assert the clearing itself, seeding row 0
13036 /// because it is the one row a `(0, 0)` spelling would wrongly keep.
13037 /// That is now covered a layer down by
13038 /// `line_set::tests::retain_range_inverted_empties_the_set`, which
13039 /// seeds `[0, 1, 400]` and asserts row 0 specifically is gone — so
13040 /// the seed here is free to become what it always described: the
13041 /// state the guard exists to reject.
13042 ///
13043 /// A fixture cannot reach this branch with a populated set at all —
13044 /// `span == 0` needs a space opened on a genuinely zero-width node
13045 /// at column 0, and every `is_func_space` matches compound
13046 /// productions — so seeding `Stats` directly is the only shape that
13047 /// can (`.claude/rules/testing.md`, "Pair any end-to-end test with a
13048 /// direct unit test on the function whose contract is verified").
13049 #[test]
13050 #[cfg(debug_assertions)]
13051 #[should_panic(expected = "zero-span space reached the clamp holding")]
13052 fn a_zero_span_holding_rows_trips_the_guard() {
13053 let mut stats = Stats::default();
13054 // The empty file's span: `0..0`, covering no row at all.
13055 stats.init_unit_span(0, 0);
13056 stats.ploc.lines.insert(0);
13057 assert_eq!((stats.sloc(), stats.ploc()), (0, 1));
13058
13059 stats.clamp_line_sets_to_span();
13060 }
13061
13062 /// The companion to the guard above: an *empty* zero-span space is
13063 /// the state every real walk arrives in, and it must pass through
13064 /// the clamp untouched rather than trip the assertion.
13065 #[test]
13066 fn a_zero_span_without_rows_clamps_cleanly() {
13067 let mut stats = Stats::default();
13068 stats.init_unit_span(0, 0);
13069
13070 stats.clamp_line_sets_to_span();
13071
13072 assert_eq!(
13073 (stats.sloc(), stats.ploc(), stats.cloc()),
13074 (0, 0, 0),
13075 "a span of no rows retains no row"
13076 );
13077 }
13078
13079 /// The phantom row lands on whichever space was open when the
13080 /// recovery token was visited, so the contract has to hold per
13081 /// space and not only on the file. Elixir's unterminated `do` block
13082 /// is the case that separates the two: before #1398 the `defmodule`
13083 /// space reported `ploc 2` against its own `sloc 1`, and so did the
13084 /// unit, and a unit-only clamp would have left the nested space
13085 /// wrong while the file read as fixed.
13086 ///
13087 /// `space_verbatim`, not `metrics_verbatim`: the claim is about the
13088 /// nested space, whose numbers the root aggregate unions away.
13089 #[cfg(feature = "elixir")]
13090 #[test]
13091 fn the_clamp_reaches_a_nested_space_not_only_the_unit() {
13092 let space = space_verbatim(
13093 crate::LANG::Elixir,
13094 b"defmodule M do\n",
13095 MetricsOptions::default(),
13096 );
13097 assert_eq!(
13098 (space.metrics.loc.sloc(), space.metrics.loc.ploc()),
13099 (1, 1),
13100 "the unit is one row, all code"
13101 );
13102 let module = space
13103 .spaces
13104 .first()
13105 .expect("the `defmodule` opens a space even unterminated");
13106 assert_eq!(module.name.as_deref(), Some("M"));
13107 assert_eq!(
13108 (module.metrics.loc.sloc(), module.metrics.loc.ploc()),
13109 (1, 1),
13110 "the module's own space is one row too — `ploc` was 2 before #1398"
13111 );
13112 }
13113
13114 /// The same phantom row reaches CLOC, which has its own contract:
13115 /// `cloc > sloc` pushes MI's comments percentage above 100%, the
13116 /// defect #461 fixed for co-located comments and left open for a
13117 /// comment node ending past end-of-input. An unterminated Perl POD
13118 /// block is the reproduction the #1398 sweep turned up — two rows,
13119 /// `cloc 3`.
13120 ///
13121 /// Pinned alongside the PLOC sweep because the clamp covers all
13122 /// three line sets in one pass; without this row the CLOC half of
13123 /// `Stats::clamp_line_sets_to_span` has no test.
13124 #[cfg(feature = "perl")]
13125 #[test]
13126 fn an_unterminated_pod_block_does_not_comment_a_row_past_eof() {
13127 let loc =
13128 metrics_verbatim(crate::LANG::Perl, b"=pod\nabc\n", MetricsOptions::default()).loc;
13129 // expected: both rows are POD, so both are comment-only and
13130 // neither is code or blank. `cloc` was 3 before #1398.
13131 assert_eq!((loc.sloc(), loc.cloc()), (2, 2));
13132 assert_eq!((loc.ploc(), loc.blank()), (0, 0));
13133 }
13134
13135 /// #1135: Tcl and its iRules dialect are the only grammars here that
13136 /// surface the row terminator as a token child of the root. `LF`'s
13137 /// start row is the row it *terminates*, so the `_` catch-all in
13138 /// their `Loc` impls inserted that row into PLOC — turning every
13139 /// comment-only and whitespace-only row into a line of code.
13140 ///
13141 /// A wholly empty row never showed the defect: the `LF` that starts
13142 /// on it is the one tree-sitter collapses at end-of-input. A row of
13143 /// *whitespace* does, and trailing whitespace on an otherwise blank
13144 /// line is ordinary in real source — which is what makes the two
13145 /// spellings' disagreement the sharpest assertion here.
13146 ///
13147 /// Uses [`metrics_verbatim`] so the fixtures reach the parser
13148 /// byte-for-byte; `check_metrics` rewrites the trailing newline.
13149 #[cfg(all(feature = "irules", feature = "tcl"))]
13150 #[test]
13151 fn tcl_family_does_not_count_terminator_rows_as_code() {
13152 for lang in [crate::LANG::Tcl, crate::LANG::Irules] {
13153 // Three rows: code, whitespace-only, code.
13154 let padded = metrics_verbatim(
13155 lang,
13156 b"proc f {} {}\n \nproc g {} {}\n",
13157 MetricsOptions::default(),
13158 )
13159 .loc;
13160 assert_eq!(padded.sloc(), 3, "{lang:?} sloc");
13161 assert_eq!(padded.ploc(), 2, "{lang:?} ploc");
13162 assert_eq!(padded.blank(), 1, "{lang:?} blank");
13163
13164 // Whether a blank row carries spaces is not a property of the
13165 // code, so the empty-row spelling must agree exactly. Pre-fix
13166 // this side stayed correct while the padded side reported
13167 // `ploc 3 / blank 0`.
13168 let empty = metrics_verbatim(
13169 lang,
13170 b"proc f {} {}\n\nproc g {} {}\n",
13171 MetricsOptions::default(),
13172 )
13173 .loc;
13174 assert_eq!(
13175 (padded.sloc(), padded.ploc(), padded.blank()),
13176 (empty.sloc(), empty.ploc(), empty.blank()),
13177 "{lang:?}: trailing whitespace on a blank row must not make it code"
13178 );
13179
13180 // The comment-only half of the same defect. It escaped
13181 // `unterminated_one_line_file_reports_one_source_line`
13182 // because that sweep's fixtures are one-liners with no
13183 // comment row. The cross-language version of this case is
13184 // `a_comment_row_is_never_counted_as_code`; the two rows here
13185 // stay so the Tcl-family regression reads in one place.
13186 let commented = metrics_verbatim(
13187 lang,
13188 b"# lead-in\nproc f {} {}\n",
13189 MetricsOptions::default(),
13190 )
13191 .loc;
13192 assert_eq!(commented.sloc(), 2, "{lang:?} commented sloc");
13193 assert_eq!(commented.ploc(), 1, "{lang:?} commented ploc");
13194 assert_eq!(commented.cloc(), 1, "{lang:?} commented cloc");
13195 assert_eq!(commented.blank(), 0, "{lang:?} commented blank");
13196 }
13197 }
13198
13199 /// The comment spellings to sweep for each language: the line form
13200 /// every language has, then the block and doc forms where one exists.
13201 ///
13202 /// Nothing here needs to be exhaustive per language — the defect this
13203 /// guards against is a stray *token* inside (or terminating) a
13204 /// comment node reaching a PLOC catch-all, which any one spelling of
13205 /// a comment exposes. The block and doc entries are there because
13206 /// those nodes have child tokens the line form does not.
13207 #[cfg(any(
13208 feature = "bash",
13209 feature = "c",
13210 feature = "cpp",
13211 feature = "csharp",
13212 feature = "elixir",
13213 feature = "go",
13214 feature = "groovy",
13215 feature = "irules",
13216 feature = "java",
13217 feature = "javascript",
13218 feature = "kotlin",
13219 feature = "lua",
13220 feature = "mozcpp",
13221 feature = "mozjs",
13222 feature = "objc",
13223 feature = "perl",
13224 feature = "php",
13225 feature = "python",
13226 feature = "ruby",
13227 feature = "rust",
13228 feature = "tcl",
13229 feature = "typescript",
13230 ))]
13231 fn comment_spellings(lang: crate::LANG) -> &'static [&'static str] {
13232 use crate::LANG::*;
13233 match lang {
13234 Python | Ruby | Bash | Elixir | Tcl | Irules | Perl => &["# c"],
13235 Lua => &["-- c", "--[[ c ]]"],
13236 Php => &["# c", "/* c */", "/** c */"],
13237 Rust => &["// c", "/* c */", "/// c"],
13238 _ => &["// c", "/* c */"],
13239 }
13240 }
13241
13242 /// A comment-only row is never a physical line of code — in any
13243 /// language, in any comment spelling, on either side of the code.
13244 ///
13245 /// Two separate defects broke this, both by letting a token reach the
13246 /// `_` catch-all that ends `stats.ploc.lines.insert(start)`. In Tcl
13247 /// and iRules it was the row terminator, whose start row is the row
13248 /// it terminates (#1135). In Perl it was the `#` *inside* the
13249 /// `comments` node, which additionally tripped
13250 /// `check_comment_ends_on_code_line` into reclassifying the row from
13251 /// comment-only to code-and-comment (#1137).
13252 ///
13253 /// Neither was visible to the per-language `*_cloc` tests — several
13254 /// assert `cloc` and `blank` and leave `ploc` unpinned — nor to
13255 /// `unterminated_one_line_file_reports_one_source_line`, whose
13256 /// fixtures carry no comment row at all. Since the failure mode is
13257 /// structural rather than language-specific, the sweep is per
13258 /// language rather than a sample.
13259 #[cfg(all(
13260 feature = "bash",
13261 feature = "c",
13262 feature = "cpp",
13263 feature = "csharp",
13264 feature = "elixir",
13265 feature = "go",
13266 feature = "groovy",
13267 feature = "irules",
13268 feature = "java",
13269 feature = "javascript",
13270 feature = "kotlin",
13271 feature = "lua",
13272 feature = "mozcpp",
13273 feature = "mozjs",
13274 feature = "objc",
13275 feature = "perl",
13276 feature = "php",
13277 feature = "python",
13278 feature = "ruby",
13279 feature = "rust",
13280 feature = "tcl",
13281 feature = "typescript",
13282 ))]
13283 #[test]
13284 fn a_comment_row_is_never_counted_as_code() {
13285 for (lang, code) in UNTERMINATED_ONE_LINERS {
13286 // PHP is the one language whose code fixture must open the
13287 // file: outside `<?php` every row is inline HTML, so a
13288 // comment placed before it is not a comment at all.
13289 let must_lead = *lang == crate::LANG::Php;
13290 for &comment in comment_spellings(*lang) {
13291 let orders: &[bool] = if must_lead { &[false] } else { &[true, false] };
13292 for &comment_first in orders {
13293 let mut src = Vec::new();
13294 let (first, second): (&[u8], &[u8]) = if comment_first {
13295 (comment.as_bytes(), code)
13296 } else {
13297 (code, comment.as_bytes())
13298 };
13299 src.extend_from_slice(first);
13300 src.push(b'\n');
13301 src.extend_from_slice(second);
13302 src.push(b'\n');
13303
13304 let loc = metrics_verbatim(*lang, &src, MetricsOptions::default()).loc;
13305 let text = String::from_utf8_lossy(&src);
13306 assert_eq!(loc.sloc(), 2, "{lang:?} sloc for {text:?}");
13307 assert_eq!(
13308 loc.ploc(),
13309 1,
13310 "{lang:?} ploc for {text:?} — the comment row is not code"
13311 );
13312 assert_eq!(loc.cloc(), 1, "{lang:?} cloc for {text:?}");
13313 assert_eq!(loc.blank(), 0, "{lang:?} blank for {text:?}");
13314 }
13315 }
13316 }
13317 }
13318
13319 /// A `var` / `let` / `const` declaration is an executable statement and
13320 /// counts one LLOC, the same as Java's `LocalVariableDeclaration` and
13321 /// Rust's `let` (#1283 — before the fix a declarations-only file
13322 /// reported `lloc 0`). The fourth row is one `variable_declaration`
13323 /// carrying two declarators, so it counts once, not twice. The fifth row
13324 /// is a `using_declaration` — the grammar's third executable declaration
13325 /// kind, which TypeScript and TSX do not have.
13326 #[cfg(feature = "javascript")]
13327 #[test]
13328 fn javascript_declaration_lloc() {
13329 check_metrics::<JavascriptParser>(
13330 "var a = 1;\nlet b = 2;\nconst c = 3;\nvar d = 4, e = 5;\nusing r = open();\n",
13331 "foo.js",
13332 |metric| {
13333 assert_eq!(metric.loc.sloc(), 5);
13334 assert_eq!(metric.loc.ploc(), 5);
13335 assert_eq!(metric.loc.lloc(), 5);
13336 assert_eq!(metric.loc.cloc(), 0);
13337 assert_eq!(metric.loc.blank(), 0);
13338 },
13339 );
13340 }
13341
13342 /// The classic `for (var i = 0; …)` header is part of the `ForStatement`,
13343 /// which already counts its own LLOC, so the header declaration must not
13344 /// add a second one. `for (const x of …)` and `for (var k in …)` need no
13345 /// carve-out at all: the grammar inlines the `const` / `var` keyword into
13346 /// `for_in_statement` and emits no declaration node — they are here so a
13347 /// carve-out wrongly widened to `ForInStatement` still has an input that
13348 /// notices. `var s = i;` in the loop *body* is a real logical line: the
13349 /// `StatementBlock` stops the ancestor walk (#1283).
13350 ///
13351 /// expected: for-statement 1 + body declaration 1 + for-of 1 + for-in 1 = 4
13352 #[cfg(feature = "javascript")]
13353 #[test]
13354 fn javascript_for_header_declaration_not_double_counted() {
13355 check_metrics::<JavascriptParser>(
13356 "function f(arr, obj) {\n for (var i = 0; i < 3; i++) {\n var s = i;\n }\n for (const x of arr) {}\n for (var k in obj) {}\n}\n",
13357 "foo.js",
13358 |metric| {
13359 assert_eq!(metric.loc.sloc(), 7);
13360 assert_eq!(metric.loc.ploc(), 7);
13361 assert_eq!(metric.loc.lloc(), 4);
13362 assert_eq!(metric.loc.cloc(), 0);
13363 assert_eq!(metric.loc.blank(), 0);
13364 },
13365 );
13366 }
13367
13368 /// `export const a = 1;` is one logical line, not two: the declaration
13369 /// nests inside the `ExportStatement`, whose arm already counted the row.
13370 /// The declaration inside the exported function body still counts — the
13371 /// `StatementBlock` stops the ancestor walk before the `ExportStatement`
13372 /// is reached (#1283).
13373 ///
13374 /// expected: 4 export statements + the `const c = 4;` in `f`'s body = 5
13375 #[cfg(feature = "javascript")]
13376 #[test]
13377 fn javascript_exported_declaration_counts_once() {
13378 check_metrics::<JavascriptParser>(
13379 "export const a = 1;\nexport let b = 2;\nexport default 3;\nexport function f() { const c = 4; }\n",
13380 "foo.js",
13381 |metric| {
13382 assert_eq!(metric.loc.sloc(), 4);
13383 assert_eq!(metric.loc.ploc(), 4);
13384 assert_eq!(metric.loc.lloc(), 5);
13385 assert_eq!(metric.loc.cloc(), 0);
13386 assert_eq!(metric.loc.blank(), 0);
13387 },
13388 );
13389 }
13390
13391 /// A `var` / `let` / `const` declaration is an executable statement and
13392 /// counts one LLOC, the same as Java's `LocalVariableDeclaration` and
13393 /// Rust's `let` (#1283 — before the fix a declarations-only file
13394 /// reported `lloc 0`). The fourth row is one `variable_declaration`
13395 /// carrying two declarators, so it counts once, not twice. The fifth row
13396 /// is a `using_declaration` — the grammar's third executable declaration
13397 /// kind, which TypeScript and TSX do not have.
13398 #[cfg(feature = "mozjs")]
13399 #[test]
13400 fn mozjs_declaration_lloc() {
13401 check_metrics::<MozjsParser>(
13402 "var a = 1;\nlet b = 2;\nconst c = 3;\nvar d = 4, e = 5;\nusing r = open();\n",
13403 "foo.js",
13404 |metric| {
13405 assert_eq!(metric.loc.sloc(), 5);
13406 assert_eq!(metric.loc.ploc(), 5);
13407 assert_eq!(metric.loc.lloc(), 5);
13408 assert_eq!(metric.loc.cloc(), 0);
13409 assert_eq!(metric.loc.blank(), 0);
13410 },
13411 );
13412 }
13413
13414 /// The classic `for (var i = 0; …)` header is part of the `ForStatement`,
13415 /// which already counts its own LLOC, so the header declaration must not
13416 /// add a second one. `for (const x of …)` and `for (var k in …)` need no
13417 /// carve-out at all: the grammar inlines the `const` / `var` keyword into
13418 /// `for_in_statement` and emits no declaration node — they are here so a
13419 /// carve-out wrongly widened to `ForInStatement` still has an input that
13420 /// notices. `var s = i;` in the loop *body* is a real logical line: the
13421 /// `StatementBlock` stops the ancestor walk (#1283).
13422 ///
13423 /// expected: for-statement 1 + body declaration 1 + for-of 1 + for-in 1 = 4
13424 #[cfg(feature = "mozjs")]
13425 #[test]
13426 fn mozjs_for_header_declaration_not_double_counted() {
13427 check_metrics::<MozjsParser>(
13428 "function f(arr, obj) {\n for (var i = 0; i < 3; i++) {\n var s = i;\n }\n for (const x of arr) {}\n for (var k in obj) {}\n}\n",
13429 "foo.js",
13430 |metric| {
13431 assert_eq!(metric.loc.sloc(), 7);
13432 assert_eq!(metric.loc.ploc(), 7);
13433 assert_eq!(metric.loc.lloc(), 4);
13434 assert_eq!(metric.loc.cloc(), 0);
13435 assert_eq!(metric.loc.blank(), 0);
13436 },
13437 );
13438 }
13439
13440 /// `export const a = 1;` is one logical line, not two: the declaration
13441 /// nests inside the `ExportStatement`, whose arm already counted the row.
13442 /// The declaration inside the exported function body still counts — the
13443 /// `StatementBlock` stops the ancestor walk before the `ExportStatement`
13444 /// is reached (#1283).
13445 ///
13446 /// expected: 4 export statements + the `const c = 4;` in `f`'s body = 5
13447 #[cfg(feature = "mozjs")]
13448 #[test]
13449 fn mozjs_exported_declaration_counts_once() {
13450 check_metrics::<MozjsParser>(
13451 "export const a = 1;\nexport let b = 2;\nexport default 3;\nexport function f() { const c = 4; }\n",
13452 "foo.js",
13453 |metric| {
13454 assert_eq!(metric.loc.sloc(), 4);
13455 assert_eq!(metric.loc.ploc(), 4);
13456 assert_eq!(metric.loc.lloc(), 5);
13457 assert_eq!(metric.loc.cloc(), 0);
13458 assert_eq!(metric.loc.blank(), 0);
13459 },
13460 );
13461 }
13462
13463 /// A `var` / `let` / `const` declaration is an executable statement and
13464 /// counts one LLOC, the same as Java's `LocalVariableDeclaration` and
13465 /// Rust's `let` (#1283 — before the fix a declarations-only file
13466 /// reported `lloc 0`). The fourth row is one `variable_declaration`
13467 /// carrying two declarators, so it counts once, not twice.
13468 #[cfg(feature = "typescript")]
13469 #[test]
13470 fn typescript_declaration_lloc() {
13471 check_metrics::<TypescriptParser>(
13472 "var a: number = 1;\nlet b = 2;\nconst c = 3;\nvar d = 4, e = 5;\n",
13473 "foo.ts",
13474 |metric| {
13475 assert_eq!(metric.loc.sloc(), 4);
13476 assert_eq!(metric.loc.ploc(), 4);
13477 assert_eq!(metric.loc.lloc(), 4);
13478 assert_eq!(metric.loc.cloc(), 0);
13479 assert_eq!(metric.loc.blank(), 0);
13480 },
13481 );
13482 }
13483
13484 /// The classic `for (var i = 0; …)` header is part of the `ForStatement`,
13485 /// which already counts its own LLOC, so the header declaration must not
13486 /// add a second one. `for (const x of …)` and `for (var k in …)` need no
13487 /// carve-out at all: the grammar inlines the `const` / `var` keyword into
13488 /// `for_in_statement` and emits no declaration node — they are here so a
13489 /// carve-out wrongly widened to `ForInStatement` still has an input that
13490 /// notices. `var s = i;` in the loop *body* is a real logical line: the
13491 /// `StatementBlock` stops the ancestor walk (#1283).
13492 ///
13493 /// expected: for-statement 1 + body declaration 1 + for-of 1 + for-in 1 = 4
13494 #[cfg(feature = "typescript")]
13495 #[test]
13496 fn typescript_for_header_declaration_not_double_counted() {
13497 check_metrics::<TypescriptParser>(
13498 "function f(arr, obj) {\n for (var i = 0; i < 3; i++) {\n var s = i;\n }\n for (const x of arr) {}\n for (var k in obj) {}\n}\n",
13499 "foo.ts",
13500 |metric| {
13501 assert_eq!(metric.loc.sloc(), 7);
13502 assert_eq!(metric.loc.ploc(), 7);
13503 assert_eq!(metric.loc.lloc(), 4);
13504 assert_eq!(metric.loc.cloc(), 0);
13505 assert_eq!(metric.loc.blank(), 0);
13506 },
13507 );
13508 }
13509
13510 /// `export const a = 1;` is one logical line, not two: the declaration
13511 /// nests inside the `ExportStatement`, whose arm already counted the row.
13512 /// The declaration inside the exported function body still counts — the
13513 /// `StatementBlock` stops the ancestor walk before the `ExportStatement`
13514 /// is reached (#1283).
13515 ///
13516 /// expected: 4 export statements + the `const c = 4;` in `f`'s body = 5
13517 ///
13518 /// The TypeScript spelling also pins `export declare const …`, where an
13519 /// `ambient_declaration` sits between the export and the declaration: the
13520 /// carve-out walks the ancestor chain rather than checking the parent, so
13521 /// it still sees the enclosing `ExportStatement`.
13522 #[cfg(feature = "typescript")]
13523 #[test]
13524 fn typescript_exported_declaration_counts_once() {
13525 check_metrics::<TypescriptParser>(
13526 "export const a: number = 1;\nexport declare const b: string;\nexport default 3;\nexport function f(): void { const c = 4; }\n",
13527 "foo.ts",
13528 |metric| {
13529 assert_eq!(metric.loc.sloc(), 4);
13530 assert_eq!(metric.loc.ploc(), 4);
13531 assert_eq!(metric.loc.lloc(), 5);
13532 assert_eq!(metric.loc.cloc(), 0);
13533 assert_eq!(metric.loc.blank(), 0);
13534 },
13535 );
13536 }
13537
13538 /// A `var` / `let` / `const` declaration is an executable statement and
13539 /// counts one LLOC, the same as Java's `LocalVariableDeclaration` and
13540 /// Rust's `let` (#1283 — before the fix a declarations-only file
13541 /// reported `lloc 0`). The fourth row is one `variable_declaration`
13542 /// carrying two declarators, so it counts once, not twice.
13543 #[cfg(feature = "typescript")]
13544 #[test]
13545 fn tsx_declaration_lloc() {
13546 check_metrics::<TsxParser>(
13547 "var a: number = 1;\nlet b = 2;\nconst c = 3;\nvar d = 4, e = 5;\n",
13548 "foo.tsx",
13549 |metric| {
13550 assert_eq!(metric.loc.sloc(), 4);
13551 assert_eq!(metric.loc.ploc(), 4);
13552 assert_eq!(metric.loc.lloc(), 4);
13553 assert_eq!(metric.loc.cloc(), 0);
13554 assert_eq!(metric.loc.blank(), 0);
13555 },
13556 );
13557 }
13558
13559 /// The classic `for (var i = 0; …)` header is part of the `ForStatement`,
13560 /// which already counts its own LLOC, so the header declaration must not
13561 /// add a second one. `for (const x of …)` and `for (var k in …)` need no
13562 /// carve-out at all: the grammar inlines the `const` / `var` keyword into
13563 /// `for_in_statement` and emits no declaration node — they are here so a
13564 /// carve-out wrongly widened to `ForInStatement` still has an input that
13565 /// notices. `var s = i;` in the loop *body* is a real logical line: the
13566 /// `StatementBlock` stops the ancestor walk (#1283).
13567 ///
13568 /// expected: for-statement 1 + body declaration 1 + for-of 1 + for-in 1 = 4
13569 #[cfg(feature = "typescript")]
13570 #[test]
13571 fn tsx_for_header_declaration_not_double_counted() {
13572 check_metrics::<TsxParser>(
13573 "function f(arr, obj) {\n for (var i = 0; i < 3; i++) {\n var s = i;\n }\n for (const x of arr) {}\n for (var k in obj) {}\n}\n",
13574 "foo.tsx",
13575 |metric| {
13576 assert_eq!(metric.loc.sloc(), 7);
13577 assert_eq!(metric.loc.ploc(), 7);
13578 assert_eq!(metric.loc.lloc(), 4);
13579 assert_eq!(metric.loc.cloc(), 0);
13580 assert_eq!(metric.loc.blank(), 0);
13581 },
13582 );
13583 }
13584
13585 /// `export const a = 1;` is one logical line, not two: the declaration
13586 /// nests inside the `ExportStatement`, whose arm already counted the row.
13587 /// The declaration inside the exported function body still counts — the
13588 /// `StatementBlock` stops the ancestor walk before the `ExportStatement`
13589 /// is reached (#1283).
13590 ///
13591 /// expected: 4 export statements + the `const c = 4;` in `f`'s body = 5
13592 ///
13593 /// The TypeScript spelling also pins `export declare const …`, where an
13594 /// `ambient_declaration` sits between the export and the declaration: the
13595 /// carve-out walks the ancestor chain rather than checking the parent, so
13596 /// it still sees the enclosing `ExportStatement`.
13597 #[cfg(feature = "typescript")]
13598 #[test]
13599 fn tsx_exported_declaration_counts_once() {
13600 check_metrics::<TsxParser>(
13601 "export const a: number = 1;\nexport declare const b: string;\nexport default 3;\nexport function f(): void { const c = 4; }\n",
13602 "foo.tsx",
13603 |metric| {
13604 assert_eq!(metric.loc.sloc(), 4);
13605 assert_eq!(metric.loc.ploc(), 4);
13606 assert_eq!(metric.loc.lloc(), 5);
13607 assert_eq!(metric.loc.cloc(), 0);
13608 assert_eq!(metric.loc.blank(), 0);
13609 },
13610 );
13611 }
13612
13613 /// A brace-less `for` body is a declaration with the *same* parent as
13614 /// the header, so a carve-out keyed on the enclosing kind alone
13615 /// dropped it (#1283 review): `for (…) var s = i;` counted one
13616 /// logical line where the braced spelling and `for (…) x++;` count
13617 /// two. The header is identified by the `initializer` field instead.
13618 /// The nested spelling — a `switch` as the brace-less body — is the
13619 /// same miss one level down.
13620 ///
13621 /// expected: for 1 + body declaration 1 = 2; for 1 + switch 1 +
13622 /// case declaration 1 = 3.
13623 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
13624 #[test]
13625 fn js_family_braceless_for_body_declaration_counts() {
13626 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
13627 const BODY: &str = "for (var i = 0; i < 3; i++) var s = i;\n";
13628 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
13629 const NESTED: &str = "for (let i = 0; i < 2; i++) switch (i) { case 0: let y = 1; }\n";
13630 check_metrics::<JavascriptParser>(BODY, "foo.js", |m| assert_eq!(m.loc.lloc(), 2));
13631 check_metrics::<MozjsParser>(BODY, "foo.js", |m| assert_eq!(m.loc.lloc(), 2));
13632 check_metrics::<TypescriptParser>(BODY, "foo.ts", |m| assert_eq!(m.loc.lloc(), 2));
13633 check_metrics::<TsxParser>(BODY, "foo.tsx", |m| assert_eq!(m.loc.lloc(), 2));
13634 check_metrics::<JavascriptParser>(NESTED, "foo.js", |m| assert_eq!(m.loc.lloc(), 3));
13635 check_metrics::<MozjsParser>(NESTED, "foo.js", |m| assert_eq!(m.loc.lloc(), 3));
13636 check_metrics::<TypescriptParser>(NESTED, "foo.ts", |m| assert_eq!(m.loc.lloc(), 3));
13637 check_metrics::<TsxParser>(NESTED, "foo.tsx", |m| assert_eq!(m.loc.lloc(), 3));
13638 }
13639
13640 /// Ambient declarations execute nothing — `declare const x: T;` has
13641 /// no initializer to run — so they are no logical line, whether the
13642 /// `declare` is top-level or the declaration sits inside a
13643 /// `declare namespace` / `declare module` body (#1283 review: only
13644 /// the `export declare` spelling was carved out, so a `.d.ts` file
13645 /// reported one LLOC per `declare const`).
13646 ///
13647 /// expected: 0 — every row is ambient.
13648 #[cfg(feature = "typescript")]
13649 #[test]
13650 fn typescript_ambient_declarations_are_not_logical_lines() {
13651 const SRC: &str = "declare const VERSION: string;\ndeclare let mutable: number;\ndeclare namespace NS { const inner: number; }\ndeclare module \"m\" { let y: string; }\n";
13652 check_metrics::<TypescriptParser>(SRC, "foo.ts", |m| assert_eq!(m.loc.lloc(), 0));
13653 check_metrics::<TsxParser>(SRC, "foo.tsx", |m| assert_eq!(m.loc.lloc(), 0));
13654 }
13655}