big-code-analysis 2.4.0

Tool to compute and export code metrics
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
#![allow(
    clippy::enum_glob_use,
    clippy::too_many_lines,
    clippy::wildcard_imports
)]
#![allow(
    clippy::cast_precision_loss,
    clippy::cast_possible_truncation,
    clippy::cast_sign_loss
)]

use super::{
    Abc, Stats, count_boolean_slot, count_each_operand, count_negated_operand, is_operand, peel,
    wrapped_operand,
};
use crate::lang_helpers::perl::{
    perl_and_is_operator, perl_bareword_is_call, perl_dash_signs_keyword_key, perl_not_is_key,
};
use crate::*;

// Fitzpatrick's ABC rules adapted for Perl.
//
// - Assignments: every assignment operator token — plain `=` plus the
//   compound forms `+=`, `-=`, `*=`, `/=`, `%=`, `**=`, `.=`, `x=`,
//   `&=`, `|=`, `^=`, `<<=`, `>>=`, `&&=`, `||=`, `//=`, and the
//   bitstring forms `&.=`, `|.=`, `^.=`. Each token fires exactly
//   once per textual occurrence inside a `binary_expression`.
// - Branches: every call expression dispatch — `call_expression_with_*`
//   (bareword / spaced args / args-with-brackets / sub / variable /
//   recursive) plus `method_invocation`. The grammar nests an inner
//   `call_expression_with_bareword` (just the function name)
//   underneath the wrapper kinds carrying argument lists, so we only
//   count `CallExpressionWithBareword` when it stands on its own;
//   when its parent is another call form, the outer wrapper has
//   already contributed the branch.
// - Conditions: numeric and string comparison operators (`==`, `!=`,
//   `<`, `>`, `<=`, `>=`, `<=>`, `eq`, `ne`, `lt`, `gt`, `le`, `ge`,
//   `cmp`, and `=~` / `!~` binding a match rather than an `s///` /
//   `tr///` edit), a bare `/re/` / `m{re}` match against `$_`,
//   the ternary operator (`TernaryExpression`),
//   and each `elsif` / `else` clause of an `if` / `unless`
//   statement. Each condition slot pays one condition unless an arm
//   inside it already did (see `perl_count_condition`).
//
//   The short-circuit and low-precedence logical operators (`&&`,
//   `||`, `//`, `and`, `or`) are not counted as tokens; each operand
//   of their chain is a boolean slot instead (Rule 9, issue #403).
//   See the module-level `Stats` doc-comment for the cross-
//   language policy (Fitzpatrick rules mapped from Figure 2 for C,
//   the closest analogue since the paper does not define rules for
//   Perl; issue #395). `xor` is no chain: it evaluates both operands,
//   so like Java's `^` it is a value, and the slot holding it pays
//   one condition (#1536).

// Whether `child` can occupy a Perl operand slot. tree-sitter-perl
// names its separators — `normal_comma` (`,`) and `fat_comma` (`=>`) —
// where every other grammar leaves them anonymous, so the shared
// `is_operand` took a trailing one for the list's last element:
// `if ($a > 1,)` and `if ($a > 1 =>)` scored the comma as an unproven
// slot occupant on top of the `>`, where `perl -MO=Deparse` reduces both
// to `if ($a > 1)`.
fn perl_is_operand(child: &Node) -> bool {
    is_operand(child) && !matches!(child.kind_id().into(), Perl::NormalComma | Perl::FatComma)
}

// The last operand of `node`, separators aside: the value a Perl list
// evaluates to in scalar context.
fn perl_last_operand<'a>(node: &Node<'a>) -> Option<Node<'a>> {
    node.children().filter(perl_is_operand).last()
}

// `node` with every `( … )` around it taken off. A parenthesised operand
// is the value inside it, so `$x =~ (s/a/b/)` binds the substitution
// as `$x =~ s/a/b/` does.
fn perl_unparenthesized(node: Node<'_>) -> Node<'_> {
    let mut node = node;
    while node.kind_id() == Perl::Array as u16
        && let Some(inner) = perl_last_operand(&node)
    {
        node = inner;
    }
    node
}

// One step of the value peel (see `PeelStep`).
//
// `Array` is tree-sitter-perl's name for the `(...)` shape used BOTH as
// the `if` / `while` / `unless` / `until` condition wrapper AND as list
// literals `(1, 2, 3)`. In the scalar context a condition imposes, a list
// evaluates to its LAST element, so the peel reads the last operand for
// both shapes: `($a)` → `$a`, `($x, $y)` → `$y`. A statement modifier's
// bare `arguments` list (`g() if $a, $b;`) is the same list without the
// parentheses, and a plain `=` evaluates to its right-hand side, which
// is its last operand too — so `if (my $y = $x > 1)` tests the
// comparison its own arm already counts. `parenthesized_argument` holds
// one operand.
//
// Both spellings of the same negation prove the operand boolean — see
// `ruby_wrapper_operand`; Perl has the identical gap (#1182). Read
// through the grammar's `operator` field, whose type list is
// `! + ++ - -- and not ~`: `-$x` yields a value and stops the peel, and
// tree-sitter-perl spells a low-precedence `$a and $b` as a two-operand
// `unary_expression`, which is a chain rather than a wrapper. Do NOT
// match the hidden `_unary_not` supertype (`P::UnaryNot`), which the
// parser never emits (grammar-dispatch item 2).
//
// FIXME(#1541 upstream): an auto-quoted `not` key (`f(not => !$a)`)
// parses as that same `unary_expression`, with the `=>` in an `ERROR`
// beside the token. It is the list `("not", !$a)`, not a negation, so
// it peels like one: to its last element, the value, proving nothing.
// FIXME(#1539 upstream): an auto-quoted `and` key (`f(and => !$a)`) is
// the same shape led by `and`, and peels the same way; a real `and`
// has a left operand, so `perl_and_is_operator` tells the two apart.
// FIXME(#1545 upstream): a `-and` / `-not` key nests that shape as the
// operand of a `-` (`f(-not => !$a)`); the sign is part of the key, so
// the peel steps through it to the key.
//
// No operand is read by index: a comment may sit before or after it
// (`(# c` / `! # c`, #1455).
fn perl_wrapper_operand<'a>(node: &Node<'a>) -> Option<(Node<'a>, bool)> {
    use Perl as P;

    match node.kind_id().into() {
        P::Array | P::Arguments => perl_last_operand(node).map(|o| (o, false)),
        P::ParenthesizedArgument => wrapped_operand(node).map(|o| (o, false)),
        P::BinaryExpression if node.is_child(P::EQ as u16) => {
            perl_last_operand(node).map(|o| (o, false))
        }
        P::UnaryExpression => {
            node.child_by_field_name("operator")
                .and_then(|op| match op.kind_id().into() {
                    P::Not if perl_not_is_key(&op, node) => {
                        perl_last_operand(node).map(|o| (o, false))
                    }
                    P::And if !perl_and_is_operator(&op, node) => {
                        perl_last_operand(node).map(|o| (o, false))
                    }
                    // The `-`'s parent is all the chain the test reads.
                    P::DASH
                        if perl_dash_signs_keyword_key(
                            &op,
                            Ancestors::known(std::slice::from_ref(node)),
                        ) =>
                    {
                        wrapped_operand(node).map(|o| (o, false))
                    }
                    P::BANG | P::Not => wrapped_operand(node).map(|o| (o, true)),
                    _ => None,
                })
        }
        _ => None,
    }
}

// Whether an arm of `compute` already charges `expr` (already peeled) as
// a condition: a comparison or match operator (the token arms), a bare
// `/re/` match (its own arm — a peeled slot occupant is never a bound
// pattern or a `split` delimiter, the two shapes that arm skips), a
// ternary (its own arm) or a logical chain, whose operands each pay
// through `perl_count_condition`. The operator is found among the node's
// tokens rather than through its `operator` field, which tree-sitter-perl
// leaves off every comparison and off the `and` it parses as a
// `unary_expression`.
fn perl_condition_scores_itself(expr: &Node) -> bool {
    use Perl as P;

    match expr.kind_id().into() {
        P::TernaryExpression | P::PatternMatcher | P::PatternMatcherM => true,
        P::BinaryExpression | P::UnaryExpression => {
            perl_is_logical_chain(expr)
                || expr.children().any(|token| match token.kind_id().into() {
                    P::EQTILDE | P::BANGTILDE => !perl_binding_rewrites(expr),
                    kind => matches!(
                        kind,
                        P::EQEQ
                            | P::BANGEQ
                            | P::LT
                            | P::GT
                            | P::LTEQ
                            | P::GTEQ
                            | P::LTEQGT
                            | P::Eq
                            | P::Ne
                            | P::Lt
                            | P::Gt
                            | P::Le
                            | P::Ge
                            | P::Cmp
                    ),
                })
        }
        _ => false,
    }
}

// Whether a `=~` / `!~` binding applies a substitution or transliteration
// (`$x =~ s/a/b/`, `$x =~ tr/a/b/`, `$x =~ y/a/b/`) rather than a match.
// Those edit `$x` and yield a count, so by use they are no test, exactly
// as the bare `s///` / `tr///` on `$_` are not (#1540) — while a match
// (`/re/`, `m{re}`, `qr//`, or a variable holding a pattern) is. A slot
// still pays for a rewrite binding through `perl_count_condition`,
// because this predicate also keeps `perl_condition_scores_itself` from
// claiming it. The grammar names no `left` / `right` field on a
// `binary_expression`, so the right operand is read as its last, with
// any parentheses taken off: `$x =~ (s/a/b/)` is the same edit.
//
// `$x !~ s///r` and `$x !~ tr///r` parse the same way but are Perl
// compile errors ("Using !~ with s///r doesn't make sense"); valid input
// cannot tell how they score, so no test pins them (grammar-dispatch §6).
//
// Only a `binary_expression` binds: under error recovery the token's
// parent can be an `ERROR` holding the whole file, whose last operand is
// no right operand, and reading it per token was quadratic.
fn perl_binding_rewrites(binding: &Node) -> bool {
    binding.kind_id() == Perl::BinaryExpression as u16
        && perl_last_operand(binding).is_some_and(|rhs| {
            matches!(
                perl_unparenthesized(rhs).kind_id().into(),
                Perl::SubstitutionPatternS | Perl::TransliterationTrOrY
            )
        })
}

// Scores one boolean slot — an `if` / `elsif` / `unless` / `while` /
// `until` condition, a statement modifier's condition, a ternary or
// C-style `for` condition, an operand of a logical chain (see
// `count_boolean_slot`). Perl is truthy-valued, so `if (-$x)`,
// `if ($x + 1)` and `if (my $y = $x)` are each a decision, and each
// scored 0 while the slot paid only for a fixed list of terminal kinds
// (#1526).
fn perl_count_condition(condition: &Node, conditions: &mut f64) {
    count_boolean_slot(
        condition,
        perl_wrapper_operand,
        perl_condition_scores_itself,
        conditions,
    );
}

// An operand outside a boolean slot — a `return` value, a call argument,
// a ternary branch — scores only when a `!` / `not` proves it boolean
// (see `count_negated_operand`), which keeps `($a > 0) ? $b : -$b` at 2
// (the ternary node and the `>`).
fn perl_count_negated(operand: &Node, conditions: &mut f64) {
    count_negated_operand(
        operand,
        perl_wrapper_operand,
        perl_condition_scores_itself,
        conditions,
    );
}

fn perl_count_slot(slot: Option<Node>, conditions: &mut f64) {
    if let Some(condition) = slot {
        perl_count_condition(&condition, conditions);
    }
}

// FIXME(#1273 upstream): tree-sitter-perl 1.1.2 lets a chaining
// comparison (`< > <= >= lt gt le ge == != eq ne`) swallow everything
// of lower precedence to its right, so `$a > $b && $c` parses as
// `$a > ($b && $c)` and `$a > $b ? 1 : 2` as `$a > ($b ? 1 : 2)`. The
// operand those trees hand to a boolean slot — `$b` — is really the
// comparison's right operand, and the real slot occupant is the
// comparison, which scores itself. Remove these helpers, and the two
// call sites' `absorbed` flags, once the pin carries the upstream
// `prec` fix.
//
// Whether the leading boolean slot of `node` — a ternary's condition or
// a logical chain's first operand — belongs to such a comparison. The
// misparse nests the stolen operand down a spine of exactly these two
// slots (`$a > $b && $c ? …` is `$a > (($b && $c) ? …)`), so the climb
// follows that spine to its root and asks whether the root directly
// follows a chaining operator. An explicit `$a > ($b ? 1 : 2)` puts an
// `array` between the two and is left alone. The token check alone
// suffices: those operators occur only in `binary_expression`, apart
// from the readline `<FH>` brackets, whose interior is never an
// expression.
//
// Two guards keep the walk linear, and neither moves a count. A slot
// that would pay nothing anyway (its occupant scores itself — on a
// left-nested `$a || $b || …` spine, every slot but the bottom one) is
// answered at once, so each spine is climbed once, from its bottom; and
// only a `binary_expression`, or an `ERROR` recovered from one, can hold
// a chaining operator, so a block, list or file the climb stops at is
// not scanned child by child. Without them every spine node climbed to
// the top and every chain scanned its statement list, both quadratic.
// The `ERROR` is the one wide parent left, holding every chain of an
// unclosed list. Recovery builds it as one flat array, which a seek by
// byte scans from its start, so the operator before `child` is read off
// the children the walk recorded (#1584).
fn perl_misparse_absorbs_leading_operand<'a>(
    node: &Node<'a>,
    ancestors: Ancestors<'a, '_>,
) -> bool {
    use Perl as P;

    if perl_leading_slot(node)
        .is_none_or(|lead| perl_condition_scores_itself(&peel(&lead, perl_wrapper_operand).0))
    {
        return false;
    }
    let (mut child, mut child_ancestors) = (*node, ancestors);
    let spine_parent = ancestors.iter(node).find(|(parent, above)| {
        let on_spine = perl_leading_slot(parent).is_some_and(|lead| lead.id() == child.id());
        if on_spine {
            (child, child_ancestors) = (*parent, *above);
        }
        !on_spine
    });
    spine_parent.is_some_and(|(parent, _)| {
        (parent.kind_id() == P::BinaryExpression as u16 || parent.is_error())
            && child_ancestors
                .siblings_before(&child)
                .find(|sibling| !sibling.as_tree_sitter().is_extra())
                .is_some_and(|operator| {
                    matches!(
                        operator.kind_id().into(),
                        P::LT
                            | P::GT
                            | P::LTEQ
                            | P::GTEQ
                            | P::Lt
                            | P::Gt
                            | P::Le
                            | P::Ge
                            | P::EQEQ
                            | P::BANGEQ
                            | P::Eq
                            | P::Ne
                    )
                })
    })
}

// The leading boolean slot of a spine node — a ternary's condition or a
// logical chain's first operand — or `None` for any other node.
fn perl_leading_slot<'a>(node: &Node<'a>) -> Option<Node<'a>> {
    if node.kind_id() == Perl::TernaryExpression as u16 {
        node.child_by_field_name("condition")
    } else if perl_is_logical_chain(node) {
        wrapped_operand(node)
    } else {
        None
    }
}

// A `&&` / `||` / `//` / `and` / `or` chain. tree-sitter-perl spells
// `and` as a two-operand `unary_expression`. `xor` is left out: it does
// not short-circuit, so it is a value, as `^` is (#1536).
fn perl_is_logical_chain(node: &Node) -> bool {
    use Perl as P;

    matches!(
        node.kind_id().into(),
        P::BinaryExpression | P::UnaryExpression
    ) && node.children().any(|token| match token.kind_id().into() {
        P::And => perl_and_is_operator(&token, node),
        P::AMPAMP | P::PIPEPIPE | P::SLASHSLASH | P::Or => true,
        _ => false,
    })
}

// Phase-2B (issues #403 / #1102): a ternary's condition is a boolean
// slot and each branch a negated operand, exactly as `java_walk_ternary`
// counts them. Without this Perl scored `$a ? !$b : !$c` as 1 (the
// `ternary_expression` node alone) against Java's 4.
//
// Slots are addressed by grammar FIELD, not by child index.
// tree-sitter-perl names the branches `true` / `false` rather than the
// C-family `consequence` / `alternative`, and all three slots are
// mandatory — Perl has no short-ternary elision.
//
// `condition_absorbed`: see `perl_misparse_absorbs_leading_operand`.
fn perl_walk_ternary(node: &Node, condition_absorbed: bool, conditions: &mut f64) {
    if !condition_absorbed {
        perl_count_slot(node.child_by_field_name("condition"), conditions);
    }
    for field in ["true", "false"] {
        if let Some(branch) = node.child_by_field_name(field) {
            perl_count_negated(&branch, conditions);
        }
    }
}

// Each operand of a logical chain is a boolean slot (Fitzpatrick Rule 9,
// #403). `$a && $b || $c` is a left-nested chain, so an operand that is
// itself a chain is paid by its own operator's visit. The operands are
// the chain's children that are neither its operator token nor an
// `extra`: tree-sitter-perl names no operand field on a comparison or on
// the two-operand `unary_expression` it parses `$a and $b` as, which the
// field-less walk is what scores at all — it scored 0.
//
// `leading_absorbed`: see `perl_misparse_absorbs_leading_operand`. The
// `chain` is the operator token's parent, which is no chain when the
// token sits in an `ERROR` or is an auto-quoted `and` key (#1539).
fn perl_count_chain_operands(chain: &Node, leading_absorbed: bool, conditions: &mut f64) {
    if perl_is_logical_chain(chain) {
        for operand in chain
            .children()
            .filter(perl_is_operand)
            .skip(usize::from(leading_absorbed))
        {
            perl_count_condition(&operand, conditions);
        }
    }
}

// Whether a pattern (`/re/` or `m{re}`) is one of the two shapes that
// are not a bare match the ABC walk should count on its own (#1467):
//
// - **Bound**: the pattern is the right operand of `$x =~ /re/` or
//   `$x !~ /re/`, whose operator token is already a condition. Read
//   through the grammar's `operator` field, so `$x ~~ /re/` — a
//   smartmatch, which no arm counts — still scores the match once. Only
//   the *right* operand is bound: `/re/ =~ $x` first matches `$_` and
//   then binds that result (`perl -MO=Deparse` prints `/re/ =~ /$x/`),
//   so it scores the match and the `=~` as its explicit twin
//   `my $m = /re/; $m =~ $x` does.
// - **`split`'s delimiter**: `split /,/, $s` and `split(/,/, $s)` hand
//   the pattern to `split` as a separator; nothing is matched against
//   `$_`, so it is no condition. Only the *first* argument is a
//   delimiter: a pattern elsewhere in the list is an ordinary match
//   whose result `split` receives.
//
// Either role is read on the pattern's outermost `( … )`, which is the
// value inside it: `$x =~ ((/re/))` binds and `split((/,/), $s)`
// delimits as their bare twins do. A call's own argument list is an
// `Array` too, but it is the call's, not a wrapper around the pattern,
// so the climb stops below one. Whether the pattern is a list's last
// operand is read forward from the pattern, which stops at the next
// operand: read off the list's last operand instead, every pattern of a
// wide `(/a/, /b/, …)` scanned the whole list.
fn perl_pattern_is_bound_or_delimiter(pattern: &Node, code: &[u8], ancestors: Ancestors) -> bool {
    use Perl as P;

    let mut up = ancestors.iter(pattern).map(|(ancestor, _)| ancestor);
    let mut slot = *pattern;
    let mut owner = up.next();
    let mut above = up.next();
    while let Some(paren) = owner
        && paren.kind_id() == P::Array as u16
        && !paren
            .children_after(&slot)
            .any(|next| perl_is_operand(&next))
        && !above.is_some_and(perl_is_call_argument_parent)
    {
        slot = paren;
        owner = above;
        above = up.next();
    }
    owner.is_some_and(|owner| match owner.kind_id().into() {
        P::BinaryExpression => {
            owner
                .child_by_field_name("operator")
                .is_some_and(|op| matches!(op.kind_id().into(), P::EQTILDE | P::BANGTILDE))
                && perl_last_operand(&owner).is_some_and(|rhs| rhs.id() == slot.id())
        }
        // The first operand, not the first named child: a comment is
        // named, so `split( # sep⏎ /,/, $s)` read it as the first
        // argument (#1455).
        P::Arguments | P::Array => {
            owner
                .children()
                .find(perl_is_operand)
                .is_some_and(|first| first.id() == slot.id())
                && above.is_some_and(|call| perl_call_is_split(&call, code))
        }
        _ => false,
    })
}

// Whether `call` is a call of the builtin `split`, spelled bare or as
// `CORE::split`. Every named child of the two call wrappers that take an
// argument list is the callee or an `args` field, so a list whose
// grandparent is one of them is its arguments. The callee's package is
// read as well as its name: `My::split` is a user sub, and the pattern
// it is handed is an ordinary match (grammar-dispatch §10).
fn perl_call_is_split(call: &Node, code: &[u8]) -> bool {
    let text = |node: &Node| code.get(node.start_byte()..node.end_byte());
    matches!(
        call.kind_id().into(),
        Perl::CallExpressionWithSpacedArgs | Perl::CallExpressionWithArgsWithBrackets
    ) && call
        .children()
        .find(|child| child.kind_id() == Perl::CallExpressionWithBareword as u16)
        .is_some_and(|callee| {
            callee
                .child_by_field_name("function_name")
                .is_some_and(|name| text(&name) == Some(b"split"))
                && callee
                    .children()
                    .find(|child| child.kind_id() == Perl::PackageName as u16)
                    .is_none_or(|package| text(&package) == Some(b"CORE"))
        })
}

fn perl_is_call_argument_parent(parent: Node) -> bool {
    use Perl as P;
    matches!(
        parent.kind_id().into(),
        P::CallExpressionWithArgsWithBrackets
            | P::CallExpressionWithSpacedArgs
            | P::CallExpressionWithSub
            | P::CallExpressionWithVariable
            | P::CallExpressionRecursive
            | P::MethodInvocation
    )
}

impl Abc for PerlCode {
    fn compute<'a>(
        node: &Node<'a>,
        code: &'a [u8],
        ancestors: Ancestors<'a, '_>,
        stats: &mut Stats,
    ) {
        // bca: suppress(halstead, cyclomatic)
        // Exhaustive one-arm-per-grammar-kind dispatch table, like
        // `CppCode::compute`, which breaches only the cyclomatic limit
        // and so carries only that marker. Perl's arm list is the
        // longest of the family — tree-sitter-perl tokenises all
        // nineteen assignment operators and all six call-expression
        // wrappers separately — so
        // `halstead.effort` here is a count of distinct enum operands,
        // and the cyclomatic count is the number of node kinds the
        // grammar can hand us, neither being reasoning a reader must
        // do. Adding the guarded `<` / `>` arm for #1297 took the
        // count from 14 to 15, the statement-modifier arm for #1464
        // to 16, the bare-match arm for #1467 to 17, and the `=~` /
        // `!~` rewrite gate for #1540 added one more; each arm is
        // independent and self-describing like every other, and there
        // is no semantic boundary to split this lookup on.
        use Perl as P;

        match node.kind_id().into() {
            // Plain `=` and every compound assignment operator. The
            // grammar tokenises each operator separately, so one
            // textual `+=` produces exactly one token and there is no
            // double-counting via a wrapper.
            P::EQ
            | P::PLUSEQ
            | P::DASHEQ
            | P::STAREQ
            | P::SLASHEQ
            | P::PERCENTEQ
            | P::STARSTAREQ
            | P::DOTEQ
            | P::XEQ
            | P::AMPEQ
            | P::PIPEEQ
            | P::CARETEQ
            | P::LTLTEQ
            | P::GTGTEQ
            | P::AMPAMPEQ
            | P::PIPEPIPEEQ
            | P::SLASHSLASHEQ
            | P::AMPDOTEQ
            | P::PIPEDOTEQ
            | P::CARETDOTEQ => {
                stats.assignments += 1.;
            }
            // Argument-bearing call wrappers always count.
            P::CallExpressionWithSpacedArgs
            | P::CallExpressionWithSub
            | P::CallExpressionWithArgsWithBrackets
            | P::CallExpressionWithVariable
            | P::CallExpressionRecursive
            | P::MethodInvocation => {
                stats.branches += 1.;
            }
            // Bareword-only call (`shift`, `time`, …) — count only
            // when this node is the outermost dispatch site. When the
            // bareword sits inside one of the wrappers above, the
            // outer node has already been counted and this child
            // would double the branch tally. Nor is an auto-quoted
            // key a call (FIXME(#1571 upstream)): the grammar reads
            // `(name => 1)`, `(-name => 1)` and `$h{-name}` as calls of
            // `name`, and `$h{-foo}` as a file test on a bareword `oo`,
            // where Perl holds a string (#1545, #1572).
            P::CallExpressionWithBareword if perl_bareword_is_call(node, ancestors) => {
                stats.branches += 1.;
            }
            // Numeric, string, and pattern-match comparison operators
            // plus the spaceship / `cmp` three-way comparisons, and each
            // `elsif` / `else` clause of an `if` / `unless` chain.
            P::EQEQ
            | P::BANGEQ
            | P::LTEQ
            | P::GTEQ
            | P::LTEQGT
            | P::Eq
            | P::Ne
            | P::Lt
            | P::Gt
            | P::Le
            | P::Ge
            | P::Cmp
            | P::ElseClause => {
                stats.conditions += 1.;
            }
            // `=~` / `!~` bind a match, a test, or a rewrite, an edit
            // that scores only in a slot (see `perl_binding_rewrites`).
            P::EQTILDE | P::BANGTILDE
                if !ancestors
                    .parent(node)
                    .is_some_and(|binding| perl_binding_rewrites(&binding)) =>
            {
                stats.conditions += 1.;
            }
            // Counts `<` / `>` only as the operator token of a
            // `binary_expression`, the allowlist polarity the rest of
            // the workspace uses. Ungated, a readline scored two
            // conditions: `<FH>` and `<$fh>` are
            // `standard_input_to_identifier` and
            // `standard_input_to_variable`, each a plain three-token
            // sequence whose brackets are the same bare `<` / `>` a
            // comparison uses. A `grammar.json` sweep of
            // tree-sitter-perl 1.1.2 finds them in exactly three
            // productions — `binary_expression` plus those two — so the
            // gate is closed (`.claude/rules/grammar-dispatch.md` §1).
            //
            // `<STDIN>` is *not* among them: the grammar lexes it as a
            // single `standard_input` token, which is why #1297's sweep
            // measured Perl at 0 and wrongly cleared it. The filehandle
            // and lexical-handle forms are the reachable ones.
            // `<=` / `>=`, the spaceship `<=>` and the word-form `lt` /
            // `gt` are distinct tokens counted above, and a heredoc
            // opener is its own token, so none reaches this arm.
            P::LT | P::GT if ancestors.parent_has_kind(node, P::BinaryExpression as u16) => {
                stats.conditions += 1.;
            }
            // A bare `/re/` or `m{re}` (sibling kinds, not aliases)
            // matches the implicit `$_`: a relational operator with no
            // operator token, so it scores by use wherever it is
            // written (#1467) — `my $r = /^#/` levels with
            // `my $r = ($x =~ /^#/)`. `perl_condition_scores_itself`
            // lists it, or every slot would score it a second time.
            // `s///` and `tr///` stay out: they edit `$_` and yield a
            // count, so by use they are no test. A slot still pays for
            // either (#1475), and the bound `$x =~ s///` agrees with the
            // bare form (#1540, `perl_binding_rewrites`).
            P::PatternMatcher | P::PatternMatcherM
                if !perl_pattern_is_bound_or_delimiter(node, code, ancestors) =>
            {
                stats.conditions += 1.;
            }
            // Fitzpatrick Rule 9 walker: each operand of a Perl
            // short-circuit / low-precedence logical chain is one
            // condition (issue #403). Covers `&&`, `||`, `//`,
            // `and`, `or` — not `xor`, which is a value (#1536).
            P::AMPAMP | P::PIPEPIPE | P::SLASHSLASH | P::And | P::Or => {
                if let Some((chain, above)) = ancestors.iter(node).next() {
                    let absorbed = perl_misparse_absorbs_leading_operand(&chain, above);
                    perl_count_chain_operands(&chain, absorbed, &mut stats.conditions);
                }
            }
            // An `elsif` is Java's `else if`: the `else` (+1, Rule 5) and
            // an `if` predicate slot, as Ruby's `elsif` is. It paid only
            // the first, so `elsif ($b)` scored one below
            // `elsif ($x > 1)` (#1526).
            P::ElsifClause => {
                stats.conditions += 1.;
                perl_count_slot(node.child_by_field_name("condition"), &mut stats.conditions);
            }
            // `return value` names no field; its value is its only
            // operand.
            P::ReturnExpression => {
                if let Some(value) = wrapped_operand(node) {
                    perl_count_negated(&value, &mut stats.conditions);
                }
            }
            // `call(!$a, !$b)` — argument list walker. Perl wraps
            // call-argument lists in an `Array` node (same kind name
            // as the `(...)` wrapper around `if` / `while`
            // conditions). To avoid re-handling condition slots that
            // were already walked as slots, only dispatch when the
            // parent is a call-expression form. Each argument is a
            // negated operand.
            P::Array
                if ancestors
                    .parent(node)
                    .is_some_and(perl_is_call_argument_parent) =>
            {
                count_each_operand(node, perl_count_negated, &mut stats.conditions);
            }
            // `$a ? !$b : !$c`. Unlike the C family, this dispatcher
            // has no `?`-token arm — the grammar emits the token, but
            // the `ternary_expression` node is what carries the
            // condition tally's +1 — so this arm keeps that increment
            // and adds the three operand slots (issue #1102).
            P::TernaryExpression => {
                stats.conditions += 1.;
                let absorbed = perl_misparse_absorbs_leading_operand(node, ancestors);
                perl_walk_ternary(node, absorbed, &mut stats.conditions);
            }
            // Phase-2B (issue #403): condition slots, each read by the
            // grammar's `condition` field, not at child(1): a comment
            // may sit before the slot (`if # c⏎ ($b)`), which the fixed
            // index scored zero (#1455). The block forms wrap it in the
            // `Array` `(...)` shape the peel reads; the C-style `for`
            // header's condition (#1276) is bare, and `for (;;)` has
            // no condition field and counts nothing.
            //
            // Statement modifiers — `return 1 if $x;`, `next unless $ok;`
            // (issue #1464). Each is its own node whose `condition` field
            // is the predicate: a `parenthesized_argument`, or a bare
            // `arguments` list the peel reads like a `(...)` one. Perl's
            // cyclomatic dispatcher already counts these kinds, so
            // before #1464 `return 1 if $x;` scored zero conditions
            // against `if ($x) { return 1; }`'s one.
            //
            // `for_simple_statement` is deliberately absent: `print $_
            // for @list;` iterates a list and has no boolean test, and
            // the grammar names its field `list`, not `condition` —
            // mirroring the block forms, where the `foreach` shape
            // `ForStatement2` contributes nothing. `when_simple_statement`
            // is listed for parity with the cyclomatic dispatcher, but
            // only error recovery reaches it: `when` is never a modifier,
            // and `perl -c` rejects `print 6 when $x;` (grammar-dispatch
            // §6). `_if_simple` (`Perl::IfSimple`) is a hidden rule the
            // parser inlines and gets no arm (§2).
            P::IfStatement
            | P::UnlessStatement
            | P::WhileStatement
            | P::UntilStatement
            | P::ForStatement1
            | P::IfSimpleStatement
            | P::UnlessSimpleStatement
            | P::WhileSimpleStatement
            | P::UntilSimpleStatement
            | P::WhenSimpleStatement => {
                perl_count_slot(node.child_by_field_name("condition"), &mut stats.conditions);
            }
            _ => {}
        }
    }
}