Skip to main content

big_code_analysis/metrics/
halstead.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::doc_markdown,
9    clippy::enum_glob_use,
10    clippy::match_wildcard_for_single_variants,
11    clippy::similar_names,
12    clippy::unused_self,
13    clippy::wildcard_imports
14)]
15// Metric counts (token, function, branch, argument, etc.) are stored as
16// `usize` and crossed with `f64` averages, ratios, and Halstead scores
17// across the cyclomatic / MI / Halstead computations. The `usize as f64`
18// and `f64 as usize` casts are intentional and snapshot-anchored — every
19// site is bounded by the count it came from. Allowing the lints at the
20// module level keeps the metric arithmetic legible.
21#![allow(
22    clippy::cast_precision_loss,
23    clippy::cast_possible_truncation,
24    clippy::cast_sign_loss
25)]
26
27use std::collections::HashMap;
28
29use std::fmt;
30
31// The operator / operand classification is syntactic and lives beside
32// the per-language `Getter` tables that produce it; re-exported here
33// because this metric is what consumes it.
34pub use crate::token_role::TokenRole;
35
36/// The former name of [`TokenRole`], kept so the
37/// `metrics::halstead::HalsteadType` path still resolves.
38///
39/// The classification never was Halstead-specific — it answers whether
40/// a node acts as an operator or an operand, which the grammar decides
41/// and other consumers can use (#1376). Renaming it outright would be a
42/// SemVer break, so the old spelling stays until the next major.
43#[deprecated(
44    since = "2.3.0",
45    note = "renamed to `TokenRole`: the classification is syntactic, not Halstead-specific"
46)]
47pub type HalsteadType = TokenRole;
48
49use crate::checker::Checker;
50use crate::getter::Getter;
51use crate::int_hash::IntKeyHashMap;
52use crate::macros::implement_metric_trait;
53
54use crate::*;
55
56/// The `Halstead` metric suite.
57#[derive(Default, Clone, Debug, PartialEq)]
58#[non_exhaustive]
59pub struct Stats {
60    u_operators: u64,
61    operators: u64,
62    u_operands: u64,
63    operands: u64,
64}
65
66/// Per-space operator / operand occurrence maps used to compute the
67/// Halstead `Stats` struct. One map per distinct operator (`kind_id`)
68/// and one per distinct operand (`text`); merged across nested spaces.
69#[derive(Debug, Default, Clone, PartialEq)]
70pub struct HalsteadMaps<'a> {
71    /// Keyed by `kind_id`, so it is hashed with [`crate::int_hash`]'s
72    /// integer hasher rather than SipHash: the key is a grammar symbol
73    /// this crate generated, drawn from an alphabet of at most a few
74    /// hundred values, so there is nothing for a keyed hash to defend.
75    pub(crate) operators: IntKeyHashMap<u16, u64>,
76    /// Primitive-type operators stored by text so each distinct primitive
77    /// (e.g. `int` vs `double`) counts as a separate distinct operator,
78    /// even when the grammar maps them all to a single kind_id. Also
79    /// holds the operators a getter keys by a fixed spelling
80    /// ([`Getter::get_operator_spelling`]), such as the generic `<>`
81    /// pair (#1559).
82    ///
83    /// Text-keyed, so it keeps SipHash — see the module doc on
84    /// [`crate::int_hash`] for why analysed source text does not qualify
85    /// for the fast hasher.
86    pub(crate) primitive_operators: HashMap<&'a [u8], u64>,
87    /// Text-keyed, and on SipHash for the same reason as
88    /// `primitive_operators`.
89    pub(crate) operands: HashMap<&'a [u8], u64>,
90}
91
92impl<'a> HalsteadMaps<'a> {
93    pub(crate) fn new() -> Self {
94        Self::default()
95    }
96
97    pub(crate) fn merge(&mut self, other: &HalsteadMaps<'a>) {
98        for (k, v) in &other.operators {
99            *self.operators.entry(*k).or_insert(0) += v;
100        }
101        for (k, v) in &other.primitive_operators {
102            *self.primitive_operators.entry(*k).or_insert(0) += v;
103        }
104        for (k, v) in &other.operands {
105            *self.operands.entry(*k).or_insert(0) += v;
106        }
107    }
108
109    pub(crate) fn finalize(&self, stats: &mut Stats) {
110        stats.u_operators = (self.operators.len() + self.primitive_operators.len()) as u64;
111        stats.operators =
112            self.operators.values().sum::<u64>() + self.primitive_operators.values().sum::<u64>();
113        stats.u_operands = self.operands.len() as u64;
114        stats.operands = self.operands.values().sum::<u64>();
115    }
116}
117
118impl fmt::Display for Stats {
119    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
120        write!(
121            f,
122            "unique_operators: {}, \
123             total_operators: {}, \
124             unique_operands: {}, \
125             total_operands: {}, \
126             length: {}, \
127             estimated_program_length: {}, \
128             purity_ratio: {}, \
129             size: {}, \
130             volume: {}, \
131             difficulty: {}, \
132             level: {}, \
133             effort: {}, \
134             time: {}, \
135             bugs: {}",
136            self.unique_operators(),
137            self.total_operators(),
138            self.unique_operands(),
139            self.total_operands(),
140            self.length(),
141            self.estimated_program_length(),
142            self.purity_ratio(),
143            self.vocabulary(),
144            self.volume(),
145            self.difficulty(),
146            self.level(),
147            self.effort(),
148            self.time(),
149            self.bugs(),
150        )
151    }
152}
153
154impl Stats {
155    // Intentionally a no-op. Halstead distinct-counts (`u_operators` /
156    // `u_operands`) cannot be summed across sibling spaces without
157    // double-counting operators/operands they share. Cross-space
158    // aggregation is instead done by unioning the occurrence maps
159    // (`HalsteadMaps::merge`) and re-running `finalize` on the parent
160    // (see `spaces/compute.rs`). Summing the finalized fields here —
161    // mirroring the sibling metrics' `merge` — would silently inflate
162    // every parent space's n1/n2/N1/N2.
163    pub(crate) fn merge(&mut self, _other: &Stats) {}
164
165    /// Returns `η1`, the number of distinct operators
166    #[inline]
167    #[must_use]
168    pub fn unique_operators(&self) -> u64 {
169        self.u_operators
170    }
171
172    /// Returns `N1`, the number of total operators
173    #[inline]
174    #[must_use]
175    pub fn total_operators(&self) -> u64 {
176        self.operators
177    }
178
179    /// Returns `η2`, the number of distinct operands
180    #[inline]
181    #[must_use]
182    pub fn unique_operands(&self) -> u64 {
183        self.u_operands
184    }
185
186    /// Returns `N2`, the number of total operands
187    #[inline]
188    #[must_use]
189    pub fn total_operands(&self) -> u64 {
190        self.operands
191    }
192
193    /// Returns the program length
194    ///
195    /// Computed as `N = N1 + N2`, the sum of [`Self::total_operators`] and
196    /// [`Self::total_operands`].
197    #[inline]
198    #[must_use]
199    pub fn length(&self) -> u64 {
200        self.total_operands() + self.total_operators()
201    }
202
203    /// Returns the calculated estimated program length
204    ///
205    /// Computed as `N^ = n1 * log2(n1) + n2 * log2(n2)`, where `n1` is
206    /// [`Self::unique_operators`] and `n2` is [`Self::unique_operands`]. Each term is
207    /// treated as `0` when its unique count is `0`.
208    #[inline]
209    #[must_use]
210    pub fn estimated_program_length(&self) -> f64 {
211        let uo = self.unique_operators() as f64;
212        let ud = self.unique_operands() as f64;
213        let uo_term = if uo == 0.0 { 0.0 } else { uo * uo.log2() };
214        let ud_term = if ud == 0.0 { 0.0 } else { ud * ud.log2() };
215        uo_term + ud_term
216    }
217
218    /// Returns the purity ratio
219    ///
220    /// Computed as `PR = N^ / N`, the ratio of
221    /// [`Self::estimated_program_length`] to [`Self::length`].
222    #[inline]
223    #[must_use]
224    pub fn purity_ratio(&self) -> f64 {
225        let len = self.length() as f64;
226        if len == 0.0 {
227            0.0
228        } else {
229            self.estimated_program_length() / len
230        }
231    }
232
233    /// Returns the program vocabulary
234    ///
235    /// Computed as `n = n1 + n2`, the sum of [`Self::unique_operators`] and
236    /// [`Self::unique_operands`].
237    #[inline]
238    #[must_use]
239    pub fn vocabulary(&self) -> u64 {
240        self.unique_operands() + self.unique_operators()
241    }
242
243    /// Returns the program volume.
244    ///
245    /// Computed as `V = N * log2(n)`, where `N` is [`Self::length`] and `n`
246    /// is [`Self::vocabulary`]. Returns `0` when the vocabulary is `<= 1`,
247    /// since `log2` would be non-positive.
248    ///
249    /// Unit of measurement: bits
250    #[inline]
251    #[must_use]
252    pub fn volume(&self) -> f64 {
253        // Assumes a uniform binary encoding for the vocabulary is used.
254        let vocab = self.vocabulary() as f64;
255        if vocab <= 1.0 {
256            0.0
257        } else {
258            self.length() as f64 * vocab.log2()
259        }
260    }
261
262    /// Returns the estimated difficulty required to program
263    ///
264    /// Computed as `D = (n1 / 2) * (N2 / n2)`, where `n1` is
265    /// [`Self::unique_operators`], `N2` is [`Self::total_operands`], and `n2` is
266    /// [`Self::unique_operands`].
267    #[inline]
268    #[must_use]
269    pub fn difficulty(&self) -> f64 {
270        let ud = self.unique_operands() as f64;
271        if ud == 0.0 {
272            0.0
273        } else {
274            self.unique_operators() as f64 / 2. * self.total_operands() as f64 / ud
275        }
276    }
277
278    /// Returns the estimated level of difficulty required to program
279    ///
280    /// Computed as `L = 1 / D`, the reciprocal of [`Self::difficulty`].
281    #[inline]
282    #[must_use]
283    pub fn level(&self) -> f64 {
284        let d = self.difficulty();
285        if d == 0.0 { 0.0 } else { 1. / d }
286    }
287
288    /// Returns the estimated effort required to program
289    ///
290    /// Computed as `E = D * V`, the product of [`Self::difficulty`] and
291    /// [`Self::volume`].
292    #[inline]
293    #[must_use]
294    pub fn effort(&self) -> f64 {
295        self.difficulty() * self.volume()
296    }
297
298    /// Returns the estimated time required to program.
299    ///
300    /// Computed as `T = E / 18`, where `E` is [`Self::effort`] and `18` is
301    /// the Stroud number (see the divisor rationale below).
302    ///
303    /// Unit of measurement: seconds
304    #[inline]
305    #[must_use]
306    pub fn time(&self) -> f64 {
307        // The floating point `18.` aims to describe the processing rate of the
308        // human brain. It is called Stoud number, S, and its
309        // unit of measurement is moments/seconds.
310        // A moment is the time required by the human brain to carry out the
311        // most elementary decision.
312        // 5 <= S <= 20. Halstead uses 18.
313        // The value of S has been empirically developed from psychological
314        // reasoning, and its recommended value for
315        // programming applications is 18.
316        //
317        // Source: https://www.geeksforgeeks.org/software-engineering-halsteads-software-metrics/
318        self.effort() / 18.
319    }
320
321    /// Returns the estimated number of delivered bugs.
322    ///
323    /// This metric represents the average amount of work a programmer can do
324    /// without introducing an error.
325    ///
326    /// Computed as `B = E^(2/3) / 3000`, where `E` is [`Self::effort`]. This
327    /// is the effort-based variant of Halstead's delivered-bugs estimate
328    /// rather than the more commonly cited volume-based form `B = V / 3000`;
329    /// it matches the formula used by upstream `rust-code-analysis`.
330    #[inline]
331    #[must_use]
332    pub fn bugs(&self) -> f64 {
333        // The floating point `3000.` represents the number of elementary
334        // mental discriminations.
335        // A mental discrimination, in psychology, is the ability to perceive
336        // and respond to differences among stimuli.
337        //
338        // The value above is obtained starting from a constant that
339        // is different for every language and assumes that natural language is
340        // the language of the brain.
341        // For programming languages, the English language constant
342        // has been considered.
343        //
344        // After every 3000 mental discriminations a result is produced.
345        // This result, whether correct or incorrect, is more than likely
346        // either used as an input for the next operation or is output to the
347        // environment.
348        // If incorrect the error should become apparent.
349        // Thus, an opportunity for error occurs every 3000
350        // mental discriminations.
351        //
352        // Source: https://docs.lib.purdue.edu/cgi/viewcontent.cgi?article=1145&context=cstech
353        self.effort().powf(2. / 3.) / 3000.
354    }
355}
356
357#[doc(hidden)]
358/// Per-language extraction of Halstead operator/operand maps.
359pub(crate) trait Halstead
360where
361    Self: Checker + Getter,
362{
363    /// Walk `node` and update `stats` with this metric for the language
364    /// implementing the trait.
365    ///
366    /// `ancestors` is the chain the walker descended through; it is
367    /// handed to [`Getter::get_op_type`], six of whose impls classify a
368    /// token by what encloses it (#1096).
369    fn compute<'a>(
370        node: &Node<'a>,
371        code: &'a [u8],
372        ancestors: Ancestors<'a, '_>,
373        halstead_maps: &mut HalsteadMaps<'a>,
374    );
375}
376
377#[inline]
378fn get_id<'a>(node: &Node<'a>, code: &'a [u8]) -> &'a [u8] {
379    &code[node.start_byte()..node.end_byte()]
380}
381
382#[inline]
383fn compute_halstead<'a, T: Getter + Checker>(
384    node: &Node<'a>,
385    code: &'a [u8],
386    ancestors: Ancestors<'a, '_>,
387    halstead_maps: &mut HalsteadMaps<'a>,
388) {
389    // Halstead counts the tokens written, and a zero-width node spells
390    // none, so it bills neither half (#1546). Error recovery is where
391    // they come from: every MISSING node tree-sitter inserts for a token
392    // the source lacks (a `;`, a `)`, an identifier) is zero-width, and
393    // so is the identifier C#'s and Elixir's recovery builds without
394    // flagging it MISSING. Checked here rather than per getter because
395    // every getter that classifies the kind would otherwise bill it, an
396    // identifier as the empty-string operand.
397    if node.start_byte() == node.end_byte() {
398        return;
399    }
400    match T::get_op_type_with_code(node, code, ancestors) {
401        TokenRole::Operator => {
402            if let Some(spelling) = T::get_operator_spelling(node, ancestors) {
403                *halstead_maps
404                    .primitive_operators
405                    .entry(spelling.as_bytes())
406                    .or_insert(0) += 1;
407            } else if T::is_primitive(node) {
408                // Store primitive-type operators by text so distinct
409                // primitives (e.g. `int` vs `double`) that share a
410                // single kind_id are counted separately in n1/N1.
411                *halstead_maps
412                    .primitive_operators
413                    .entry(get_id(node, code))
414                    .or_insert(0) += 1;
415            } else {
416                *halstead_maps.operators.entry(node.kind_id()).or_insert(0) += 1;
417            }
418        }
419        TokenRole::Operand => {
420            *halstead_maps
421                .operands
422                .entry(T::get_operand_id(node, code, ancestors))
423                .or_insert(0) += 1;
424        }
425        _ => {}
426    }
427}
428
429// Every language's `Halstead::compute` is the same forward to
430// `compute_halstead`, which classifies each node through the language's
431// own `Getter` / `Checker`. Nothing per-language lives here — it lives
432// in `big-code-analysis-ast/src/getter/<lang>.rs` — so writing the
433// impls out was 23 copies of one signature. (This is the only metric
434// whose per-language impls are all identical; every other trait has
435// real per-language bodies.)
436macro_rules! impl_halstead_forwarding {
437    ($($code:ty),+ $(,)?) => {
438        $(
439            impl Halstead for $code {
440                fn compute<'a>(
441                    node: &Node<'a>,
442                    code: &'a [u8],
443                    ancestors: Ancestors<'a, '_>,
444                    halstead_maps: &mut HalsteadMaps<'a>,
445                ) {
446                    compute_halstead::<Self>(node, code, ancestors, halstead_maps);
447                }
448            }
449        )+
450    };
451}
452
453impl_halstead_forwarding!(
454    PythonCode,
455    MozjsCode,
456    JavascriptCode,
457    TypescriptCode,
458    TsxCode,
459    RustCode,
460    CppCode,
461    CCode,
462    ObjcCode,
463    MozcppCode,
464    JavaCode,
465    GroovyCode,
466    CsharpCode,
467    GoCode,
468    PerlCode,
469    KotlinCode,
470    LuaCode,
471    PhpCode,
472    RubyCode,
473    ElixirCode,
474    BashCode,
475    TclCode,
476    IrulesCode,
477);
478
479// Real defaults — no operators / operands to count. Audited in #188.
480implement_metric_trait!(Halstead, PreprocCode, CcommentCode);
481
482#[cfg(test)]
483#[allow(
484    clippy::float_cmp,
485    clippy::cast_precision_loss,
486    clippy::cast_possible_truncation,
487    clippy::cast_sign_loss,
488    clippy::similar_names,
489    clippy::doc_markdown,
490    clippy::needless_raw_string_hashes,
491    clippy::too_many_lines
492)]
493mod tests {
494    use std::collections::{BTreeSet, HashSet};
495    use std::path::PathBuf;
496
497    use crate::test_support::{ast_has_kind_id, check_metrics_only_shim, for_each_node_with_chain};
498
499    use super::*;
500
501    check_metrics_only_shim!(check_metrics, Halstead);
502
503    // `HalsteadType` is the pre-#1376 name for `TokenRole`, kept as a
504    // deprecated alias because removing it before `3.0` would be a
505    // SemVer break (STABILITY.md). Nothing in this repository uses the
506    // old spelling any more, so only this guard would notice it being
507    // dropped — and dropping it silently is exactly what the promise
508    // rules out.
509    //
510    // The `matches!` arms are the assertion: a value typed through the
511    // alias can only be matched against `TokenRole`'s variants if the
512    // alias still names that enum, so this fails to *compile* if the
513    // alias is removed or re-pointed, rather than failing at runtime.
514    // `TokenRole` derives nothing, so there is no `assert_eq!` to reach
515    // for here.
516    #[test]
517    #[allow(deprecated)]
518    fn halstead_type_alias_still_names_token_role() {
519        let via_alias: HalsteadType = HalsteadType::Operand;
520        assert!(matches!(via_alias, TokenRole::Operand));
521        assert!(!matches!(via_alias, TokenRole::Operator));
522    }
523
524    /// Runs the `--ops` walk over `source` and returns the root space's
525    /// merged vocabulary, with every nested space still reachable
526    /// through [`crate::ops::Ops::spaces`].
527    #[cfg(any(
528        feature = "c",
529        feature = "cpp",
530        feature = "csharp",
531        feature = "groovy",
532        feature = "irules",
533        feature = "java",
534        feature = "javascript",
535        feature = "kotlin",
536        feature = "mozcpp",
537        feature = "mozjs",
538        feature = "objc",
539        feature = "perl",
540        feature = "ruby",
541        feature = "tcl",
542        feature = "typescript",
543    ))]
544    fn ops_of<T: crate::MetricSuite>(source: &str, file: &str) -> crate::ops::Ops {
545        let path = PathBuf::from(file);
546        let parser = T::new(source.as_bytes().to_vec(), &path, None);
547        crate::ops::ops_inner(&parser, None).expect("ops walk succeeds")
548    }
549
550    // Pins the lesson-4 invariant `n2 == len(dedupe(ops.operands))` by
551    // running `operands_and_operators` (the text-keyed `--ops` store)
552    // on the same source and comparing its deduplicated operand count
553    // to the expected `n2`. The metrics store and the ops store are
554    // independent (lesson 4); this catches a classification change that
555    // moves one without the other.
556    // `#[track_caller]` so a failure reports the *caller's* line rather
557    // than this helper's. Callers that wrap it in a per-language helper
558    // (`assert_char_literal_operands`, #1316) are tracked too, so the
559    // reported location names the language row instead of a shared line
560    // no assertion message distinguishes.
561    #[cfg(any(
562        feature = "c",
563        feature = "cpp",
564        feature = "groovy",
565        feature = "irules",
566        feature = "kotlin",
567        feature = "mozcpp",
568        feature = "objc",
569        feature = "perl",
570        feature = "ruby",
571        feature = "tcl",
572    ))]
573    #[track_caller]
574    fn assert_ops_operands<T: crate::MetricSuite>(
575        source: &str,
576        file: &str,
577        expected_n2: usize,
578        mut expected_operands: Vec<&str>,
579    ) {
580        let ops = ops_of::<T>(source, file);
581
582        let unique: HashSet<&str> = ops.operands.iter().map(String::as_str).collect();
583        assert_eq!(
584            unique.len(),
585            expected_n2,
586            "dedupe(ops.operands) must equal n2; operands were {:?}",
587            ops.operands
588        );
589
590        let mut got: Vec<&str> = unique.into_iter().collect();
591        got.sort_unstable();
592        expected_operands.sort_unstable();
593        assert_eq!(got, expected_operands, "operand vocabulary for {file}");
594    }
595
596    /// Asserts each keyword in `keywords` reaches the operand vocabulary
597    /// and reaches the operator vocabulary nowhere.
598    ///
599    /// Both halves are load-bearing for #1380. A keyword listed in two
600    /// arms is billed twice — once into `n1`/`N1` and once into
601    /// `n2`/`N2` — and an assertion that only looked for its arrival
602    /// among the operands would pass on that. The operator side is what
603    /// pins the removal.
604    #[cfg(any(
605        feature = "csharp",
606        feature = "groovy",
607        feature = "java",
608        feature = "kotlin"
609    ))]
610    #[track_caller]
611    fn assert_keywords_are_operands_only<T: crate::MetricSuite>(
612        source: &str,
613        file: &str,
614        keywords: &[&str],
615    ) {
616        let ops = ops_of::<T>(source, file);
617
618        for keyword in keywords {
619            assert!(
620                ops.operands.iter().any(|o| o == keyword),
621                "{file}: `{keyword}` must be an operand; operands were {:?}",
622                ops.operands
623            );
624            assert!(
625                !ops.operators.iter().any(|o| o == keyword),
626                "{file}: `{keyword}` must not be an operator; operators were {:?}",
627                ops.operators
628            );
629        }
630    }
631
632    /// How one space's vocabularies classify a single keyword.
633    #[derive(Debug, PartialEq, Eq)]
634    enum Role {
635        OperatorOnly,
636        OperandOnly,
637        Both,
638    }
639
640    /// Records `keyword`'s role in `ops` and in every space below it.
641    ///
642    /// A space that bills the keyword in neither vocabulary contributes
643    /// no row, so the result is the set of places the keyword actually
644    /// reaches — which is what distinguishes a parent gate from its own
645    /// inverse, and what the merged root vocabulary cannot show.
646    #[cfg(feature = "csharp")]
647    fn collect_keyword_roles(ops: &crate::ops::Ops, keyword: &str, out: &mut Vec<Role>) {
648        let operator = ops.operators.iter().any(|o| o == keyword);
649        let operand = ops.operands.iter().any(|o| o == keyword);
650        match (operator, operand) {
651            (true, true) => out.push(Role::Both),
652            (true, false) => out.push(Role::OperatorOnly),
653            (false, true) => out.push(Role::OperandOnly),
654            (false, false) => {}
655        }
656        for space in &ops.spaces {
657            collect_keyword_roles(space, keyword, out);
658        }
659    }
660
661    /// Pins the three grammar facts every #1380 operand arm rests on,
662    /// over a fixture that samples one self- or super-reference per
663    /// syntactic position the language allows.
664    ///
665    /// Each `keywords` row is `(spelling, kind_id, occurrences)`.
666    ///
667    /// * **Grammar-dispatch section 1.** Every node the grammar spells
668    ///   `this` / `super` / `base` / `this@` / `super@` carries the one
669    ///   `kind_id` its arm lists. An alias arises in a *different*
670    ///   syntactic position, which no fixture can enumerate, so this is
671    ///   the weaker half of the alias evidence — the strong half is that
672    ///   these generated enums carry numeric-suffix aliases in quantity
673    ///   and none spells a `This2`. What this loop catches is a grammar
674    ///   bump introducing one under an existing fixture.
675    /// * **Grammar-dispatch section 5.** Each is a childless leaf and no
676    ///   node *containing* it is classified, so the reference cannot be
677    ///   billed twice. Every ancestor is checked, not just the parent:
678    ///   Kotlin puts `this_expression` *and* `navigation_expression`
679    ///   between the leaf and the nearest classified node, and the claim
680    ///   the arms make is about containment, not parentage.
681    /// * **Grammar-dispatch section 11.** The arm is unconditional. A
682    ///   parent-scoped guard added later — the shape the two deliberate
683    ///   carve-outs use, and so the shape a reviewer might reasonably
684    ///   add — would satisfy the single-position count tests and still
685    ///   drop `this` in a method reference or a constructor delegation.
686    ///
687    /// The fixtures deliberately exclude the two gated positions (C#'s
688    /// indexer declarator, Java's wildcard bound); those are operators,
689    /// and their own tests pin them.
690    #[cfg(any(feature = "csharp", feature = "java", feature = "kotlin"))]
691    #[track_caller]
692    fn assert_self_reference_leaves<L: LanguageInfo + Getter>(
693        source: &str,
694        keywords: &[(&str, u16, usize)],
695        label: &str,
696    ) {
697        let code = source.as_bytes();
698        let mut seen = vec![0_usize; keywords.len()];
699
700        for_each_node_with_chain::<L>(code, |node, chain| {
701            let Some(index) = keywords.iter().position(|(kw, _, _)| *kw == node.kind()) else {
702                return;
703            };
704            let (keyword, kind_id, _) = keywords[index];
705            seen[index] += 1;
706
707            assert_eq!(
708                node.kind_id(),
709                kind_id,
710                "{label}: a `{keyword}` carries kind_id {} rather than the {kind_id} its arm \
711                 lists — an alias the arm cannot see",
712                node.kind_id()
713            );
714            assert_eq!(
715                node.child_count(),
716                0,
717                "{label}: `{keyword}` grew children, which the operand arm would now \
718                 double-count against"
719            );
720            for (depth, ancestor) in chain.iter().enumerate() {
721                assert!(
722                    matches!(
723                        L::get_op_type_with_code(ancestor, code, Ancestors::known(&chain[..depth])),
724                        TokenRole::Unknown
725                    ),
726                    "{label}: `{}` contains a `{keyword}` and is itself classified, so the \
727                     reference now counts twice",
728                    ancestor.kind()
729                );
730            }
731            // `_with_code` is the spelling `compute_halstead` calls. The
732            // default forwards to the byte-less form, so asking the wrong
733            // one reads as correct right up until a language grows an
734            // override (grammar-dispatch section 7).
735            assert!(
736                matches!(
737                    L::get_op_type_with_code(node, code, Ancestors::known(chain)),
738                    TokenRole::Operand
739                ),
740                "{label}: a `{keyword}` under `{}` is not an operand",
741                chain.last().map_or("<root>", Node::kind)
742            );
743        });
744
745        for ((keyword, _, expected), got) in keywords.iter().zip(&seen) {
746            assert_eq!(
747                got, expected,
748                "{label}: fixture holds {got} `{keyword}` rather than {expected}"
749            );
750        }
751    }
752
753    /// Asserts the root space's `[n1, N1, n2, N2]`, naming `label` when
754    /// it does not hold.
755    ///
756    /// The delimiter-invariance tests (#1256 Elixir, #1312 Ruby and
757    /// Perl) each loop over spellings of one literal and need the
758    /// spelling in the failure message; `check_metrics` expands to a
759    /// plain `fn` that cannot capture a loop variable, so they reach
760    /// for the closure-taking helper it wraps. Three copies of that
761    /// dance is two too many.
762    #[cfg(any(
763        feature = "bash",
764        feature = "c",
765        feature = "cpp",
766        feature = "csharp",
767        feature = "elixir",
768        feature = "groovy",
769        feature = "irules",
770        feature = "java",
771        feature = "javascript",
772        feature = "kotlin",
773        feature = "mozcpp",
774        feature = "mozjs",
775        feature = "objc",
776        feature = "perl",
777        feature = "php",
778        feature = "ruby",
779        feature = "tcl",
780        feature = "typescript",
781    ))]
782    fn assert_halstead_counts<T: crate::MetricSuite>(
783        source: &str,
784        file: &str,
785        expected: [u64; 4],
786        label: &str,
787    ) {
788        crate::test_support::check_func_space_only::<T, _>(
789            source,
790            file,
791            &[crate::Metric::Halstead],
792            |space| {
793                let halstead = &space.metrics.halstead;
794                assert_eq!(
795                    [
796                        halstead.unique_operators(),
797                        halstead.total_operators(),
798                        halstead.unique_operands(),
799                        halstead.total_operands(),
800                    ],
801                    expected,
802                    "{label}"
803                );
804            },
805        );
806    }
807
808    #[cfg(feature = "python")]
809    #[test]
810    fn python_operators_and_operands() {
811        check_metrics::<PythonParser>(
812            "def foo():
813                 def bar():
814                     def toto():
815                        a = 1 + 1
816                     b = 2 + a
817                 c = 3 + 3",
818            "foo.py",
819            |metric| {
820                // unique operators: def, (), :, =, +
821                // operators: def, (), : for each of the three headers,
822                //   then =, =, =, +, +, + (the `()` and `:` since #1486)
823                // unique operands: foo, bar, toto, a, b, c, 1, 2, 3
824                // operands: foo, bar, toto, a, b, c, 1, 1, 2, a, 3, 3
825                assert_eq!(metric.halstead.unique_operators(), 5);
826                assert_eq!(metric.halstead.total_operators(), 15);
827                insta::assert_json_snapshot!(
828                    metric.halstead,
829                    @r#"
830                {
831                  "unique_operators": 5,
832                  "total_operators": 15,
833                  "unique_operands": 9,
834                  "total_operands": 12,
835                  "length": 27,
836                  "estimated_program_length": 40.13896548741762,
837                  "purity_ratio": 1.4866283513858378,
838                  "vocabulary": 14,
839                  "volume": 102.79858289555531,
840                  "difficulty": 3.3333333333333335,
841                  "level": 0.3,
842                  "effort": 342.6619429851844,
843                  "time": 19.03677461028802,
844                  "bugs": 0.01632259960095138
845                }
846                "#
847                );
848            },
849        );
850    }
851
852    /// Pointer-arithmetic operators: `*` (dereference), `&` (address-of),
853    /// `->` (member-of-pointer), `+` (pointer + offset). Each is counted
854    /// once in `n1`; multiple uses bump `N1`. The headline integer values
855    /// (`u_operators`, `u_operands`) anchor the snapshot per the
856    /// snapshot-anchor policy.
857    #[cfg(feature = "c")]
858    #[test]
859    fn c_pointer_arithmetic_operators() {
860        check_metrics::<CParser>(
861            "int g(int* p, int* q) {
862                 return *(p + 1) + *q;
863             }",
864            "foo.c",
865            |metric| {
866                // Unique operators: int, *, (), {, }, +, ;, return  (= 8)
867                //   `*` covers both pointer-type and dereference; the grammar
868                //   does NOT split them.  `,` does not appear (only one
869                //   parameter on each side of the body).
870                // Unique operands: g, p, q, 1                       (= 4)
871                assert_eq!(metric.halstead.unique_operators(), 8);
872                assert_eq!(metric.halstead.unique_operands(), 4);
873                insta::assert_json_snapshot!(metric.halstead);
874            },
875        );
876    }
877
878    /// Bitwise (`&`, `|`, `^`, `~`, `<<`, `>>`) and logical (`&&`, `||`,
879    /// `!`) operators are distinct kind_ids and count as separate unique
880    /// operators in Halstead.  `&` (bitwise-and) and `&&` (logical-and)
881    /// must NOT collapse, even though both render as ampersands.
882    #[cfg(feature = "c")]
883    #[test]
884    fn c_bitwise_and_logical_operators() {
885        check_metrics::<CParser>(
886            "int f(int a, int b) {
887                 int x = (a & b) | (a ^ b);
888                 int y = ~a;
889                 int z = (a << 1) >> 2;
890                 return (a && b) || !x;
891             }",
892            "foo.c",
893            |metric| {
894                // Expect: 6 bitwise op kinds (& | ^ ~ << >>), 3 logical (&& || !).
895                // Plus int, (), {, }, =, ;, return, , — 8 syntactic / arithmetic
896                // operator kinds.  Six bitwise + three logical + eight = 17 unique
897                // operators is the upper bound; actuals depend on grammar collapse,
898                // so we assert a lower-bound and anchor via snapshot below.
899                let s = &metric.halstead;
900                assert!(
901                    s.unique_operators() >= 14,
902                    "expected >= 14 unique operators (bitwise + logical + syntax), got {}",
903                    s.unique_operators(),
904                );
905                assert_eq!(s.unique_operands(), 8); // f, a, b, x, y, z, 1, 2
906                insta::assert_json_snapshot!(metric.halstead);
907            },
908        );
909    }
910
911    /// Increment / decrement (`++`, `--`) and `sizeof` / cast operators
912    /// each contribute distinct unique operators.  C-style casts in the
913    /// tree-sitter grammar surface as `cast_expression` with the type
914    /// token classified as a primitive_type operator.
915    #[cfg(feature = "c")]
916    #[test]
917    fn c_increment_decrement_and_sizeof() {
918        check_metrics::<CParser>(
919            "void f(int* p) {
920                 int n = sizeof(int);
921                 ++p;
922                 --n;
923                 long w = (long) n;
924             }",
925            "foo.c",
926            |metric| {
927                // Unique operators include: void, int, long, *, =, sizeof, ++, --, (), {, }, ;
928                // Unique operands: f, p, n, w
929                let s = &metric.halstead;
930                assert!(
931                    s.unique_operators() >= 10,
932                    "expected >= 10 unique operators including ++ / -- / sizeof / cast, got {}",
933                    s.unique_operators(),
934                );
935                assert_eq!(s.unique_operands(), 4);
936                insta::assert_json_snapshot!(metric.halstead);
937            },
938        );
939    }
940
941    #[cfg(feature = "cpp")]
942    #[test]
943    fn cpp_operators_and_operands() {
944        // Define operators and operands for C/C++ grammar according to this specification:
945        // https://www.verifysoft.com/en_halstead_metrics.html
946        // The only difference with the specification above is that
947        // primitive types are treated as operators, since the definition of a
948        // primitive type can be seen as the creation of a slot of a certain size.
949        // i.e. The `int a;` definition creates a n-bytes slot.
950        check_metrics::<CppParser>(
951            "main()
952            {
953              int a, b, c, avg;
954              scanf(\"%d %d %d\", &a, &b, &c);
955              avg = (a + b + c) / 3;
956              printf(\"avg = %d\", avg);
957            }",
958            "foo.c",
959            |metric| {
960                // unique operators: (), {}, int, &, =, +, /, ,, ;
961                // unique operands: main, a, b, c, avg, scanf, "%d %d %d", 3, printf, "avg = %d"
962                insta::assert_json_snapshot!(
963                    metric.halstead,
964                    @r#"
965                {
966                  "unique_operators": 9,
967                  "total_operators": 24,
968                  "unique_operands": 10,
969                  "total_operands": 18,
970                  "length": 42,
971                  "estimated_program_length": 61.74860596185444,
972                  "purity_ratio": 1.470204903853677,
973                  "vocabulary": 19,
974                  "volume": 178.41295556463058,
975                  "difficulty": 8.1,
976                  "level": 0.1234567901234568,
977                  "effort": 1445.1449400735075,
978                  "time": 80.28583000408375,
979                  "bugs": 0.04260752914034329
980                }
981                "#
982                );
983            },
984        );
985    }
986
987    /// A `sized_type_specifier` carries its `unsigned`/`signed`/`long`/
988    /// `short` modifiers as bare keyword tokens (distinct kind_ids), not
989    /// as `primitive_type` children. Prior to issue #466 those tokens
990    /// fell through to the `Unknown` arm and were dropped from `n1`/`N1`,
991    /// so `unsigned int` collapsed to just `int` and `signed long`
992    /// contributed nothing. They must each count as a distinct operator,
993    /// while `long long`'s two `long` tokens fold to one `n1` entry but
994    /// two `N1` hits. Regression test for issue #466.
995    #[cfg(feature = "cpp")]
996    #[test]
997    fn cpp_sized_type_specifier_operators() {
998        let source = "unsigned int u = 3; signed long b = 4; long long c = 5;";
999        check_metrics::<CppParser>(source, "foo.cpp", |metric| {
1000            // Distinct operators (n1): unsigned, signed, long, int, =, ; = 6
1001            // Total operators (N1):
1002            //   unsigned(1) + int(1) + =(3) + ;(3) + signed(1) + long(3) = 12
1003            //   (`long` appears once in `signed long` and twice in `long long`)
1004            // Distinct/total operands: u, b, c, 3, 4, 5 = 6 / 6
1005            assert_eq!(metric.halstead.unique_operators(), 6);
1006            assert_eq!(metric.halstead.total_operators(), 12);
1007            assert_eq!(metric.halstead.unique_operands(), 6);
1008            assert_eq!(metric.halstead.total_operands(), 6);
1009        });
1010
1011        // Pin the lesson-4 `n1 == dedupe(ops.operators)` invariant: the
1012        // kind_id-keyed metrics store and the text-keyed `--ops` store are
1013        // independent, so a modifier classified in one but not the other
1014        // would diverge here.
1015        let path = PathBuf::from("foo.cpp");
1016        let parser = CppParser::new(source.as_bytes().to_vec(), &path, None);
1017        let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
1018        let unique_operators: HashSet<&str> = ops.operators.iter().map(String::as_str).collect();
1019        assert_eq!(
1020            unique_operators.len(),
1021            6,
1022            "dedupe(ops.operators) must equal n1; operators were {:?}",
1023            ops.operators
1024        );
1025        for modifier in ["unsigned", "signed", "long"] {
1026            assert!(
1027                unique_operators.contains(modifier),
1028                "sized_type_specifier modifier {modifier:?} missing from ops.operators: {:?}",
1029                ops.operators
1030            );
1031        }
1032    }
1033
1034    /// C++20 spaceship operator `<=>` (`Cpp::LTEQGT`) is a comparison
1035    /// operator and must be counted in Halstead, like its sibling
1036    /// comparison operators `<`, `>`, `<=`, `>=`, `==`, `!=`. Prior to
1037    /// this fix it fell through to the `Unknown` arm and was silently
1038    /// dropped from `n1` / `N1`, under-reporting volume / effort on any
1039    /// C++20+ codebase that defines `operator<=>`. Regression test for
1040    /// issue #197.
1041    #[cfg(feature = "cpp")]
1042    #[test]
1043    fn cpp_spaceship_operator_is_halstead_operator() {
1044        check_metrics::<CppParser>(
1045            "int f(int a, int b) {
1046                 return (a <=> b) != 0;
1047             }",
1048            "foo.cpp",
1049            |metric| {
1050                // Unique operators (grammar collapses matched delimiters
1051                // to a single kind_id): int, (), {}, <=>, !=, return, ;, ,
1052                //   `<=>` is the regression target — without the fix it
1053                //   would be Unknown and `u_operators` would be 7.
1054                // Unique operands: f, a, b, 0
1055                let s = &metric.halstead;
1056                assert_eq!(s.unique_operators(), 8);
1057                assert_eq!(s.unique_operands(), 4);
1058                insta::assert_json_snapshot!(
1059                    s,
1060                    @r#"
1061                {
1062                  "unique_operators": 8,
1063                  "total_operators": 11,
1064                  "unique_operands": 4,
1065                  "total_operands": 6,
1066                  "length": 17,
1067                  "estimated_program_length": 32.0,
1068                  "purity_ratio": 1.8823529411764706,
1069                  "vocabulary": 12,
1070                  "volume": 60.94436251225965,
1071                  "difficulty": 6.0,
1072                  "level": 0.16666666666666666,
1073                  "effort": 365.6661750735579,
1074                  "time": 20.31478750408655,
1075                  "bugs": 0.01704519358507665
1076                }
1077                "#
1078                );
1079            },
1080        );
1081    }
1082
1083    /// C++ compound subtract-assign `-=` (`Cpp::DASHEQ`) must be counted
1084    /// in Halstead like every other compound assignment (`+=`, `*=`,
1085    /// `/=`, etc.). Prior to the fix it fell through to the `Unknown`
1086    /// arm and was silently dropped from `n1` / `N1` — under-reporting
1087    /// volume / effort wherever C++ code subtracts in place. Regression
1088    /// test for issue #198.
1089    #[cfg(feature = "cpp")]
1090    #[test]
1091    fn cpp_dash_eq_is_halstead_operator() {
1092        check_metrics::<CppParser>("void f(int a, int b) { a -= b; }", "foo.cpp", |metric| {
1093            // Unique operators: void, (), {}, int, ,, -=, ;
1094            //   `-=` is the regression target — without the fix it
1095            //   would be Unknown and `u_operators` would be 6.
1096            // Unique operands: f, a, b
1097            let s = &metric.halstead;
1098            assert_eq!(s.unique_operators(), 7);
1099            assert_eq!(s.unique_operands(), 3);
1100        });
1101    }
1102
1103    /// C++ pointer-to-member access `.*` (`Cpp::DOTSTAR`) must be
1104    /// counted in Halstead. Prior to the fix it fell through to the
1105    /// `Unknown` arm and was silently dropped from `n1` / `N1`.
1106    /// Regression test for issue #198.
1107    ///
1108    /// The snippet uses an `operator.*` declaration because that is
1109    /// where the C++ tree-sitter grammar reliably emits a single
1110    /// `DOTSTAR` leaf; in expression position (`a.*b`) some grammar
1111    /// versions split the token into `DOT` + `STAR` and the regression
1112    /// would be masked.
1113    #[cfg(feature = "cpp")]
1114    #[test]
1115    fn cpp_dot_star_is_halstead_operator() {
1116        check_metrics::<CppParser>("struct S { void operator.*(int); };", "foo.cpp", |metric| {
1117            // Unique operators with fix: {}, ;, (), int, void, .*, the
1118            //   `operator` keyword (#1296) and `struct` (#1552).
1119            //   `.*` is the regression target — without the fix it
1120            //   falls through to `Unknown` and `u_operators` is 7.
1121            // Unique operands: S
1122            let s = &metric.halstead;
1123            assert_eq!(s.unique_operators(), 8);
1124            assert_eq!(s.unique_operands(), 1);
1125        });
1126    }
1127
1128    /// C++ pointer-to-member access through pointer `->*`
1129    /// (`Cpp::DASHGTSTAR`) must be counted in Halstead. Prior to the
1130    /// fix it fell through to the `Unknown` arm and was silently
1131    /// dropped from `n1` / `N1`. Regression test for issue #198.
1132    ///
1133    /// The snippet uses an `operator->*` declaration because that is
1134    /// where the C++ tree-sitter grammar reliably emits a single
1135    /// `DASHGTSTAR` leaf; in expression position (`a->*b`) the grammar
1136    /// splits the token into `DASHGT` + `STAR` and the regression would
1137    /// be masked.
1138    #[cfg(feature = "cpp")]
1139    #[test]
1140    fn cpp_dash_gt_star_is_halstead_operator() {
1141        check_metrics::<CppParser>(
1142            "struct S { void operator->*(int); };",
1143            "foo.cpp",
1144            |metric| {
1145                // Unique operators with fix: {}, ;, (), int, void, ->*,
1146                //   the `operator` keyword (#1296) and `struct` (#1552).
1147                //   `->*` is the regression target — without the fix it
1148                //   falls through to `Unknown` and `u_operators` is 7.
1149                // Unique operands: S
1150                let s = &metric.halstead;
1151                assert_eq!(s.unique_operators(), 8);
1152                assert_eq!(s.unique_operands(), 1);
1153            },
1154        );
1155    }
1156
1157    #[cfg(feature = "cpp")]
1158    #[test]
1159    fn cpp_raw_string_delimiter_is_not_an_operator() {
1160        // Regression: issue #1314, the C++ sibling of Elixir #1256 and
1161        // Ruby/Perl #1312. A `raw_string_literal` carries its `R"(`
1162        // opener as a bare `LPAREN` child — the kind id a call uses —
1163        // so `auto a = R"(raw)";` reported a `()` operator with no call
1164        // in the source.
1165        //
1166        // The fixture holds both sides at once: two raw strings and one
1167        // real call. A guard widened past the literal would drop
1168        // `f(a)`'s parenthesis and fail here rather than silently
1169        // passing.
1170        //
1171        // The second literal uses the custom-delimiter form to pin that
1172        // shape too — it adds a `raw_string_delimiter` child but keeps
1173        // the same `(` — and its distinct text makes n2 differ from N2.
1174        //
1175        // expected: operators `;` × 3, `=` × 3, `int`, `()` × 1 →
1176        // n1 = 4, N1 = 8. Operands the two literals, `a` × 2, `b`, `c`,
1177        // `f` → n2 = 6, N2 = 7. Before the guard the two openers added
1178        // two more `()` → N1 = 10.
1179        check_metrics::<CppParser>(
1180            "auto a = R\"(raw)\";\nauto b = R\"tag(raw)tag\";\nint c = f(a);\n",
1181            "foo.cpp",
1182            |metric| {
1183                assert_eq!(metric.halstead.unique_operators(), 4);
1184                assert_eq!(metric.halstead.total_operators(), 8);
1185                assert_eq!(metric.halstead.unique_operands(), 6);
1186                assert_eq!(metric.halstead.total_operands(), 7);
1187            },
1188        );
1189    }
1190
1191    #[cfg(feature = "rust")]
1192    #[test]
1193    fn rust_operators_and_operands() {
1194        check_metrics::<RustParser>(
1195            "fn main() {
1196              let a = 5; let b = 5; let c = 5;
1197              let avg = (a + b + c) / 3;
1198              println!(\"{}\", avg);
1199            }",
1200            "foo.rs",
1201            |metric| {
1202                // unique operators: fn, (), {}, let, =, +, /, ;, !, ,
1203                // unique operands: main, a, b, c, avg, 5, 3, println, "{}"
1204                insta::assert_json_snapshot!(
1205                    metric.halstead,
1206                    @r#"
1207                {
1208                  "unique_operators": 10,
1209                  "total_operators": 23,
1210                  "unique_operands": 9,
1211                  "total_operands": 15,
1212                  "length": 38,
1213                  "estimated_program_length": 61.74860596185444,
1214                  "purity_ratio": 1.624963314785643,
1215                  "vocabulary": 19,
1216                  "volume": 161.42124551085624,
1217                  "difficulty": 8.333333333333334,
1218                  "level": 0.12,
1219                  "effort": 1345.177045923802,
1220                  "time": 74.7320581068779,
1221                  "bugs": 0.040619232256751396
1222                }
1223                "#
1224                );
1225            },
1226        );
1227    }
1228
1229    #[cfg(feature = "rust")]
1230    #[test]
1231    fn rust_aliased_primitive_type_classification() {
1232        // Regression for issue #95 (lesson #2): the Rust grammar emits 17
1233        // distinct `kind_id`s for `primitive_type` (one base plus 16
1234        // numeric-suffixed alias variants). `RustCode::is_primitive` in
1235        // `big-code-analysis-ast/src/checker.rs` must list every variant;
1236        // if a future regression omits one, primitive type names emitted
1237        // in that aliased position silently drop into the kind_id-keyed
1238        // operators bucket instead of the text-keyed primitive_operators
1239        // map, miscounting Halstead n1.
1240        //
1241        // The snippet exercises every primitive scalar type across many
1242        // syntactic positions (function parameter types, return types,
1243        // let-binding annotations, `as` casts, const items, type aliases,
1244        // struct fields, function pointer types, tuple types, array types,
1245        // reference types, generic type arguments). Empirically, ordinary
1246        // Rust source emits the base `Rust::PrimitiveType` variant from
1247        // all of these positions; the 16 suffixed alias variants are
1248        // produced by specific grammar productions not reachable from
1249        // user-written code. Mutation-verified: dropping
1250        // `Rust::PrimitiveType` from `is_primitive` fails this test
1251        // (u_operators 30→15). Dropping any single suffixed variant
1252        // currently leaves the test passing; if a future grammar bump
1253        // makes any suffixed variant reachable from idiomatic source,
1254        // extend the snippet so the test fires for that variant too.
1255        check_metrics::<RustParser>(
1256            "const C: u8 = 0;
1257            type T = i64;
1258            struct S { x: u32, y: u64 }
1259            fn g(p: fn(u8) -> u16) -> bool { let _ = p(0); true }
1260            fn f(a: u8, b: u16, c: u32, d: u64) -> u128 {
1261                let _x: i8 = 0;
1262                let _y: i16 = 0;
1263                let _z: i32 = 0;
1264                let _w: i64 = 0;
1265                let _v: i128 = 0;
1266                let _p: f32 = 1.0;
1267                let _q: f64 = 2.0;
1268                let _r: bool = true;
1269                let _s: char = 'x';
1270                let _t: usize = 0;
1271                let _u: isize = 0;
1272                let _arr: [u32; 4] = [0; 4];
1273                let _ref: &u8 = &0;
1274                let _tup: (u32, u64) = (0, 0);
1275                let _opt: Option<u32> = None;
1276                a as u128 + b as u128 + c as u128 + d
1277            }",
1278            "foo.rs",
1279            |metric| {
1280                // Headline: u_operators is the load-bearing assertion —
1281                // the 16 distinct primitive type names dedupe by text in
1282                // the primitive_operators map. Total operators (N1) and
1283                // operand counts pin the rest of the Halstead state.
1284                // Grew from 30 → 33 with the issue #394 fix: `const`,
1285                // `type`, and `struct` keywords are now classified as
1286                // operators (one occurrence each). #1486 adds the
1287                // annotation `:` — one entry, 23 occurrences: `C`, the
1288                // two fields, `p`, the four params of `f`, and the
1289                // fifteen `let` bindings. #1559 bills `Option<u32>`'s
1290                // brackets as one `<>` pair: one entry and one
1291                // occurrence fewer.
1292                assert_eq!(metric.halstead.unique_operators(), 33);
1293                assert_eq!(metric.halstead.total_operators(), 143);
1294                // u_operands / operands grew (was 31/50 before #390): the
1295                // fix now classifies TypeIdentifier (`T`, `S`, `Option`)
1296                // and FieldIdentifier (struct fields `x`, `y`) as operands
1297                // alongside the existing primitive type names.
1298                assert_eq!(metric.halstead.unique_operands(), 36);
1299                assert_eq!(metric.halstead.total_operands(), 55);
1300            },
1301        );
1302    }
1303
1304    #[cfg(feature = "rust")]
1305    #[test]
1306    fn rust_field_identifier_is_operand() {
1307        // Regression for issue #390: prior to the fix, `FieldIdentifier`
1308        // (e.g. the `x` / `y` in `p.x`, `p.y`) fell through to
1309        // `TokenRole::Unknown`, so the field names were not counted
1310        // as operands. Both C++ and Go already classify FieldIdentifier
1311        // as an operand. After the fix:
1312        //   unique operators: fn, (), {}, let, =, +, ;, ., ,, :
1313        //   unique operands : main, p, Point, x, y, sum, 0, 1
1314        // Field names `x` and `y` each appear twice (`p.x + p.y` and
1315        // the struct literal `Point { x: 0, y: 1 }`).
1316        check_metrics::<RustParser>(
1317            "fn main() {
1318              let p = Point { x: 0, y: 1 };
1319              let sum = p.x + p.y;
1320            }",
1321            "foo.rs",
1322            |metric| {
1323                // Headline: pre-fix, FieldIdentifier (`x`, `y`) and
1324                // TypeIdentifier (`Point`) fell through to Unknown, so
1325                // u_operands was 5 (main, p, sum, 0, 1). After the
1326                // fix, +Point, +x, +y → 8 distinct names.
1327                assert_eq!(metric.halstead.unique_operands(), 8);
1328                assert_eq!(metric.halstead.total_operands(), 12);
1329                // N1: fn, (), {} ×2, let ×2, = ×2, +, ; ×2, . ×2, `,`,
1330                // and the struct literal's two field `:` (#1486) = 16.
1331                assert_eq!(metric.halstead.unique_operators(), 10);
1332                assert_eq!(metric.halstead.total_operators(), 16);
1333                insta::assert_json_snapshot!(
1334                    metric.halstead,
1335                    @r#"
1336                {
1337                  "unique_operators": 10,
1338                  "total_operators": 16,
1339                  "unique_operands": 8,
1340                  "total_operands": 12,
1341                  "length": 28,
1342                  "estimated_program_length": 57.219280948873624,
1343                  "purity_ratio": 2.043545748174058,
1344                  "vocabulary": 18,
1345                  "volume": 116.75790004038474,
1346                  "difficulty": 7.5,
1347                  "level": 0.13333333333333333,
1348                  "effort": 875.6842503028855,
1349                  "time": 48.64912501682697,
1350                  "bugs": 0.03051010983631521
1351                }
1352                "#
1353                );
1354            },
1355        );
1356    }
1357
1358    #[cfg(feature = "rust")]
1359    #[test]
1360    fn rust_type_identifier_is_operand() {
1361        // Regression for issue #390: `TypeIdentifier` (e.g. `Vec`,
1362        // `HashMap`, `String` when used as a path name) was dropped to
1363        // `TokenRole::Unknown` for Rust. C++ and Go classify them as
1364        // operands. After the fix, u_operands = 8:
1365        //   main, v, m, Vec, HashMap, new, K, V
1366        // (`i32` is a primitive type, classified as an operator.)
1367        //
1368        // Also covers issue #394: `::` is now an operator. The snippet
1369        // has two `::` tokens (`Vec::new`, `HashMap::new`), so n1 grew
1370        // from 10 → 11 and N1 from 17 → 19. #1486 then billed the two
1371        // annotation `:` tokens as their own entry: 12 and 21. #1559
1372        // billed each generic's brackets as one `<>` pair, replacing the
1373        // `<` and `>` entries and their four occurrences: 11 and 19.
1374        check_metrics::<RustParser>(
1375            "fn main() {
1376              let v: Vec<i32> = Vec::new();
1377              let m: HashMap<K, V> = HashMap::new();
1378            }",
1379            "foo.rs",
1380            |metric| {
1381                // Headline: u_operands includes `Vec`, `HashMap`, `K`,
1382                // `V` (and `i32` as a primitive operator). Without the
1383                // fix, Vec/HashMap/K/V silently dropped to Unknown.
1384                assert_eq!(metric.halstead.unique_operands(), 8);
1385                assert_eq!(metric.halstead.total_operands(), 11);
1386                // `::` appears twice (Vec::new, HashMap::new); without
1387                // the #394 fix u_operators was 10 and operators 17.
1388                // `:` appears twice and stays distinct from `::`.
1389                assert_eq!(metric.halstead.unique_operators(), 11);
1390                assert_eq!(metric.halstead.total_operators(), 19);
1391                insta::assert_json_snapshot!(
1392                    metric.halstead,
1393                    @r#"
1394                {
1395                  "unique_operators": 11,
1396                  "total_operators": 19,
1397                  "unique_operands": 8,
1398                  "total_operands": 11,
1399                  "length": 30,
1400                  "estimated_program_length": 62.05374780501027,
1401                  "purity_ratio": 2.068458260167009,
1402                  "vocabulary": 19,
1403                  "volume": 127.43782540330756,
1404                  "difficulty": 7.5625,
1405                  "level": 0.1322314049586777,
1406                  "effort": 963.7485546125134,
1407                  "time": 53.54158636736186,
1408                  "bugs": 0.03252279825177962
1409                }
1410                "#
1411                );
1412            },
1413        );
1414    }
1415
1416    /// #1486: Rust's single `:` is vocabulary in every position it is
1417    /// spelled, and stays a separate entry from `::`.
1418    #[cfg(feature = "rust")]
1419    #[test]
1420    fn rust_colon_is_operator_1486() {
1421        const ROWS: &[(&str, &str, [u64; 2])] = &[
1422            // `let`, `:`, `u8`, `=`, `;`
1423            ("let annotation", "let x: u8 = 1;\n", [5, 5]),
1424            // `struct`, `{}`, `:`, `u8`
1425            ("field declaration", "struct S { a: u8 }\n", [4, 4]),
1426            // `const`, `:` twice, `=`, `{}`, `;`
1427            ("field initializer", "const C: S = S { a: 1 };\n", [5, 6]),
1428            // `fn`, `{}` twice, `()`, `let`, `{}` (pattern), `:`, `=`, `;`
1429            ("field pattern", "fn f() { let S { a: b } = s; }\n", [7, 8]),
1430            // `fn`, `()`, `:`, `u8`, `{}`
1431            ("parameter", "fn f(x: u8) {}\n", [5, 5]),
1432            // `fn`, `<>`, `:`, `()`, `{}`
1433            ("trait bound", "fn f<T: Clone>() {}\n", [5, 5]),
1434            // `fn`, `<>`, `()`, `where`, `:`, `{}`
1435            ("where clause", "fn f<T>() where T: Clone {}\n", [6, 6]),
1436            // `fn`, `()`, `{}` twice, `:`, `loop`, `break`, `;`
1437            ("loop label", "fn f() { 'a: loop { break 'a; } }\n", [7, 8]),
1438            // `fn`, `()`, `{}`, `let`, `=`, `|` twice, `:`, `u8`, `;`
1439            (
1440                "closure parameter",
1441                "fn f() { let c = |q: u8| q; }\n",
1442                [9, 10],
1443            ),
1444            // `fn`, `()` twice, `{}`, `let`, `:`, `<>`, `u8`, `=`, `::`,
1445            // `;` — `:` and `::` are two entries
1446            (
1447                "beside a path",
1448                "fn f() { let v: Vec<u8> = Vec::new(); }\n",
1449                [10, 11],
1450            ),
1451        ];
1452        assert_operator_rows::<RustParser>("foo.rs", ROWS);
1453    }
1454
1455    #[cfg(feature = "rust")]
1456    #[test]
1457    fn rust_path_separator_is_operator() {
1458        // Regression for issue #394: `::` (`COLONCOLON`) was missing
1459        // from the Rust `get_op_type` operator arm even though C++,
1460        // Java, C#, and Kotlin all classify it as an operator. Path-
1461        // heavy code (`std::collections::HashMap`, `Vec::new`,
1462        // `T::method`) had every `::` silently dropped into
1463        // TokenRole::Unknown.
1464        //
1465        // Snippet has three `::` tokens (`std::collections::HashMap`,
1466        // counted as two `::` separators, plus `HashMap::new`).
1467        check_metrics::<RustParser>(
1468            "fn main() {
1469              let m = std::collections::HashMap::new();
1470            }",
1471            "foo.rs",
1472            |metric| {
1473                // `::` appears 3 times across the two path expressions
1474                // (`std::collections::HashMap` contributes two; the
1475                // `HashMap::new` contributes one). Pre-fix all three
1476                // dropped to Unknown: u_operators would be 6 (no `::`
1477                // distinct) and total_operators() would be 7 (minus 3 `::`
1478                // occurrences). With the fix u_operators=7 and
1479                // operators=10.
1480                //
1481                // unique operators (post-fix): fn, LPAREN, LBRACE,
1482                // let, =, ::, ;. unique operands: main, m, std,
1483                // collections, HashMap, new.
1484                assert_eq!(metric.halstead.unique_operators(), 7);
1485                assert_eq!(metric.halstead.total_operators(), 10);
1486                assert_eq!(metric.halstead.unique_operands(), 6);
1487                assert_eq!(metric.halstead.total_operands(), 6);
1488            },
1489        );
1490    }
1491
1492    #[cfg(feature = "rust")]
1493    #[test]
1494    fn rust_declaration_keywords_are_operators() {
1495        // Regression for issue #394: the Rust impl already accepted 17
1496        // keywords as operators (As, Async, Await, …, Fn) but omitted
1497        // 14 declaration / visibility keywords. The fix adds `Const`,
1498        // `Static`, `Enum`, `Struct`, `Trait`, `Impl`, `Use`, `Mod`,
1499        // `Pub`, `Type`, `Union`, `Where`, `Extern`, `Dyn`.
1500        //
1501        // Snippet exercises `use`, `pub`, `struct`, and `impl` (one of
1502        // each); together they account for 4 new operator occurrences
1503        // and 4 new unique operators.
1504        check_metrics::<RustParser>(
1505            "use std::fmt;
1506            pub struct S;
1507            impl S { fn n() -> u8 { 0 } }",
1508            "foo.rs",
1509            |metric| {
1510                // expected: unique operators (11) = use, ::, ;, pub,
1511                // struct, impl, LBRACE, fn, LPAREN, DASHGT, u8. Without
1512                // the #394 fix, `use`, `pub`, `struct`, and `impl`
1513                // would each drop to Unknown and u_operators would be
1514                // 7. unique operands (5): std, fmt, S, n, 0.
1515                assert_eq!(metric.halstead.unique_operators(), 11);
1516                assert_eq!(metric.halstead.total_operators(), 13);
1517                assert_eq!(metric.halstead.unique_operands(), 5);
1518                assert_eq!(metric.halstead.total_operands(), 6);
1519            },
1520        );
1521    }
1522
1523    #[cfg(feature = "javascript")]
1524    #[test]
1525    fn javascript_operators_and_operands() {
1526        check_metrics::<JavascriptParser>(
1527            "function main() {
1528              var a, b, c, avg;
1529              a = 5; b = 5; c = 5;
1530              avg = (a + b + c) / 3;
1531              console.log(\"{}\", avg);
1532            }",
1533            "foo.js",
1534            |metric| {
1535                // unique operators: function, (), {}, var, =, +, /, ,, ., ;
1536                // unique operands: main, a, b, c, avg, 3, 5, console, log, "{}"
1537                // `console.log` is the `.` operator applied to the two
1538                // identifier leaves; the composite `member_expression`
1539                // text is deliberately not a third operand (#1263), so
1540                // n2/N2 are 10/20 rather than the pre-#1263 11/21.
1541                insta::assert_json_snapshot!(
1542                    metric.halstead,
1543                    @r#"
1544                {
1545                  "unique_operators": 10,
1546                  "total_operators": 24,
1547                  "unique_operands": 10,
1548                  "total_operands": 20,
1549                  "length": 44,
1550                  "estimated_program_length": 66.43856189774725,
1551                  "purity_ratio": 1.5099673158578921,
1552                  "vocabulary": 20,
1553                  "volume": 190.16483617504394,
1554                  "difficulty": 10.0,
1555                  "level": 0.1,
1556                  "effort": 1901.6483617504396,
1557                  "time": 105.64713120835775,
1558                  "bugs": 0.05116412536051621
1559                }
1560                "#
1561                );
1562            },
1563        );
1564    }
1565
1566    #[cfg(feature = "mozjs")]
1567    #[test]
1568    fn mozjs_operators_and_operands() {
1569        check_metrics::<MozjsParser>(
1570            "function main() {
1571              var a, b, c, avg;
1572              a = 5; b = 5; c = 5;
1573              avg = (a + b + c) / 3;
1574              console.log(\"{}\", avg);
1575            }",
1576            "foo.js",
1577            |metric| {
1578                // unique operators: function, (), {}, var, =, +, /, ,, ., ;
1579                // unique operands: main, a, b, c, avg, 3, 5, console, log, "{}"
1580                // `console.log` is the `.` operator applied to the two
1581                // identifier leaves; the composite `member_expression`
1582                // text is deliberately not a third operand (#1263), so
1583                // n2/N2 are 10/20 rather than the pre-#1263 11/21.
1584                insta::assert_json_snapshot!(
1585                    metric.halstead,
1586                    @r#"
1587                {
1588                  "unique_operators": 10,
1589                  "total_operators": 24,
1590                  "unique_operands": 10,
1591                  "total_operands": 20,
1592                  "length": 44,
1593                  "estimated_program_length": 66.43856189774725,
1594                  "purity_ratio": 1.5099673158578921,
1595                  "vocabulary": 20,
1596                  "volume": 190.16483617504394,
1597                  "difficulty": 10.0,
1598                  "level": 0.1,
1599                  "effort": 1901.6483617504396,
1600                  "time": 105.64713120835775,
1601                  "bugs": 0.05116412536051621
1602                }
1603                "#
1604                );
1605            },
1606        );
1607    }
1608
1609    #[cfg(feature = "typescript")]
1610    #[test]
1611    fn typescript_operators_and_operands() {
1612        check_metrics::<TypescriptParser>(
1613            "function main() {
1614              var a, b, c, avg;
1615              a = 5; b = 5; c = 5;
1616              avg = (a + b + c) / 3;
1617              console.log(\"{}\", avg);
1618            }",
1619            "foo.ts",
1620            |metric| {
1621                // unique operators: function, (), {}, var, =, +, /, ,, ., ;
1622                // unique operands: main, a, b, c, avg, 3, 5, console, log, "{}"
1623                // `console.log` is the `.` operator applied to the two
1624                // identifier leaves; the composite `member_expression`
1625                // text is deliberately not a third operand (#1263), so
1626                // n2/N2 are 10/20 rather than the pre-#1263 11/21.
1627                insta::assert_json_snapshot!(
1628                    metric.halstead,
1629                    @r#"
1630                {
1631                  "unique_operators": 10,
1632                  "total_operators": 24,
1633                  "unique_operands": 10,
1634                  "total_operands": 20,
1635                  "length": 44,
1636                  "estimated_program_length": 66.43856189774725,
1637                  "purity_ratio": 1.5099673158578921,
1638                  "vocabulary": 20,
1639                  "volume": 190.16483617504394,
1640                  "difficulty": 10.0,
1641                  "level": 0.1,
1642                  "effort": 1901.6483617504396,
1643                  "time": 105.64713120835775,
1644                  "bugs": 0.05116412536051621
1645                }
1646                "#
1647                );
1648            },
1649        );
1650    }
1651
1652    #[cfg(feature = "typescript")]
1653    #[test]
1654    fn tsx_operators_and_operands() {
1655        check_metrics::<TsxParser>(
1656            "function main() {
1657              var a, b, c, avg;
1658              a = 5; b = 5; c = 5;
1659              avg = (a + b + c) / 3;
1660              console.log(\"{}\", avg);
1661            }",
1662            "foo.ts",
1663            |metric| {
1664                // unique operators: function, (), {}, var, =, +, /, ,, ., ;
1665                // unique operands: main, a, b, c, avg, 3, 5, console, log, "{}"
1666                // `console.log` is the `.` operator applied to the two
1667                // identifier leaves; the composite `member_expression`
1668                // text is deliberately not a third operand (#1263), so
1669                // n2/N2 are 10/20 rather than the pre-#1263 11/21.
1670                insta::assert_json_snapshot!(
1671                    metric.halstead,
1672                    @r#"
1673                {
1674                  "unique_operators": 10,
1675                  "total_operators": 24,
1676                  "unique_operands": 10,
1677                  "total_operands": 20,
1678                  "length": 44,
1679                  "estimated_program_length": 66.43856189774725,
1680                  "purity_ratio": 1.5099673158578921,
1681                  "vocabulary": 20,
1682                  "volume": 190.16483617504394,
1683                  "difficulty": 10.0,
1684                  "level": 0.1,
1685                  "effort": 1901.6483617504396,
1686                  "time": 105.64713120835775,
1687                  "bugs": 0.05116412536051621
1688                }
1689                "#
1690                );
1691            },
1692        );
1693    }
1694
1695    #[cfg(feature = "javascript")]
1696    #[test]
1697    fn javascript_template_string_plain_is_operand() {
1698        // Regression: issue #192. A backtick-delimited `` `hello` ``
1699        // without `${...}` is semantically identical to `"hello"` /
1700        // `'hello'` and must contribute exactly one operand — before
1701        // the fix `TemplateString` fell through to `TokenRole::Unknown`
1702        // and contributed zero. expected: operands are `f` (function
1703        // name) and the wrapping `` `hello` `` template literal →
1704        // u_operands = 2, N2 = 2 (matches the equivalent
1705        // `function f() { return "hello"; }` baseline).
1706        check_metrics::<JavascriptParser>("function f() { return `hello`; }", "foo.js", |metric| {
1707            assert_eq!(metric.halstead.unique_operands(), 2);
1708            assert_eq!(metric.halstead.total_operands(), 2);
1709        });
1710    }
1711
1712    /// Regression for #695. The `get` / `set` property-accessor keywords
1713    /// are operators, matching the C# getter's `Get | Set | Init | Add |
1714    /// Remove` accessor arm. Before #695 the JS family classified them as
1715    /// operands, so the same accessor keyword landed in opposite Halstead
1716    /// groups across languages. This pins them in the operator store and
1717    /// out of the operand store.
1718    #[cfg(feature = "javascript")]
1719    #[test]
1720    fn js_get_set_accessors_are_operators() {
1721        let source = "class C { get x() { return 1; } set x(v) { this._x = v; } }";
1722        let path = PathBuf::from("foo.js");
1723        let parser = JavascriptParser::new(source.as_bytes().to_vec(), &path, None);
1724        let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
1725        assert!(
1726            ops.operators.iter().any(|o| o.as_str() == "get")
1727                && ops.operators.iter().any(|o| o.as_str() == "set"),
1728            "`get`/`set` accessors must be operators; operators were {:?}",
1729            ops.operators
1730        );
1731        assert!(
1732            !ops.operands.iter().any(|o| o.as_str() == "get")
1733                && !ops.operands.iter().any(|o| o.as_str() == "set"),
1734            "`get`/`set` accessors must not be operands; operands were {:?}",
1735            ops.operands
1736        );
1737    }
1738
1739    #[cfg(feature = "javascript")]
1740    #[test]
1741    fn javascript_template_string_interpolation_no_double_count() {
1742        // Regression: issue #192. An interpolated template literal
1743        // `` `Hi ${name}!` `` used to fall through to `Unknown`,
1744        // dropping the wrapper from the count entirely; the inner
1745        // `name` was still walked and counted via the
1746        // `TemplateSubstitution` child. Mirrors #183 (C#), #191
1747        // (Kotlin), #199 (Perl): the wrapper is skipped when a
1748        // `TemplateSubstitution` child is present so the inner
1749        // expression is not double-counted.
1750        //
1751        // expected: for `function f(name) { return ` + "`Hi ${name}!`"
1752        // + `; }`, operands are `f` and `name` (twice — `name` as the
1753        // parameter, then again inside the interpolation), so
1754        // u_operands = 2 and N2 = 3. Without the wrapper-skip guard
1755        // the wrapping literal would also be counted, lifting
1756        // u_operands to 3 and N2 to 4.
1757        check_metrics::<JavascriptParser>(
1758            "function f(name) { return `Hi ${name}!`; }",
1759            "foo.js",
1760            |metric| {
1761                assert_eq!(metric.halstead.unique_operands(), 2);
1762                assert_eq!(metric.halstead.total_operands(), 3);
1763            },
1764        );
1765    }
1766
1767    #[cfg(feature = "mozjs")]
1768    #[test]
1769    fn mozjs_template_string_plain_is_operand() {
1770        // Regression: issue #192. Mirrors
1771        // `javascript_template_string_plain_is_operand` for the
1772        // Firefox-mode dialect — the four JS-family `get_op_type`
1773        // impls share the same template-literal handling.
1774        check_metrics::<MozjsParser>("function f() { return `hello`; }", "foo.js", |metric| {
1775            assert_eq!(metric.halstead.unique_operands(), 2);
1776            assert_eq!(metric.halstead.total_operands(), 2);
1777        });
1778    }
1779
1780    #[cfg(feature = "mozjs")]
1781    #[test]
1782    fn mozjs_template_string_interpolation_no_double_count() {
1783        // Regression: issue #192. Mirrors
1784        // `javascript_template_string_interpolation_no_double_count`
1785        // for the Firefox-mode dialect.
1786        check_metrics::<MozjsParser>(
1787            "function f(name) { return `Hi ${name}!`; }",
1788            "foo.js",
1789            |metric| {
1790                assert_eq!(metric.halstead.unique_operands(), 2);
1791                assert_eq!(metric.halstead.total_operands(), 3);
1792            },
1793        );
1794    }
1795
1796    /// One JSX shape per row of #1483's decision: the source, the
1797    /// expected `n2` / `N2`, and the deduplicated operand vocabulary.
1798    /// Every row is a single element bound to `a`, so the operands
1799    /// outside the element's content are always `a` plus the tag names.
1800    #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
1801    const JSX_TEXT_CASES: &[(&str, &str, [u64; 2], &[&str])] = &[
1802        // Element text is one operand. N2: a, p x2 (both tags), hi.
1803        ("text", "const a = <p>hi</p>;\n", [3, 4], &["a", "p", "hi"]),
1804        // The same-line space between `<b/>` and `<i/>` is a jsx_text
1805        // node of its own, and bills nothing. Billing it would key an
1806        // empty operand: (5, 6).
1807        (
1808            "whitespace",
1809            "const a = <p><b/> <i/></p>;\n",
1810            [4, 5],
1811            &["a", "p", "b", "i"],
1812        ),
1813        // `Start` at two indentation depths is one distinct operand,
1814        // because the key is the trimmed text. Untrimmed: (5, 7).
1815        (
1816            "indentation",
1817            "const a = (\n  <p>\n    Start\n    <span>\n      Start\n    </span>\n  </p>\n);\n",
1818            [4, 7],
1819            &["a", "p", "span", "Start"],
1820        ),
1821        // An entity splits the text into three siblings, each an
1822        // operand. Without the entity arm: (4, 5).
1823        (
1824            "entity in text",
1825            "const a = <p>x &amp; y</p>;\n",
1826            [5, 6],
1827            &["a", "p", "x", "&amp;", "y"],
1828        ),
1829        // Inside an attribute string the entity is part of the string
1830        // operand and bills nothing of its own. Without the parent
1831        // guard: (5, 5).
1832        (
1833            "entity in attribute",
1834            "const a = <p title=\"x &amp; y\" />;\n",
1835            [4, 4],
1836            &["a", "p", "title", "\"x &amp; y\""],
1837        ),
1838        // An expression child is unchanged by #1483: its identifier was
1839        // already an operand.
1840        (
1841            "expression",
1842            "const a = <p>{v}</p>;\n",
1843            [3, 4],
1844            &["a", "p", "v"],
1845        ),
1846    ];
1847
1848    /// Asserts every row of [`JSX_TEXT_CASES`] under parser `T`, through
1849    /// the metrics store (`n2` / `N2`) and the `--ops` vocabulary.
1850    #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
1851    #[track_caller]
1852    fn assert_jsx_text_operands<T: crate::MetricSuite>(file: &str) {
1853        for (label, source, expected, vocabulary) in JSX_TEXT_CASES {
1854            crate::test_support::check_func_space_only::<T, _>(
1855                source,
1856                file,
1857                &[crate::Metric::Halstead],
1858                |space| {
1859                    let halstead = &space.metrics.halstead;
1860                    assert_eq!(
1861                        [halstead.unique_operands(), halstead.total_operands()],
1862                        *expected,
1863                        "{file}: {label}"
1864                    );
1865                },
1866            );
1867            let ops = ops_of::<T>(source, file);
1868            let got: BTreeSet<&str> = ops.operands.iter().map(String::as_str).collect();
1869            let want: BTreeSet<&str> = vocabulary.iter().copied().collect();
1870            assert_eq!(got, want, "{file}: {label} operand vocabulary");
1871        }
1872    }
1873
1874    /// Regression for #1483: JSX element text and its character
1875    /// references are operands. One row per grammar, so reverting one
1876    /// invocation's `jsx:` argument fails its own row.
1877    #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
1878    #[test]
1879    fn jsx_text_is_an_operand() {
1880        // A whitespace-only node bills nothing, so no count can tell the
1881        // "whitespace" row's fixture from one that stopped spelling the
1882        // same-line space. Pin the node's presence directly.
1883        let (_, source, _, _) = JSX_TEXT_CASES
1884            .iter()
1885            .find(|(label, ..)| *label == "whitespace")
1886            .expect("the whitespace row exists");
1887        let parser =
1888            JavascriptParser::new(source.as_bytes().to_vec(), &PathBuf::from("foo.jsx"), None);
1889        assert!(
1890            parser.root().preorder().any(|n| {
1891                n.kind_id() == Javascript::JsxText as u16
1892                    && source.as_bytes()[n.start_byte()..n.end_byte()]
1893                        .trim_ascii()
1894                        .is_empty()
1895            }),
1896            "the whitespace row must carry a whitespace-only jsx_text node"
1897        );
1898
1899        assert_jsx_text_operands::<JavascriptParser>("foo.jsx");
1900        assert_jsx_text_operands::<MozjsParser>("foo.js");
1901        assert_jsx_text_operands::<TsxParser>("foo.tsx");
1902    }
1903
1904    /// The `.ts` grammar's enum carries `JsxText` in the externals it
1905    /// shares with TSX, but no production emits it, which is why the
1906    /// TypeScript invocation passes no `jsx:` argument (#1483). Pinned
1907    /// so a grammar that starts emitting it cannot go unbilled quietly.
1908    #[cfg(feature = "typescript")]
1909    #[test]
1910    fn typescript_never_emits_jsx_text() {
1911        let parser = TypescriptParser::new(
1912            b"const a = <p>hi</p>;\nlet b = <T>(x);\n".to_vec(),
1913            &PathBuf::from("foo.ts"),
1914            None,
1915        );
1916        // The positive half: the parse reached the angle-bracket syntax
1917        // TSX would read as JSX, so the absence below is a property of
1918        // the grammar rather than of a parse that produced nothing.
1919        assert!(
1920            ast_has_kind_id(&parser, Typescript::TypeAssertion as u16),
1921            "fixture lost the `<T>(x)` type assertion"
1922        );
1923        assert!(
1924            !ast_has_kind_id(&parser, Typescript::JsxText as u16),
1925            "the .ts grammar now emits jsx_text; give its invocation a `jsx:` argument"
1926        );
1927    }
1928
1929    #[cfg(feature = "typescript")]
1930    #[test]
1931    fn typescript_template_string_plain_is_operand() {
1932        // Regression: issue #192. Mirrors
1933        // `javascript_template_string_plain_is_operand` for
1934        // TypeScript — the four JS-family `get_op_type` impls share
1935        // the same template-literal handling.
1936        //
1937        // The `: string` annotation contributes no operand — its
1938        // keyword counts once, as the text-keyed operator (#1261) — so
1939        // the operands are `f` and `` `hello` `` (2 each). The headline
1940        // of this test — that the plain template literal contributes
1941        // one operand — is unaffected.
1942        check_metrics::<TypescriptParser>(
1943            "function f(): string { return `hello`; }",
1944            "foo.ts",
1945            |metric| {
1946                assert_eq!(metric.halstead.unique_operands(), 2);
1947                assert_eq!(metric.halstead.total_operands(), 2);
1948            },
1949        );
1950    }
1951
1952    #[cfg(feature = "typescript")]
1953    #[test]
1954    fn typescript_template_string_interpolation_no_double_count() {
1955        // Regression: issue #192. Mirrors
1956        // `javascript_template_string_interpolation_no_double_count`
1957        // for TypeScript.
1958        //
1959        // The `: string` annotations contribute no operands (#1261).
1960        // Unique operands: `f`, `name` (2). Total operands: `f`, `name`
1961        // (param), `name` (in the interpolation) (3). The interpolation
1962        // guard from #192 still holds — the wrapping `` `Hi ${name}!` ``
1963        // is `Unknown`, not double-counted.
1964        check_metrics::<TypescriptParser>(
1965            "function f(name: string): string { return `Hi ${name}!`; }",
1966            "foo.ts",
1967            |metric| {
1968                assert_eq!(metric.halstead.unique_operands(), 2);
1969                assert_eq!(metric.halstead.total_operands(), 3);
1970            },
1971        );
1972    }
1973
1974    #[cfg(feature = "typescript")]
1975    #[test]
1976    fn tsx_template_string_plain_is_operand() {
1977        // Regression: issue #192. Mirrors
1978        // `javascript_template_string_plain_is_operand` for the
1979        // TSX (TypeScript + JSX) variant.
1980        //
1981        // TSX's type-keyword `string` (`String3`) contributes no
1982        // operand, mirroring TS::String2 (#1261): operands are `f` and
1983        // `` `hello` `` (2 each).
1984        check_metrics::<TsxParser>(
1985            "function f(): string { return `hello`; }",
1986            "foo.tsx",
1987            |metric| {
1988                assert_eq!(metric.halstead.unique_operands(), 2);
1989                assert_eq!(metric.halstead.total_operands(), 2);
1990            },
1991        );
1992    }
1993
1994    #[cfg(feature = "typescript")]
1995    #[test]
1996    fn tsx_template_string_interpolation_no_double_count() {
1997        // Regression: issue #192. Mirrors
1998        // `javascript_template_string_interpolation_no_double_count`
1999        // for the TSX (TypeScript + JSX) variant.
2000        //
2001        // The `: string` annotations contribute no `String3` operands
2002        // (#1261); see `typescript_template_string_…` for the count
2003        // derivation.
2004        check_metrics::<TsxParser>(
2005            "function f(name: string): string { return `Hi ${name}!`; }",
2006            "foo.tsx",
2007            |metric| {
2008                assert_eq!(metric.halstead.unique_operands(), 2);
2009                assert_eq!(metric.halstead.total_operands(), 3);
2010            },
2011        );
2012    }
2013
2014    /// The JS-family regex fixture, asserted against all four grammars.
2015    ///
2016    /// `impl_js_family_get_op_type!` is instantiated four times against
2017    /// four distinct `kind_id` enums (`SLASH` 87/81/90/87, `Regex`
2018    /// 224/250/264/225), so each expansion is a separate compiled arm
2019    /// and a drift in one grammar is invisible if only one is checked
2020    /// (grammar-dispatch section 11).
2021    #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2022    fn assert_js_family_counts(source: &str, expected: [u64; 4]) {
2023        assert_halstead_counts::<JavascriptParser>(source, "foo.js", expected, "javascript");
2024        assert_halstead_counts::<MozjsParser>(source, "foo.jsm", expected, "mozjs");
2025        assert_halstead_counts::<TypescriptParser>(source, "foo.ts", expected, "typescript");
2026        assert_halstead_counts::<TsxParser>(source, "foo.tsx", expected, "tsx");
2027    }
2028
2029    #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2030    #[test]
2031    fn js_family_regex_delimiters_are_not_operators() {
2032        // Regression: issue #1314, the JS-family sibling of Elixir
2033        // #1256 and Ruby/Perl #1312. A `regex` literal spells both of
2034        // its delimiters `SLASH` — the kind id real division uses — so
2035        // `const a = /abc/g;` reported a `/` operator with no division
2036        // in the source, and n1/N1 counted the literal's punctuation as
2037        // arithmetic.
2038        //
2039        // The same fixture pins the second, independent half: `Regex`
2040        // was in neither arm, so the literal contributed no operand
2041        // either and reached the vocabulary from *neither* side.
2042        //
2043        // expected: operators `const`, `=`, `;`, `let` → n1 = 4;
2044        // `const` `=` `;` on line 1, `let` `=` `;` on line 2, `=` `;`
2045        // on line 3 → N1 = 8. Operands `a`, `/abc/g`, `b` → n2 = 3,
2046        // with `a` used three times and `b` twice → N2 = 6.
2047        //
2048        // Before the fix: n1 = 5 and N1 = 10 (the two fabricated `/`),
2049        // n2 = 2 and N2 = 5 (no operand for the literal).
2050        //
2051        // The four values are deliberately distinct so no transposition
2052        // of the unique-vs-total axes inside `assert_halstead_counts`
2053        // can pass (#1312).
2054        assert_js_family_counts("const a = /abc/g;\nlet b = a;\nb = a;\n", [4, 8, 3, 6]);
2055    }
2056
2057    #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2058    #[test]
2059    fn js_family_division_survives_the_regex_guard() {
2060        // Control for #1314: the guard is scoped to a `Regex` parent,
2061        // so real division must still count. This fixture holds both
2062        // sides at once — two divisions and one regex literal — so a
2063        // guard widened to every `SLASH` fails here rather than
2064        // silently passing the test above.
2065        //
2066        // expected: operators `const`, `=`, `;`, `/` → n1 = 4; two
2067        // `const`, two `=`, two `;` and two `/` → N1 = 8. Operands
2068        // `q`, `a`, `b`, `c`, `r`, `/x/` → n2 = N2 = 6.
2069        assert_js_family_counts("const q = a / b / c;\nconst r = /x/;\n", [4, 8, 6, 6]);
2070    }
2071
2072    #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2073    #[test]
2074    fn js_regex_delimiter_guard_is_parent_scoped_is_unobservable() {
2075        // Companion to the two above, and a statement of what they do
2076        // *not* cover. Ruby's guard has
2077        // `ruby_regex_guard_is_parent_scoped_not_ancestor_scoped`
2078        // because a division inside `#{…}` sits under a `Regex`
2079        // ancestor without being its child. No JS fixture can do that:
2080        // a regex literal admits no nested expression at all, its
2081        // `regex_pattern` and `regex_flags` children being leaves. So
2082        // the ancestor-scoped mutant of this guard — the one #1256's
2083        // post-mortem says survives every ordinary fixture — is
2084        // unobservable here. Measured, not assumed.
2085        //
2086        // Rather than write a fixture that would pass under both
2087        // spellings and read as coverage, pin the grammar property the
2088        // claim rests on: within a fixture that puts a division, a
2089        // template substitution and a regex in one file, every `SLASH`
2090        // reachable *below* a `Regex` is its immediate child. Should a
2091        // bump start nesting expressions inside a regex, this turns red
2092        // and the distinction becomes both observable and worth a real
2093        // test.
2094        //
2095        // Checked against all four grammars, not just JavaScript: the
2096        // guard is instantiated four times against four distinct enums,
2097        // and the property this test exists to watch could hold in one
2098        // and lapse in another.
2099        let source = b"const a = /abc/g;\nconst q = x / y;\nconst t = `p ${x / y} ${/zz/} q`;\n";
2100        assert_regex_slashes_are_immediate_children::<crate::langs::JavascriptCode>(
2101            source,
2102            Javascript::SLASH as u16,
2103            Javascript::Regex as u16,
2104            "javascript",
2105        );
2106        assert_regex_slashes_are_immediate_children::<crate::langs::MozjsCode>(
2107            source,
2108            Mozjs::SLASH as u16,
2109            Mozjs::Regex as u16,
2110            "mozjs",
2111        );
2112        assert_regex_slashes_are_immediate_children::<crate::langs::TypescriptCode>(
2113            source,
2114            Typescript::SLASH as u16,
2115            Typescript::Regex as u16,
2116            "typescript",
2117        );
2118        assert_regex_slashes_are_immediate_children::<crate::langs::TsxCode>(
2119            source,
2120            Tsx::SLASH as u16,
2121            Tsx::Regex as u16,
2122            "tsx",
2123        );
2124    }
2125
2126    /// Asserts every `slash` token below a `regex` node in `source` is
2127    /// that node's *immediate* child, for one grammar.
2128    ///
2129    /// Backs `js_regex_delimiter_guard_is_parent_scoped_is_unobservable`
2130    /// — see there for why the property is worth pinning.
2131    #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2132    fn assert_regex_slashes_are_immediate_children<L: crate::traits::LanguageInfo>(
2133        source: &[u8],
2134        slash: u16,
2135        regex: u16,
2136        label: &str,
2137    ) {
2138        let mut slashes_below_a_regex = 0;
2139        let visited = for_each_node_with_chain::<L>(source, |node: &Node<'_>, chain| {
2140            if node.kind_id() != slash {
2141                return;
2142            }
2143            let Some(depth) = chain.iter().position(|a| a.kind_id() == regex) else {
2144                return;
2145            };
2146            slashes_below_a_regex += 1;
2147            assert_eq!(
2148                depth,
2149                chain.len() - 1,
2150                "{label}: a slash at row {} has a regex ancestor that is not its parent, so \
2151                 the parent-vs-ancestor mutant is now observable and needs a real test",
2152                node.start_row()
2153            );
2154        });
2155        assert!(visited > 20, "{label}: fixture is too small to prove much");
2156        // Without this the assertion above is vacuous whenever the
2157        // fixture stops containing a regex at all — the failure mode a
2158        // filter that matches nothing always has.
2159        assert_eq!(
2160            slashes_below_a_regex, 4,
2161            "{label}: expected the two regex literals' four delimiters; the fixture no \
2162             longer exercises what this test claims"
2163        );
2164    }
2165
2166    // Issue #281: optional chaining (`?.`) was double-counted as a
2167    // Halstead operator in TypeScript and TSX because the grammar
2168    // exposes both an `optional_chain` named wrapper AND a child
2169    // `?.` token, and both were classified as `Operator`. The fix
2170    // counts only the bare `?.` token (`QMARKDOT`) in TS/TSX so each
2171    // textual `?.` contributes exactly once, matching JS / MozJS
2172    // (whose grammars expose only `OptionalChain` — the `?.` token
2173    // itself).
2174    //
2175    // The four assertions below all compare against the same totals:
2176    // for `function f(a) { return a?.b?.c; }` the operator stream is
2177    // `function`, `(`, `{`, `return`, `?.`, `?.`, `;` (7 total, 6
2178    // unique — `LPAREN`/`LBRACE` count once, closing tokens are not
2179    // in the operator set). Before the fix, TS/TSX reported 9/7
2180    // instead of 7/6.
2181    #[cfg(feature = "javascript")]
2182    #[test]
2183    fn javascript_optional_chain_not_double_counted_in_halstead_281() {
2184        check_metrics::<JavascriptParser>("function f(a) { return a?.b?.c; }", "foo.js", |m| {
2185            assert_eq!(m.halstead.unique_operators(), 6);
2186            assert_eq!(m.halstead.total_operators(), 7);
2187        });
2188    }
2189
2190    #[cfg(feature = "mozjs")]
2191    #[test]
2192    fn mozjs_optional_chain_not_double_counted_in_halstead_281() {
2193        check_metrics::<MozjsParser>("function f(a) { return a?.b?.c; }", "foo.js", |m| {
2194            assert_eq!(m.halstead.unique_operators(), 6);
2195            assert_eq!(m.halstead.total_operators(), 7);
2196        });
2197    }
2198
2199    #[cfg(feature = "typescript")]
2200    #[test]
2201    fn typescript_optional_chain_not_double_counted_in_halstead_281() {
2202        // The TS grammar wraps member-expression `?.` in an
2203        // `optional_chain` named node containing the bare `?.`
2204        // token; classifying both as `Operator` double-counted the
2205        // chain. We now count only the bare token, so TS matches JS.
2206        check_metrics::<TypescriptParser>("function f(a) { return a?.b?.c; }", "foo.ts", |m| {
2207            assert_eq!(m.halstead.unique_operators(), 6);
2208            assert_eq!(m.halstead.total_operators(), 7);
2209        });
2210    }
2211
2212    #[cfg(feature = "typescript")]
2213    #[test]
2214    fn tsx_optional_chain_not_double_counted_in_halstead_281() {
2215        check_metrics::<TsxParser>("function f(a) { return a?.b?.c; }", "foo.tsx", |m| {
2216            assert_eq!(m.halstead.unique_operators(), 6);
2217            assert_eq!(m.halstead.total_operators(), 7);
2218        });
2219    }
2220
2221    // Issue #299: parity guard for the JS-family `get_op_type` macro
2222    // on the optional-chain operator token (#281's prior regression
2223    // surface). All four languages must classify the bare `?.` token
2224    // identically — `OptionalChain` in JS/MozJS, `QMARKDOT` in
2225    // TS/TSX — and emit the same totals for
2226    // `function f(a) { return a?.b?.c; }`:
2227    //
2228    // * Operators: `function`, `(`, `{`, `return`, `?.`, `?.`, `;`
2229    //   (7 total, 6 unique).
2230    // * Operands: `f`, `a` (parameter), `a`, `b`, `c` — the identifier
2231    //   and property leaves only (5 total, 4 unique). Until #1263 the
2232    //   two wrapping member expressions (`a?.b`, `a?.b?.c`) were
2233    //   classified as `MemberExpression*` operands on top of the leaves
2234    //   they contain, making this 7 total / 6 unique.
2235    //
2236    // Verified by test-via-revert: dropping `OptionalChain` from
2237    // JS/MozJS, or `QMARKDOT` from TS/TSX, trips the test
2238    // (u_operators 6→5). This input does NOT exercise every operand
2239    // alias in the per-language `operand_extras` (`Identifier2`, the
2240    // JS/MozJS/TSX string-literal `String2`); drift in
2241    // those is out of scope for this regression guard and would need a
2242    // separate fixture. The `PredefinedType` operator path (`: void`
2243    // double-count) is now covered by `ts_void_return_type_single_operator_453`
2244    // below.
2245    #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2246    #[test]
2247    fn js_family_get_op_type_parity_optional_chain_member_299() {
2248        // Non-capturing closure (coerced to the `fn` pointer that
2249        // `check_metrics` accepts) avoids the
2250        // `clippy::needless_pass_by_value` warning that a free `fn`
2251        // taking `CodeMetrics` by value would trigger.
2252        #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2253        const SRC: &str = "function f(a) { return a?.b?.c; }";
2254        let check = |m: crate::CodeMetrics| {
2255            assert_eq!(m.halstead.unique_operators(), 6);
2256            assert_eq!(m.halstead.total_operators(), 7);
2257            assert_eq!(m.halstead.unique_operands(), 4);
2258            assert_eq!(m.halstead.total_operands(), 5);
2259        };
2260
2261        check_metrics::<JavascriptParser>(SRC, "foo.js", check);
2262        check_metrics::<MozjsParser>(SRC, "foo.js", check);
2263        check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2264        check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2265    }
2266
2267    // Issue #1263: a member access contributes its leaves and the `.`
2268    // operator, never the `member_expression` composite as well. The
2269    // classification does not stop the walk, so `a` and `b` were always
2270    // counted; listing the wrapper billed a third operand keyed on the
2271    // whole `a.b` text, which no other language here does.
2272    //
2273    // expected, for `var r = a.b;`:
2274    //
2275    // * Operators: `var`, `=`, `.`, `;` — 4 total, 4 unique.
2276    // * Operands: `r`, `a`, `b` — 3 total, 3 unique. Before the fix
2277    //   the `member_expression` wrapper added `a.b`, making both 4.
2278    //
2279    // All four JS-family languages are asserted because
2280    // `impl_js_family_get_op_type!` emits one shared operand arm: the
2281    // lockstep is the point of the macro, and a per-language extras
2282    // list is exactly where a future edit could break it.
2283    #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2284    #[test]
2285    fn js_family_member_access_counts_leaves_not_the_composite_1263() {
2286        #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2287        const SRC: &str = "var r = a.b;";
2288        let check = |m: crate::CodeMetrics| {
2289            assert_eq!(m.halstead.unique_operators(), 4);
2290            assert_eq!(m.halstead.total_operators(), 4);
2291            assert_eq!(m.halstead.unique_operands(), 3);
2292            assert_eq!(m.halstead.total_operands(), 3);
2293        };
2294
2295        check_metrics::<JavascriptParser>(SRC, "foo.js", check);
2296        check_metrics::<MozjsParser>(SRC, "foo.js", check);
2297        check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2298        check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2299    }
2300
2301    // Issue #1263, the grammar-dispatch section 6 half: dropping
2302    // `MemberExpression*` from the operand arm would have regressed
2303    // private-field access to *zero* operands for the field, because
2304    // `PrivatePropertyIdentifier` — the `#x` leaf — was in no operand
2305    // list and the composite `this.#x` had been its only count. Adding
2306    // the leaf also fixes the declaration site `#x = 1`, which no
2307    // wrapper covered and which therefore counted nothing at all.
2308    //
2309    // expected, for `class C { #x = 1; m() { return this.#x; } }`:
2310    //
2311    // * Operators: `class`, `{`×2 (class body, method body), `=`,
2312    //   `;`×2, `(`, `return`, `.` — 9 total, 7 unique. (`class` joined
2313    //   the JS-family operator arm in #1552.)
2314    // * Operands: `C`, `#x`, `1`, `m`, `this`, `#x` — 6 total, 5
2315    //   unique in all four grammars. Under TS/TSX the class *name* `C`
2316    //   is a `type_identifier`, which those getters did not classify
2317    //   until #1557, so they read 5/4 before it.
2318    #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2319    #[test]
2320    fn js_family_private_field_leaf_is_the_operand_1263() {
2321        #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2322        const SRC: &str = "class C { #x = 1; m() { return this.#x; } }";
2323        let check = |m: crate::CodeMetrics| {
2324            assert_eq!(m.halstead.unique_operators(), 7);
2325            assert_eq!(m.halstead.total_operators(), 9);
2326            assert_eq!(m.halstead.unique_operands(), 5);
2327            assert_eq!(m.halstead.total_operands(), 6);
2328        };
2329
2330        check_metrics::<JavascriptParser>(SRC, "foo.js", check);
2331        check_metrics::<MozjsParser>(SRC, "foo.js", check);
2332        check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2333        check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2334    }
2335
2336    // Issue #1263, the other section 6 half: `meta_property` is the one
2337    // composite the leaves-not-composites drop has to keep. `import.meta`
2338    // / `new.target` have no classified leaf — `meta` and `target` are
2339    // anonymous tokens in no arm — so with `MemberExpression*` gone the
2340    // meta-object contributed no operand at all while `this.env.x` still
2341    // yielded three.
2342    //
2343    // expected operands, for `var t = import.meta.url; function f() {
2344    // return new.target; }`: `t`, `import.meta`, `url`, `f`,
2345    // `new.target` — 5 total, 5 unique. Operators are deliberately not
2346    // asserted: the `import` / `new` keyword tokens inside the
2347    // meta-property keep their pre-existing operator classification,
2348    // which this fixture neither pins nor contests.
2349    #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2350    #[test]
2351    fn js_family_meta_property_is_one_operand_1263() {
2352        #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2353        const SRC: &str = "var t = import.meta.url; function f() { return new.target; }";
2354        let check = |m: crate::CodeMetrics| {
2355            assert_eq!(m.halstead.unique_operands(), 5);
2356            assert_eq!(m.halstead.total_operands(), 5);
2357        };
2358
2359        check_metrics::<JavascriptParser>(SRC, "foo.js", check);
2360        check_metrics::<MozjsParser>(SRC, "foo.js", check);
2361        check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2362        check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2363    }
2364
2365    // Issue #1263: TS/TSX `nested_identifier` (`namespace N.M`) is the
2366    // same container/leaf double-count as `member_expression`.
2367    //
2368    // expected, for `namespace N.M { }`:
2369    //
2370    // * Operators: `namespace`, `.`, `{` — 3 total, 3 unique.
2371    //   (`namespace` joined the TS operator extras in #1557.)
2372    // * Operands: `N`, `M` — 2 total, 2 unique. Before the fix the
2373    //   `nested_identifier` added `N.M`, making both 3.
2374    #[cfg(feature = "typescript")]
2375    #[test]
2376    fn ts_nested_identifier_counts_leaves_not_the_composite_1263() {
2377        const SRC: &str = "namespace N.M { }";
2378        let check = |m: crate::CodeMetrics| {
2379            assert_eq!(m.halstead.unique_operators(), 3);
2380            assert_eq!(m.halstead.total_operators(), 3);
2381            assert_eq!(m.halstead.unique_operands(), 2);
2382            assert_eq!(m.halstead.total_operands(), 2);
2383        };
2384
2385        check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2386        check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2387    }
2388
2389    // Issue #1261 (inverting the #313 pin): the `"string"` type-keyword
2390    // aliases the TS / TSX grammars expose must contribute NO operand.
2391    // #313 put them in `operand_extras` for parity with the then-wider
2392    // `Checker::is_string`, but the `predefined_type` wrapper already
2393    // counts as the text-keyed `"string"` operator, so one `: string`
2394    // token tallied as operator AND operand while `: number` counted
2395    // once. #1261 drops the aliases from both `operand_extras` and
2396    // `is_string`, so the keyword counts once, as the operator.
2397    //
2398    // For the input `let x: string = "y";`:
2399    //
2400    // * TypeScript emits `Typescript::String2` for the `string` type
2401    //   keyword (kind_id 135, in the type-keyword block of the enum).
2402    // * TSX emits `Tsx::String3` for the same role (kind_id 141).
2403    //
2404    // Verified by test-via-revert: restoring `String2` to TS's
2405    // `operand_extras` (or `String3` to TSX's) trips this test on
2406    // `u_operands` / `operands` for the affected language.
2407    #[cfg(feature = "typescript")]
2408    #[test]
2409    fn ts_family_type_keyword_counts_once_1261() {
2410        const SRC: &str = "let x: string = \"y\";";
2411        // Operators (n1 = 5, N1 = 5):
2412        //   `let`, `:`, `=`, `;`, plus `string` (PredefinedType wrapper,
2413        //   routed through `is_primitive` so it's keyed by its lexeme
2414        //   `"string"` in `primitive_operators`).
2415        // Operands (n2 = 2, N2 = 2):
2416        //   `x` and the `"y"` literal. Under #313 the type-keyword
2417        //   child of `predefined_type` added a third, phantom
2418        //   `"string"` operand (n2 = 3 / N2 = 3).
2419        let check = |m: crate::CodeMetrics| {
2420            assert_eq!(m.halstead.unique_operators(), 5);
2421            assert_eq!(m.halstead.total_operators(), 5);
2422            assert_eq!(m.halstead.unique_operands(), 2);
2423            assert_eq!(m.halstead.total_operands(), 2);
2424        };
2425
2426        check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2427        check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2428    }
2429
2430    // Issue #1261 regression, the issue's reproducer plus a literal
2431    // whose contents spell the keyword: `: string` and `: number` must
2432    // contribute symmetrically — one text-keyed operator each, zero
2433    // operands — while a string *literal* `"string"` stays an operand
2434    // (distinct from the keyword: TS kind `String`, TSX kind `String2`,
2435    // both quoted in the operand key).
2436    #[cfg(feature = "typescript")]
2437    #[test]
2438    fn ts_family_string_annotation_symmetric_with_number_1261() {
2439        const SRC: &str = "let x: string = \"a\";\nlet y: number = 1;\nlet s = \"string\";";
2440        // Operators (n1 = 6, N1 = 13):
2441        //   `let` ×3, `:` ×2, `=` ×3, `;` ×3, `string` ×1, `number` ×1.
2442        //   Pre-fix N1 was identical — the wrapper operator was always
2443        //   counted; the defect was the extra operand below.
2444        // Operands (n2 = 6, N2 = 6):
2445        //   `x`, `"a"`, `y`, `1`, `s`, `"string"` — one each. Pre-fix
2446        //   the `: string` keyword added a bare `string` operand
2447        //   (n2 = 7 / N2 = 7) that `: number` had no analogue of.
2448        let check = |m: crate::CodeMetrics| {
2449            assert_eq!(m.halstead.unique_operators(), 6);
2450            assert_eq!(m.halstead.total_operators(), 13);
2451            assert_eq!(m.halstead.unique_operands(), 6);
2452            assert_eq!(m.halstead.total_operands(), 6);
2453        };
2454
2455        check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2456        check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2457    }
2458
2459    /// Drift marker for #1261 (lesson 34 / grammar-dispatch §2): the
2460    /// anonymous `string` type-keyword token appears **only** as a
2461    /// `predefined_type` child.
2462    ///
2463    /// That is the whole argument for classifying the keyword without a
2464    /// parent guard — the wrapper is guaranteed to be there to carry the
2465    /// operator. The two tests above measure the consequence and would
2466    /// still pass if the grammar started emitting the keyword somewhere
2467    /// else, as long as their own two fixtures kept their counts; this one
2468    /// measures the premise, over every position #1261's dump probes
2469    /// covered: annotation, parameter and return type, union member,
2470    /// generic argument, and template-literal type. A string *literal*
2471    /// spelling `"string"` is a different kind and must not be confused
2472    /// for the keyword, so one is in the fixture too.
2473    #[cfg(feature = "typescript")]
2474    #[test]
2475    fn ts_family_type_keyword_only_appears_under_predefined_type_1261() {
2476        // Exercises each position the keyword can take. Valid in both
2477        // grammars: no angle-bracket cast, which TSX would read as JSX.
2478        const SRC: &str = "const a: string = \"string\";\n\
2479                           function f(x: string): string {\n\
2480                               return x;\n\
2481                           }\n\
2482                           type U = string | number;\n\
2483                           type A = Array<string>;\n\
2484                           type M = Map<string, number>;\n\
2485                           type T = `id-${string}`;\n";
2486        // Seven type positions: `a`, `x`, `f`'s return, the union member,
2487        // `Array`'s argument, `Map`'s first argument, and the template
2488        // placeholder. The `"string"` initialiser is a literal, not the
2489        // keyword, and must not be among them.
2490        const EXPECTED_OCCURRENCES: usize = 7;
2491
2492        fn keyword_occurrences<P: ParserTrait>(
2493            path: &str,
2494            keyword: u16,
2495            predefined_type: u16,
2496        ) -> usize {
2497            let parser = P::new(SRC.as_bytes().to_vec(), &PathBuf::from(path), None);
2498            parser
2499                .root()
2500                .preorder()
2501                .filter(|node| node.kind_id() == keyword)
2502                .inspect(|node| {
2503                    assert_eq!(
2504                        node.parent().map(|parent| parent.kind_id()),
2505                        Some(predefined_type),
2506                        "the `string` type keyword surfaced outside \
2507                         `predefined_type` in {path}; the operator is carried \
2508                         by the wrapper, so `get_op_type` needs a parent guard \
2509                         before that arm can be trusted (#1261)",
2510                    );
2511                })
2512                .count()
2513        }
2514
2515        // Kind ids re-read from the generated enums, not carried over: TS
2516        // `String2` = 135, TSX `String3` = 141 (TSX's `String2` = 261 is the
2517        // string-literal production and stays an operand).
2518        assert_eq!(
2519            keyword_occurrences::<TypescriptParser>(
2520                "foo.ts",
2521                Typescript::String2 as u16,
2522                Typescript::PredefinedType as u16,
2523            ),
2524            EXPECTED_OCCURRENCES,
2525            "TypeScript no longer emits the `string` type keyword in every \
2526             position #1261 probed",
2527        );
2528        assert_eq!(
2529            keyword_occurrences::<TsxParser>(
2530                "foo.tsx",
2531                Tsx::String3 as u16,
2532                Tsx::PredefinedType as u16,
2533            ),
2534            EXPECTED_OCCURRENCES,
2535            "TSX no longer emits the `string` type keyword in every position \
2536             #1261 probed",
2537        );
2538    }
2539
2540    // Issue #453: a `void` return type must contribute exactly one
2541    // Halstead operator. The TS / TSX grammars parse `: void` as a
2542    // `predefined_type` wrapper around an inner `void` token. `is_primitive`
2543    // routes the wrapper into the text-keyed `primitive_operators` map as
2544    // `"void"`, while the inner `Void` token is independently a standalone
2545    // expression operator (`void 0`). Pre-fix both classified as operators
2546    // and one source `void` counted as TWO distinct Halstead operators.
2547    // The fix suppresses the wrapper when its child is a `Void` token, so
2548    // only the inner token carries the operator — matching expression
2549    // `void 0` and keeping the kind_id-keyed count consistent.
2550    //
2551    // For `function f(): void { return; }`:
2552    //
2553    // * Operators (n1 = 7, N1 = 7): `function`, `()`, `{}`, `:`, `return`,
2554    //   `;`, and a single `void`. (The untyped form is n1 = 5; the `: void`
2555    //   annotation adds the `:` operator and one `void`, NOT two — the
2556    //   issue's "n1 = 6" target overlooked the annotation colon.)
2557    //
2558    // Verified by test-via-revert: removing the `predefined_void` guard
2559    // restores the pre-fix `u_operators` 7 -> 8 with a duplicate `"void"`
2560    // (one kind_id-keyed, one in `primitive_operators`). Both `metrics()`
2561    // and the `ops`-list dedup invariant (`ts_void_return_and_expression_*`
2562    // in `ops.rs`) are pinned per lesson 4.
2563    #[cfg(feature = "typescript")]
2564    #[test]
2565    fn ts_void_return_type_single_operator_453() {
2566        const SRC: &str = "function f(): void { return; }";
2567        let check = |m: crate::CodeMetrics| {
2568            assert_eq!(m.halstead.unique_operators(), 7);
2569            assert_eq!(m.halstead.total_operators(), 7);
2570        };
2571
2572        check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2573        check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2574    }
2575
2576    // Issue #453 over-suppression guard: expression `void 0` (a
2577    // `unary_expression`, NOT a `predefined_type` wrapper) must still
2578    // count `void` as exactly one operator. The fix keys only on a
2579    // `predefined_type` whose child is a `Void` token, so the bare
2580    // expression operator is untouched.
2581    //
2582    // For `const x = void 0;`:
2583    //
2584    // * Operators (n1 = 4, N1 = 4): `const`, `=`, `void`, `;`.
2585    // * Operands (n2 = 2, N2 = 2): `x`, `0`.
2586    #[cfg(feature = "typescript")]
2587    #[test]
2588    fn ts_void_expression_still_single_operator_453() {
2589        const SRC: &str = "const x = void 0;";
2590        let check = |m: crate::CodeMetrics| {
2591            assert_eq!(m.halstead.unique_operators(), 4);
2592            assert_eq!(m.halstead.total_operators(), 4);
2593            assert_eq!(m.halstead.unique_operands(), 2);
2594            assert_eq!(m.halstead.total_operands(), 2);
2595        };
2596
2597        check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2598        check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2599    }
2600
2601    #[cfg(feature = "python")]
2602    #[test]
2603    fn python_wrong_operators() {
2604        // Not valid Python — the parser recovers `()` as a tuple, the `[`
2605        // as a bare token and `{}` as a dict — but each opener is still a
2606        // delimiter the source spells, so since #1486 the three are
2607        // billed once each as `()`, `[]` and `{}`. The closers never are.
2608        check_metrics::<PythonParser>("()[]{}", "foo.py", |metric| {
2609            assert_eq!(metric.halstead.unique_operators(), 3);
2610            assert_eq!(metric.halstead.total_operators(), 3);
2611            insta::assert_json_snapshot!(
2612                metric.halstead,
2613                @r#"
2614            {
2615              "unique_operators": 3,
2616              "total_operators": 3,
2617              "unique_operands": 0,
2618              "total_operands": 0,
2619              "length": 3,
2620              "estimated_program_length": 4.754887502163468,
2621              "purity_ratio": 1.584962500721156,
2622              "vocabulary": 3,
2623              "volume": 4.754887502163468,
2624              "difficulty": 0.0,
2625              "level": 0.0,
2626              "effort": 0.0,
2627              "time": 0.0,
2628              "bugs": 0.0
2629            }
2630            "#
2631            );
2632        });
2633    }
2634
2635    #[cfg(feature = "python")]
2636    #[test]
2637    fn python_check_metrics() {
2638        check_metrics::<PythonParser>(
2639            "def f():
2640                 pass",
2641            "foo.py",
2642            |metric| {
2643                // expected operators: def, (), :, pass — one each (#1486
2644                // added the `()` and the block `:`).
2645                assert_eq!(metric.halstead.unique_operators(), 4);
2646                assert_eq!(metric.halstead.total_operators(), 4);
2647                insta::assert_json_snapshot!(
2648                    metric.halstead,
2649                    @r#"
2650                {
2651                  "unique_operators": 4,
2652                  "total_operators": 4,
2653                  "unique_operands": 1,
2654                  "total_operands": 1,
2655                  "length": 5,
2656                  "estimated_program_length": 8.0,
2657                  "purity_ratio": 1.6,
2658                  "vocabulary": 5,
2659                  "volume": 11.60964047443681,
2660                  "difficulty": 2.0,
2661                  "level": 0.5,
2662                  "effort": 23.21928094887362,
2663                  "time": 1.289960052715201,
2664                  "bugs": 0.002712967490108627
2665                }
2666                "#
2667                );
2668            },
2669        );
2670    }
2671
2672    #[cfg(feature = "java")]
2673    #[test]
2674    fn java_operators_and_operands() {
2675        check_metrics::<JavaParser>(
2676            "public class Main {
2677            public static void main(string args[]) {
2678                  int a, b, c, avg;
2679                  a = 5; b = 5; c = 5;
2680                  avg = (a + b + c) / 3;
2681                  MessageFormat.format(\"{0}\", avg);
2682                }
2683            }",
2684            "foo.java",
2685            |metric| {
2686                // Operators (n1=12): class {} void () [] , . ; int = + /
2687                // Operands (n2=13): Main main string args a b c avg 5 3 MessageFormat format "{0}"
2688                // (`string` is the parameter's type name, an operand since #1560)
2689                insta::assert_json_snapshot!(
2690                    metric.halstead,
2691                    @r#"
2692                {
2693                  "unique_operators": 12,
2694                  "total_operators": 27,
2695                  "unique_operands": 13,
2696                  "total_operands": 23,
2697                  "length": 50,
2698                  "estimated_program_length": 91.12526634448807,
2699                  "purity_ratio": 1.8225053268897613,
2700                  "vocabulary": 25,
2701                  "volume": 232.19280948873623,
2702                  "difficulty": 10.615384615384615,
2703                  "level": 0.09420289855072464,
2704                  "effort": 2464.8159776496614,
2705                  "time": 136.93422098053674,
2706                  "bugs": 0.06082308053869885
2707                }
2708                "#
2709                );
2710            },
2711        );
2712    }
2713
2714    #[cfg(feature = "java")]
2715    #[test]
2716    fn java_primitive_types_and_booleans() {
2717        check_metrics::<JavaParser>(
2718            "public class Prims {
2719                byte a = 1;
2720                short b = 2;
2721                int c = 3;
2722                long d = 4;
2723                char e = 'x';
2724                float f = 1.0f;
2725                double g = 2.0;
2726                boolean h = true;
2727                boolean i = false;
2728            }",
2729            "foo.java",
2730            |metric| {
2731                // Verifies all 8 Java primitive-type keywords (byte, short, int, long,
2732                // char, float, double, boolean) are counted as distinct operators, and
2733                // that true/false are counted as operands. The other four operators
2734                // are `class` (#1552), `{}`, `=` and `;`.
2735                insta::assert_json_snapshot!(
2736                    metric.halstead,
2737                    @r#"
2738                {
2739                  "unique_operators": 12,
2740                  "total_operators": 29,
2741                  "unique_operands": 19,
2742                  "total_operands": 19,
2743                  "length": 48,
2744                  "estimated_program_length": 123.730172764082,
2745                  "purity_ratio": 2.5777119325850415,
2746                  "vocabulary": 31,
2747                  "volume": 237.80142289857,
2748                  "difficulty": 6.0,
2749                  "level": 0.16666666666666666,
2750                  "effort": 1426.80853739142,
2751                  "time": 79.26714096619,
2752                  "bugs": 0.04224635182363915
2753                }
2754                "#
2755                );
2756            },
2757        );
2758    }
2759
2760    // #1380: `this` and `super` were swept into the operator arm under a
2761    // `// Operator: … keywords` heading — classification by lexical
2762    // class rather than by Halstead role. A member access is
2763    // `<receiver> <op> <field>`, so billing the receiver as an operator
2764    // scored `this.x` as a binary operator with one operand while `p.x`
2765    // is one operator and two operands. Both are operands now, matching
2766    // the eleven other languages that classify a self-reference.
2767    #[cfg(feature = "java")]
2768    #[test]
2769    fn java_self_and_super_references_are_operands() {
2770        let source = "class T {\n    int f() { return this.x + super.y; }\n}";
2771        // Operators (n1=8, N1=10): class (#1552), {} x2, int, (),
2772        // return, . x2, +, ;
2773        // Operands (n2=6, N2=6): T, f, this, x, super, y
2774        // Before the fix this read (10, 12, 4, 4) — `this` and `super`
2775        // billed into the operator side of both counts.
2776        assert_halstead_counts::<JavaParser>(
2777            source,
2778            "foo.java",
2779            [8, 10, 6, 6],
2780            "java self/super references",
2781        );
2782        assert_keywords_are_operands_only::<JavaParser>(source, "foo.java", &["this", "super"]);
2783    }
2784
2785    // The `super` of a wildcard type bound denotes no value, so it keeps
2786    // the operator classification `? extends T`'s `extends` already has
2787    // — the two keywords are siblings under the same `wildcard` node and
2788    // nothing but the parent separates them from a real self-reference
2789    // (#1380). The explicit receiver parameter in the same fixture is
2790    // the independent path through the operand arm (grammar-dispatch
2791    // section 11): only it can put `this` in the operand vocabulary
2792    // here, and only the wildcard can put `super` in the operator one.
2793    #[cfg(feature = "java")]
2794    #[test]
2795    fn java_wildcard_super_bound_stays_an_operator() {
2796        let source = "import java.util.List;\n\
2797                      class T {\n    \
2798                      void m(T T.this, List<? super String> a, List<? extends String> b) { }\n\
2799                      }";
2800        let ops = ops_of::<JavaParser>(source, "foo.java");
2801
2802        assert!(
2803            ops.operators.iter().any(|o| o == "super"),
2804            "`? super String` must keep `super` an operator; operators were {:?}",
2805            ops.operators
2806        );
2807        assert!(
2808            !ops.operands.iter().any(|o| o == "super"),
2809            "a wildcard bound must not bill `super` as an operand; operands were {:?}",
2810            ops.operands
2811        );
2812        assert!(
2813            ops.operators.iter().any(|o| o == "extends"),
2814            "the fixture must still contain the `? extends String` \
2815             bound this arm mirrors; operators were {:?}",
2816            ops.operators
2817        );
2818        assert_keywords_are_operands_only::<JavaParser>(source, "foo.java", &["this"]);
2819    }
2820
2821    /// One `this` and one `super` per container the pinned
2822    /// `tree-sitter-java` can put them in: a delegating constructor
2823    /// call, an explicit superclass constructor call, a field access, a
2824    /// method-invocation receiver, a method reference, a call argument,
2825    /// and the two qualified forms an inner class allows.
2826    #[cfg(feature = "java")]
2827    const JAVA_SELF_POSITIONS: &str = "class Pos extends P {
2828        int x;
2829        Pos() { this(1); }
2830        Pos(int v) { super(v); this.x = v; }
2831        int a() { return this.x; }
2832        int b() { return super.h(); }
2833        java.util.function.IntSupplier c() { return this::a; }
2834        void d() { g(this); }
2835        void g(Object o) { }
2836        class In {
2837            int e() { return Pos.this.x; }
2838            int f() { return Pos.super.h(); }
2839        }
2840    }";
2841
2842    #[cfg(feature = "java")]
2843    #[test]
2844    fn java_self_and_super_leaves_are_unaliased_and_unconditional() {
2845        assert_self_reference_leaves::<JavaCode>(
2846            JAVA_SELF_POSITIONS,
2847            &[
2848                ("this", Java::This as u16, 6),
2849                ("super", Java::Super as u16, 3),
2850            ],
2851            "java",
2852        );
2853    }
2854
2855    // Groovy's `Super` arm, pinned from all three sides. The pinned
2856    // grammar emits the `super` token only as a `wildcard` bound, where it
2857    // is an operator exactly as in Java above; every super-*reference* is
2858    // a plain `identifier`, and so an operand.
2859    //
2860    // Since #1419 the arm carries Java's `Wildcard` parent gate, so the
2861    // bound half below exercises the gate's operator branch — inverting
2862    // the gate fails it. The gate's operand branch is unreachable under
2863    // the pin (only error recovery on invalid Groovy, `List<? super>`,
2864    // detaches a `super` token from its `wildcard`), so it is
2865    // deliberately untested rather than pinned against an invalid
2866    // fixture. The node census is the drift marker for it: a grammar bump
2867    // that routes a reference to `Groovy::Super` fails here by name.
2868    #[cfg(feature = "groovy")]
2869    #[test]
2870    fn groovy_wildcard_super_bound_stays_an_operator() {
2871        let bounds =
2872            "class T {\n    void m(List<? super String> a, List<? extends String> b) { }\n}";
2873        let ops = ops_of::<GroovyParser>(bounds, "foo.groovy");
2874        assert!(
2875            ops.operators.iter().any(|o| o == "super"),
2876            "`? super String` must keep `super` an operator; operators were {:?}",
2877            ops.operators
2878        );
2879        assert!(
2880            !ops.operands.iter().any(|o| o == "super"),
2881            "a wildcard bound must not bill `super` as an operand; operands were {:?}",
2882            ops.operands
2883        );
2884        assert!(
2885            ops.operators.iter().any(|o| o == "extends"),
2886            "the fixture must still contain the `? extends String` bound this arm \
2887             mirrors; operators were {:?}",
2888            ops.operators
2889        );
2890
2891        let mixed = "class T extends B {\n    void m(List<? super String> a) { super.h() }\n}";
2892        let mut tokens = 0;
2893        for_each_node_with_chain::<GroovyCode>(mixed.as_bytes(), |node, chain| {
2894            if node.kind_id() == Groovy::Super as u16 {
2895                tokens += 1;
2896                assert_eq!(
2897                    chain.last().map(Node::kind_id),
2898                    Some(Groovy::Wildcard as u16),
2899                    "groovy: a `super` token outside a wildcard bound now reaches \
2900                     the gate's operand branch, which the pinned grammar made \
2901                     unreachable — re-check the arm against the new grammar (#1419)"
2902                );
2903            }
2904        });
2905        assert_eq!(
2906            tokens, 1,
2907            "the fixture's one wildcard bound must still spell `super`, and its \
2908             `super.h()` reference must not"
2909        );
2910
2911        // The reference direction, on valid input the gate must leave
2912        // alone: every super-reference form the grammar accepts stays an
2913        // operand, so splitting `Super` out of the operator alternation
2914        // cannot have moved one. `this` rides along for the same reason
2915        // Java's test carries it.
2916        let refs = "class A extends B {\n    A() { super(1) }\n    \
2917                    def g() { return super.h() }\n    \
2918                    def i() { return A.super.h() }\n    \
2919                    def j() { return super::h }\n    \
2920                    def k() { return super?.h() }\n    \
2921                    def l() { return this.x }\n}";
2922        assert_keywords_are_operands_only::<GroovyParser>(refs, "foo.groovy", &["super", "this"]);
2923    }
2924
2925    #[cfg(feature = "groovy")]
2926    #[test]
2927    fn groovy_operators_and_operands() {
2928        check_metrics::<GroovyParser>(
2929            "class Main {
2930                static void main(String[] args) {
2931                    int a, b, c, avg;
2932                    a = 5; b = 5; c = 5;
2933                    avg = (a + b + c) / 3;
2934                    println(avg);
2935                }
2936            }",
2937            "foo.groovy",
2938            |metric| {
2939                // Groovy mirror of `java_operators_and_operands`. The juxt
2940                // call `println avg` exercises `juxt_function_call` in
2941                // place of Java's `MessageFormat.format(...)`. amaanq's
2942                // grammar inherits Java's tokenisation, so n1/N1/n2/N2
2943                // shapes match Java up to those substitutions.
2944                // The dekobon grammar parses primitive type names
2945                // (`void`, `int`, `String`) as `type_identifier`
2946                // rather than as distinct keyword tokens, so they
2947                // count as operands here — the prior amaanq grammar
2948                // treated them as operators. Net shift: −2 unique
2949                // operators (`void`, `int`), +2 unique operands
2950                // (`void`, `int` were the only two type_identifiers
2951                // not already counted as operands, since `String`
2952                // was already an identifier in the prior grammar's
2953                // counting).
2954                // `class` is the ninth unique operator since #1552.
2955                assert_eq!(metric.halstead.unique_operators(), 9);
2956                assert_eq!(metric.halstead.unique_operands(), 13);
2957                // `bugs` is `effort.powf(2/3) / 3000`, and the platform libm's
2958                // `powf` differs in the last ulp: macOS rendered this value
2959                // as `…92342184` where Linux and Windows gave `…923421845`.
2960                // Round it so the snapshot pins the metric, not the libm.
2961                insta::assert_json_snapshot!(
2962                    metric.halstead,
2963                    { ".bugs" => insta::rounded_redaction(12) },
2964                    @r#"
2965                {
2966                  "unique_operators": 9,
2967                  "total_operators": 23,
2968                  "unique_operands": 13,
2969                  "total_operands": 23,
2970                  "length": 46,
2971                  "estimated_program_length": 76.63504134881501,
2972                  "purity_ratio": 1.665979159756848,
2973                  "vocabulary": 22,
2974                  "volume": 205.13385445731566,
2975                  "difficulty": 7.961538461538462,
2976                  "level": 0.12560386473429952,
2977                  "effort": 1633.1810720255517,
2978                  "time": 90.73228177919731,
2979                  "bugs": 0.046227630923
2980                }
2981                "#
2982                );
2983            },
2984        );
2985    }
2986
2987    #[cfg(feature = "groovy")]
2988    #[test]
2989    fn groovy_primitive_types_and_booleans() {
2990        check_metrics::<GroovyParser>(
2991            "class Prims {
2992                byte a = 1
2993                short b = 2
2994                int c = 3
2995                long d = 4
2996                char e = 'x'
2997                float f = 1.0f
2998                double g = 2.0
2999                boolean h = true
3000                boolean i = false
3001            }",
3002            "foo.groovy",
3003            |metric| {
3004                // The dekobon grammar consolidates the 8 primitive
3005                // type names (`byte`, `short`, `int`, `long`, `char`,
3006                // `float`, `double`, `boolean`) under `type_identifier`
3007                // — so they count as operands, not as distinct
3008                // operators. Likewise numeric literals collapse to one
3009                // `NumberLiteral` shape (no Hex/Octal/Binary/Decimal
3010                // split), and `'x'` parses as `StringLiteral` (Groovy
3011                // single-quoted strings) rather than as
3012                // `CharacterLiteral`. Operators remaining in this
3013                // fixture: `=`, the `class` keyword (#1552) and the
3014                // class-body braces (only `{` is in the operator set).
3015                // True/false collapse under one `BooleanLiteral`.
3016                assert_eq!(metric.halstead.unique_operators(), 3);
3017                assert_eq!(metric.halstead.unique_operands(), 27);
3018                insta::assert_json_snapshot!(
3019                    metric.halstead,
3020                    @r#"
3021                {
3022                  "unique_operators": 3,
3023                  "total_operators": 11,
3024                  "unique_operands": 27,
3025                  "total_operands": 28,
3026                  "length": 39,
3027                  "estimated_program_length": 133.1368500605771,
3028                  "purity_ratio": 3.413765386168644,
3029                  "vocabulary": 30,
3030                  "volume": 191.36873322873222,
3031                  "difficulty": 1.5555555555555556,
3032                  "level": 0.6428571428571428,
3033                  "effort": 297.6846961335835,
3034                  "time": 16.53803867408797,
3035                  "bugs": 0.014861058740943886
3036                }
3037                "#
3038                );
3039            },
3040        );
3041    }
3042
3043    // Issue #1263 swept Groovy alongside the JS family and C#: its
3044    // operand arm listed `QualifiedName` (a `package` / `import` path)
3045    // and `QualifiedType` on top of the identifier leaves the walker
3046    // already reached.
3047    //
3048    // Only the `QualifiedName` half was observable. The runtime emits
3049    // `qualified_type` as the *alias* `QualifiedType2` (kind_id 228),
3050    // which the arm never named — a lesson-2 miss that, by accident,
3051    // made that half already leaves-only and is why #1263's issue body
3052    // recorded Groovy as compliant. Both kinds are gone rather than
3053    // completed.
3054    //
3055    // expected, for `package com.example`: operators `.` (1/1);
3056    // operands `com`, `example` (2/2). Pre-fix the `qualified_name`
3057    // added `com.example`, making the operand counts 3/3.
3058    #[cfg(feature = "groovy")]
3059    #[test]
3060    fn groovy_qualified_name_counts_leaves_not_the_composite_1263() {
3061        check_metrics::<GroovyParser>("package com.example", "foo.groovy", |metric| {
3062            assert_eq!(metric.halstead.unique_operators(), 1);
3063            assert_eq!(metric.halstead.total_operators(), 1);
3064            assert_eq!(metric.halstead.unique_operands(), 2);
3065            assert_eq!(metric.halstead.total_operands(), 2);
3066        });
3067    }
3068
3069    // The type half of the same arm (#1352). #1263 dropped
3070    // `QualifiedType` alongside `QualifiedName`, but only the latter
3071    // was pinned: adding `QualifiedType2` back to the operand arm
3072    // failed none of the 3,523 tests then in the lib targets, so the
3073    // leaves-only reading of a qualified *type* was correct by accident
3074    // rather than by contract. The tempting "fix" for the alias miss
3075    // #1263 recorded is to complete the list with 228, which is exactly
3076    // the double count #1263 removed — this test is what stops that.
3077    //
3078    // The kind assertions are the grammar-dispatch section 1 / 2 drift
3079    // marker: if a grammar bump renumbers the alias, the operand
3080    // assertions below would keep passing while measuring a construct
3081    // this arm no longer describes.
3082    //
3083    // expected, for `java.util.List x = null`: operators `.` × 2 and
3084    // `=` → n1 = 2, N1 = 3; operands `java`, `util`, `List`, `x`,
3085    // `null` → n2 = 5, N2 = 5. Listing the wrapper would add the whole
3086    // span `java.util.List`, making the operand counts 6/6.
3087    #[cfg(feature = "groovy")]
3088    #[test]
3089    fn groovy_qualified_type_counts_leaves_not_the_composite_1352() {
3090        const SOURCE: &str = "java.util.List x = null";
3091
3092        let parser = GroovyParser::new(
3093            SOURCE.as_bytes().to_vec(),
3094            &PathBuf::from("foo.groovy"),
3095            None,
3096        );
3097        assert!(
3098            ast_has_kind_id(&parser, Groovy::QualifiedType2 as u16),
3099            "the dekobon grammar no longer emits `qualified_type` as the \
3100             alias `QualifiedType2`; re-derive the Groovy operand arm \
3101             before trusting the counts below",
3102        );
3103        assert!(
3104            !ast_has_kind_id(&parser, Groovy::QualifiedType as u16),
3105            "the unsuffixed `QualifiedType` is now reachable; it is a \
3106             second wrapper this arm must keep excluded",
3107        );
3108
3109        check_metrics::<GroovyParser>(SOURCE, "foo.groovy", |metric| {
3110            assert_eq!(metric.halstead.unique_operators(), 2);
3111            assert_eq!(metric.halstead.total_operators(), 3);
3112            assert_eq!(metric.halstead.unique_operands(), 5);
3113            assert_eq!(metric.halstead.total_operands(), 5);
3114        });
3115    }
3116
3117    #[cfg(feature = "groovy")]
3118    #[test]
3119    fn groovy_closure_operators_and_operands() {
3120        check_metrics::<GroovyParser>("def double = { x -> x * 2 }", "foo.groovy", |metric| {
3121            // Closure with arrow-style parameter list.
3122            // Distinct operators: def, =, {}, ->, * = 5.
3123            // Distinct operands: double, x, 2 = 3.
3124            assert_eq!(metric.halstead.unique_operators(), 5);
3125            assert_eq!(metric.halstead.unique_operands(), 3);
3126        });
3127    }
3128
3129    /// Regression for issue #247: every Groovy-specific operator the
3130    /// prior amaanq grammar dropped to ERROR or mis-shaped as a Java
3131    /// node now parses as a distinct lexer token in the dekobon
3132    /// grammar, so Halstead counts each one. The fixture below
3133    /// exercises Elvis `?:`, safe-nav `?.`, safe-chain `??.`,
3134    /// spread-dot `*.`, method-pointer `.&`, direct-field `.@`,
3135    /// identity `===` / `!==`, spaceship `<=>`, regex `=~` / `==~`,
3136    /// exclusive ranges `..<` / `<..` / `<..<`, `as` coercion, and
3137    /// `?[` safe index — every distinct operator kind must appear in
3138    /// `u_operators` (the count grows by exactly the number of new
3139    /// distinct operator tokens introduced).
3140    #[cfg(feature = "groovy")]
3141    #[test]
3142    fn groovy_dekobon_operator_coverage_247() {
3143        check_metrics::<GroovyParser>(
3144            "def f(a, b, list, s) {
3145                def x = a ?: b
3146                def y = a?.field
3147                def z = a??.field
3148                def items = list*.size()
3149                def ptr = a.&size
3150                def fld = a.@field
3151                def id1 = a === b
3152                def id2 = a !== b
3153                def ship = a <=> b
3154                def find = s =~ /pat/
3155                def match = s ==~ /^pat\\$/
3156                def r1 = 0..<10
3157                def r2 = 0<..10
3158                def r3 = 0<..<10
3159                def cast = a as String
3160                def safe = list?[0]
3161                return x
3162            }",
3163            "foo.groovy",
3164            |metric| {
3165                // Exact pin: with the dekobon Groovy grammar this
3166                // fixture exercises 16 Groovy-specific tokens (`?:`,
3167                // `?.`, `??.`, `*.`, `.&`, `.@`, `===`, `!==`, `<=>`,
3168                // `=~`, `==~`, `..<`, `<..`, `<..<`, `as`, `?[`) plus
3169                // 6 ambient Java-shaped operators the fixture also
3170                // uses (`def`, `=`, `,`, `{}`, `()`, `return`), for a
3171                // total of 22 distinct operator kinds. A regression
3172                // that drops any one of the 16 #247 operators would
3173                // push the count below 22 and fail this assertion. The
3174                // complementary AST walk below pins each #247
3175                // operator's identity individually so a grammar change
3176                // that adds an unrelated operator (lifting
3177                // `u_operators` to 23) still flags the loss of a #247
3178                // operator at the per-token level.
3179                //
3180                // Was 23 until #1314. The extra entry was a `/` — the
3181                // fixture's two slashy literals (`/pat/`, `/^pat\$/`)
3182                // each spelled their closing delimiter with the
3183                // division kind, and the arm now guards them. The
3184                // enumeration above was wrong in two ways at that
3185                // count: it listed an ambient `[`, which this fixture
3186                // never emits (`list?[0]` is the single `?[` token),
3187                // and omitted the fabricated `/` that made up the
3188                // difference. Both are corrected here.
3189                assert_eq!(
3190                    metric.halstead.unique_operators(),
3191                    22,
3192                    "u_operators changed; check whether a #247 operator was dropped or an unrelated operator added (and update the comment / token list above accordingly)",
3193                );
3194            },
3195        );
3196    }
3197
3198    #[cfg(feature = "groovy")]
3199    #[test]
3200    fn groovy_gstring_no_double_count() {
3201        // Issue #454: before the fix Groovy had no interpolation guard
3202        // at all — `StringLiteral` was classified as a plain operand, so
3203        // a GString counted the wrapping literal AND descended into its
3204        // interpolated expression, double-counting the inner identifier
3205        // in N2. The fix routes `StringLiteral` through
3206        // `string_operand_type` with both GString interpolation child
3207        // kinds (`gstring_brace_interpolation` / `gstring_dollar_-
3208        // interpolation`), so the wrapper is Unknown and only the inner
3209        // expression contributes.
3210        //
3211        // `def greet(name) {\n  return "Hi ${name}"\n}\n`
3212        //   operands by token text: `greet` × 1, `name` × 2 (param +
3213        //   inside `${name}`). The wrapping `"Hi ${name}"` is suppressed
3214        //   → u_operands = 2 (`greet`, `name`), N2 = 3. Without the fix
3215        //   the wrapping literal would also count → u_operands = 3,
3216        //   N2 = 4.
3217        let src = "def greet(name) {\n  return \"Hi ${name}\"\n}\n";
3218        check_metrics::<GroovyParser>(src, "foo.groovy", |metric| {
3219            assert_eq!(metric.halstead.unique_operands(), 2);
3220            assert_eq!(metric.halstead.total_operands(), 3);
3221        });
3222        assert_ops_operands::<GroovyParser>(src, "foo.groovy", 2, vec!["greet", "name"]);
3223    }
3224
3225    #[cfg(feature = "groovy")]
3226    #[test]
3227    fn groovy_gstring_dollar_form_no_double_count() {
3228        // Issue #454: the short `$name` GString form emits a distinct
3229        // `gstring_dollar_interpolation` child whose inner `identifier`
3230        // text is `$name` (the grammar's identifier node spans the
3231        // leading `$`). The wrapper is suppressed; the inner `$name`
3232        // operand is distinct from the bare `name` param.
3233        //
3234        // `def greet(name) {\n  return "Hi $name"\n}\n`
3235        //   operands: `greet`, `name` (param), `$name` (interp) →
3236        //   u_operands = 3, N2 = 3. Without the fix the wrapping
3237        //   `"Hi $name"` would also count → u_operands = 4, N2 = 4.
3238        let src = "def greet(name) {\n  return \"Hi $name\"\n}\n";
3239        check_metrics::<GroovyParser>(src, "foo.groovy", |metric| {
3240            assert_eq!(metric.halstead.unique_operands(), 3);
3241            assert_eq!(metric.halstead.total_operands(), 3);
3242        });
3243        assert_ops_operands::<GroovyParser>(src, "foo.groovy", 3, vec!["greet", "name", "$name"]);
3244    }
3245
3246    // `Groovy::Identifier2` (141) is the grammar's external token
3247    // `_gstring_dollar_path` — the `$a.b` path of a dollar interpolation —
3248    // whose public symbol is `identifier`, so the parser reports it as
3249    // the unsuffixed `Identifier` and never as 141 (#1475). That is why
3250    // the operand arm lists `Identifier` but not `Identifier2`; if a
3251    // grammar bump starts emitting 141, the dotted path drops out of N2
3252    // with no error.
3253    //
3254    // expected: operands `greet`, `name` (param), `$name.b` (the whole
3255    // path, one node) → n2 = 3, N2 = 3.
3256    #[cfg(feature = "groovy")]
3257    #[test]
3258    fn groovy_gstring_dollar_path_is_the_public_identifier() {
3259        let src = "def greet(name) {\n  return \"Hi $name.b\"\n}\n";
3260        let parser = GroovyParser::new(src.as_bytes().to_vec(), &PathBuf::from("foo.groovy"), None);
3261        assert!(
3262            ast_has_kind_id(&parser, Groovy::GstringDollarInterpolation as u16),
3263            "the fixture no longer spells a dollar interpolation",
3264        );
3265        assert!(
3266            !ast_has_kind_id(&parser, Groovy::Identifier2 as u16),
3267            "the hidden `_gstring_dollar_path` token is now emitted as \
3268             `Identifier2`; add it to the Groovy operand arm",
3269        );
3270        check_metrics::<GroovyParser>(src, "foo.groovy", |metric| {
3271            assert_eq!(metric.halstead.unique_operands(), 3);
3272            assert_eq!(metric.halstead.total_operands(), 3);
3273        });
3274        assert_ops_operands::<GroovyParser>(src, "foo.groovy", 3, vec!["greet", "name", "$name.b"]);
3275    }
3276
3277    #[cfg(feature = "groovy")]
3278    #[test]
3279    fn groovy_plain_string_still_operand() {
3280        // Counterpart to `groovy_gstring_no_double_count`: a plain
3281        // non-interpolated literal has neither GString interpolation
3282        // child and must still contribute exactly one operand.
3283        //
3284        // `def f() {\n  return "plain"\n}\n`
3285        //   operands: `f`, `"plain"` → u_operands = 2, N2 = 2.
3286        let src = "def f() {\n  return \"plain\"\n}\n";
3287        check_metrics::<GroovyParser>(src, "foo.groovy", |metric| {
3288            assert_eq!(metric.halstead.unique_operands(), 2);
3289            assert_eq!(metric.halstead.total_operands(), 2);
3290        });
3291        assert_ops_operands::<GroovyParser>(src, "foo.groovy", 2, vec!["f", "\"plain\""]);
3292    }
3293
3294    #[cfg(feature = "groovy")]
3295    #[test]
3296    fn groovy_slashy_string_delimiter_is_not_an_operator() {
3297        // Regression: issue #1314, the Groovy sibling of Elixir #1256
3298        // and Ruby/Perl #1312. A slashy string is a `StringLiteral`
3299        // whose closing delimiter is a `SLASH` — the kind id real
3300        // division uses — so `def b = /xyz/` reported a `/` operator
3301        // with no division in the source. Only the closer is a child
3302        // (the grammar folds the opening `/` into the literal's span),
3303        // so this fabricated one `/` per literal rather than Ruby's two.
3304        //
3305        // expected: operators `def` × 3, `=` × 3 → n1 = 2, N1 = 6.
3306        // Operands `b`, `/xyz/` × 2, `c`, `s` → n2 = 4, N2 = 6. Before
3307        // the guard the two closers added `/` → n1 = 3, N1 = 8.
3308        check_metrics::<GroovyParser>(
3309            "def b = /xyz/\ndef c = /xyz/\ndef s = b\n",
3310            "foo.groovy",
3311            |metric| {
3312                assert_eq!(metric.halstead.unique_operators(), 2);
3313                assert_eq!(metric.halstead.total_operators(), 6);
3314                assert_eq!(metric.halstead.unique_operands(), 4);
3315                assert_eq!(metric.halstead.total_operands(), 6);
3316            },
3317        );
3318    }
3319
3320    #[cfg(feature = "groovy")]
3321    #[test]
3322    fn groovy_division_survives_the_slashy_guard() {
3323        // Control for #1314: the guard is scoped to a `StringLiteral`
3324        // parent, so real division must still count. Both sides are in
3325        // one fixture — two divisions and one slashy literal — so a
3326        // guard widened to every `SLASH` fails here rather than
3327        // silently passing the test above.
3328        //
3329        // expected: operators `def` × 2, `=` × 2, `/` × 2 → n1 = 3,
3330        // N1 = 6. Operands `q`, `a`, `b`, `c`, `r`, `/x/` → n2 = N2 = 6.
3331        check_metrics::<GroovyParser>("def q = a / b / c\ndef r = /x/\n", "foo.groovy", |metric| {
3332            assert_eq!(metric.halstead.unique_operators(), 3);
3333            assert_eq!(metric.halstead.total_operators(), 6);
3334            assert_eq!(metric.halstead.unique_operands(), 6);
3335            assert_eq!(metric.halstead.total_operands(), 6);
3336        });
3337    }
3338
3339    #[cfg(feature = "groovy")]
3340    #[test]
3341    fn groovy_slashy_guard_is_parent_scoped_not_ancestor_scoped() {
3342        // The input that separates the parent-scoped guard from the
3343        // ancestor-scanning mutant of it — the mutant #1256's
3344        // post-mortem says survives every ordinary fixture. Groovy is
3345        // one of only two languages in #1314 where such an input
3346        // exists at all: a slashy string may carry a GString
3347        // interpolation, so `/x${a / b}y/` puts a real division under a
3348        // `StringLiteral` *ancestor* while its parent is the
3349        // `binary_expression`. An ancestor scan swallows it; the parent
3350        // check leaves it alone. (The JS, C++ and Tcl/iRules guards
3351        // have no such input — see
3352        // `js_regex_delimiter_guard_is_parent_scoped_is_unobservable`.)
3353        //
3354        // expected: operators `def` × 2, `=` × 2, `/` (the
3355        // interpolated division) → n1 = 3, N1 = 5. The wrapping literal
3356        // is not an operand — it carries an interpolation, so
3357        // `string_operand_type` yields `Unknown` and the inner
3358        // expression's operands carry the count (#454) — leaving `r`,
3359        // `a`, `b`, `s` → n2 = 4, with `a` twice → N2 = 5. Under the
3360        // ancestor-scoped mutant the division vanishes: n1 = 2, N1 = 4.
3361        check_metrics::<GroovyParser>(
3362            "def r = /x${a / b}y/\ndef s = a\n",
3363            "foo.groovy",
3364            |metric| {
3365                assert_eq!(metric.halstead.unique_operators(), 3);
3366                assert_eq!(metric.halstead.total_operators(), 5);
3367                assert_eq!(metric.halstead.unique_operands(), 4);
3368                assert_eq!(metric.halstead.total_operands(), 5);
3369            },
3370        );
3371    }
3372
3373    #[cfg(feature = "groovy")]
3374    #[test]
3375    fn groovy_every_string_spelling_scores_alike() {
3376        // Companion to the three above (#1314). Groovy has five ways to
3377        // write an inert one-character string, and the choice is
3378        // spelling rather than semantics, so all five must score
3379        // identically. Before the guard the two slashy forms reported
3380        // an extra `/` operator that the other three did not — the
3381        // author's delimiter choice moved n1/N1.
3382        //
3383        // The dollar-slashy and quoted forms are no-change controls:
3384        // `$/…/$` closes with `/$` (kind 144) and `"…"` with `"` (134),
3385        // neither of which the operator arm classifies. `'x'` is a
3386        // childless leaf. The escaped-slash row is the one that would
3387        // regress if the guard were ever narrowed to a literal whose
3388        // *only* child is the closer.
3389        //
3390        // expected per spelling: operators `def` × 3, `=` × 3 → n1 = 2,
3391        // N1 = 6; operands `a`, the literal, `b`, `c` → n2 = 4, with
3392        // `a` used three times → N2 = 6.
3393        for literal in ["/x/", "$/x/$", "'x'", "\"x\"", r"/esc\/aped/"] {
3394            assert_halstead_counts::<GroovyParser>(
3395                &format!("def a = {literal}\ndef b = a\ndef c = a\n"),
3396                "foo.groovy",
3397                [2, 6, 4, 6],
3398                &format!("literal {literal}"),
3399            );
3400        }
3401    }
3402
3403    #[cfg(feature = "csharp")]
3404    #[test]
3405    fn csharp_operators_and_operands() {
3406        // After issue #286, `void`, `string`, and `int` count as three
3407        // distinct Halstead operators rather than collapsing into one
3408        // `PredefinedType` kind_id entry, lifting u_operators from 13
3409        // to 15. Total operators (N1) is unchanged because the same
3410        // nodes are still counted, just keyed by lexeme.
3411        check_metrics::<CsharpParser>(
3412            "public class Main {
3413                public static void Run(string[] args) {
3414                    int a, b, c, avg;
3415                    a = 5; b = 5; c = 5;
3416                    avg = (a + b + c) / 3;
3417                    System.Console.WriteLine(\"{0}\", avg);
3418                }
3419            }",
3420            "foo.cs",
3421            |metric| {
3422                assert_eq!(metric.halstead.unique_operators(), 15);
3423                assert_eq!(metric.halstead.total_operators(), 32);
3424                assert_eq!(metric.halstead.unique_operands(), 13);
3425                assert_eq!(metric.halstead.total_operands(), 23);
3426                // Pin every Halstead field; values are whatever the
3427                // classifier produces and become the regression spec.
3428                insta::assert_json_snapshot!(metric.halstead);
3429            },
3430        );
3431    }
3432
3433    // Issue #1263: C#'s three name *containers* — `qualified_name`
3434    // (`System.Text`), `generic_name` (`List<int>`) and
3435    // `alias_qualified_name` (`global::Foo`) — were operands alongside
3436    // every leaf the walker already reached, so one occurrence of each
3437    // billed twice. `member_access_expression` never was, which is why
3438    // `csharp_operators_and_operands`' `System.Console.WriteLine` is
3439    // unaffected by this change: the bug lived in the *name* grammar,
3440    // not in member access.
3441    //
3442    // Three fixtures rather than one, so a regression names which
3443    // container came back. Each is hand-tallied; the removed composite
3444    // is called out per case.
3445    #[cfg(feature = "csharp")]
3446    #[test]
3447    fn csharp_name_containers_count_leaves_not_the_composite_1263() {
3448        // expected: operators `using`, `.`, `;` (3/3); operands
3449        // `System`, `Text` (2/2). Pre-fix the `qualified_name` added
3450        // `System.Text`, making the operand counts 3/3.
3451        check_metrics::<CsharpParser>("using System.Text;", "foo.cs", |metric| {
3452            assert_eq!(metric.halstead.unique_operators(), 3);
3453            assert_eq!(metric.halstead.total_operators(), 3);
3454            assert_eq!(metric.halstead.unique_operands(), 2);
3455            assert_eq!(metric.halstead.total_operands(), 2);
3456        });
3457
3458        // expected: operators `class`, `{`×2, `void`, `(`, `;`, `<>`,
3459        // `int` — 8 total, 7 unique (`int` is the text-keyed primitive
3460        // operator, per #286; `<>` is the type-argument pair, #1559). Operands `C`, `M`, `List`, `l`
3461        // — 4/4. Pre-fix the `generic_name` added `List<int>`, making
3462        // them 5/5.
3463        check_metrics::<CsharpParser>(
3464            "class C { void M() { List<int> l; } }",
3465            "foo.cs",
3466            |metric| {
3467                assert_eq!(metric.halstead.unique_operators(), 7);
3468                assert_eq!(metric.halstead.total_operators(), 8);
3469                assert_eq!(metric.halstead.unique_operands(), 4);
3470                assert_eq!(metric.halstead.total_operands(), 4);
3471            },
3472        );
3473
3474        // expected: operators `class`, `{`×2, `void`, `(`, `;`, `=`,
3475        // `::`, `.` — 9 total, 8 unique. `var` has no operator arm.
3476        // Operands `C`, `M`, `x`, `global`, `Foo`, `Bar` — 6/6. Pre-fix
3477        // the `alias_qualified_name` added `global::Foo`, making them
3478        // 7/7. The `::` staying an operator is what makes the leaf-only
3479        // tally lossless here, so it is asserted by the operator count
3480        // rather than assumed.
3481        check_metrics::<CsharpParser>(
3482            "class C { void M() { var x = global::Foo.Bar; } }",
3483            "foo.cs",
3484            |metric| {
3485                assert_eq!(metric.halstead.unique_operators(), 8);
3486                assert_eq!(metric.halstead.total_operators(), 9);
3487                assert_eq!(metric.halstead.unique_operands(), 6);
3488                assert_eq!(metric.halstead.total_operands(), 6);
3489            },
3490        );
3491    }
3492
3493    #[cfg(feature = "csharp")]
3494    #[test]
3495    fn csharp_primitive_types_and_booleans() {
3496        // After issue #286: each of `byte`, `short`, `int`, `long`,
3497        // `char`, `float`, `double`, `bool`, `object` is now a distinct
3498        // Halstead operator (9 primitives) rather than collapsing into
3499        // one `PredefinedType` kind_id entry. u_operators rises from 6
3500        // to 14 (5 non-primitive operators + 9 distinct primitives);
3501        // total operators (N1) is unchanged because the same nodes are
3502        // still counted, just keyed by lexeme.
3503        //
3504        // N2 dropped 23 → 21 with issue #1253: `true` and `false` each
3505        // reached the walker twice — once as `boolean_literal`, once as
3506        // the keyword leaf under it — so each added one spurious
3507        // occurrence. n2 is unchanged at 21 because operands are keyed
3508        // by source text, so the duplicate collapsed into the existing
3509        // vocabulary entry; that is exactly why the inflation was
3510        // invisible in n2. Every operand here is distinct, so
3511        // N2 == n2 == 21 after the fix.
3512        check_metrics::<CsharpParser>(
3513            "public class Prims {
3514                byte a = 1;
3515                short b = 2;
3516                int c = 3;
3517                long d = 4;
3518                char e = 'x';
3519                float f = 1.0f;
3520                double g = 2.0;
3521                bool h = true;
3522                bool i = false;
3523                object j = null;
3524            }",
3525            "foo.cs",
3526            |metric| {
3527                assert_eq!(metric.halstead.unique_operators(), 14);
3528                assert_eq!(metric.halstead.total_operators(), 33);
3529                assert_eq!(metric.halstead.unique_operands(), 21);
3530                assert_eq!(metric.halstead.total_operands(), 21);
3531                insta::assert_json_snapshot!(metric.halstead);
3532            },
3533        );
3534    }
3535
3536    #[cfg(feature = "csharp")]
3537    #[test]
3538    fn csharp_boolean_literal_counts_once() {
3539        // Regression: issue #1253. `boolean_literal: choice('true',
3540        // 'false')` wraps the keyword leaf, and both kinds sat in the
3541        // operand arm, so every `true` / `false` occurrence added +1 to
3542        // N2. Operands are keyed by source text, so the duplicate
3543        // collapsed into the same vocabulary entry and n2 stayed
3544        // correct — which is why nothing caught it.
3545        //
3546        // Source repeats `true` so N2 exceeds n2 and the assertions can
3547        // tell "counted once per occurrence" from "deduplicated into
3548        // the vocabulary".
3549        //
3550        // Operands by text key: `A`, `M`, `a`, `b`, `c`, `d`, `true` × 2,
3551        // `false`, `null` ⇒ n2 = 9, N2 = 10. Before the fix the keyword
3552        // leaves added one occurrence per boolean ⇒ N2 = 13.
3553        check_metrics::<CsharpParser>(
3554            "class A {\n    void M() {\n        bool a = true;\n        bool b = false;\n        bool c = true;\n        object d = null;\n    }\n}\n",
3555            "foo.cs",
3556            |metric| {
3557                assert_eq!(metric.halstead.unique_operands(), 9);
3558                assert_eq!(metric.halstead.total_operands(), 10);
3559            },
3560        );
3561    }
3562
3563    #[cfg(feature = "csharp")]
3564    #[test]
3565    fn csharp_operator_true_names_an_operator_not_a_value() {
3566        // C#'s overloadable-operator list emits a bare `true` / `false`
3567        // token with no `boolean_literal` wrapper — `operator_declaration`
3568        // is the grammar's only such position. There the keyword names
3569        // the operator being declared, as `+` does in `operator +`, so it
3570        // is an operator (#1296); the companion test above pins the
3571        // wrapped literal, which bills only its wrapper (#1253).
3572        //
3573        // Each declaration names one boolean and returns the other, so
3574        // the fixture carries each keyword once as a name and once as a
3575        // value.
3576        //
3577        // Operators (n1 = 11, N1 = 18): class 1, {} 1, public 2,
3578        // static 2, bool 2, operator 2, true 1, false 1, () 2, => 2,
3579        // ; 2. Operands (n2 = 4, N2 = 7): A × 3 (class name, two
3580        // parameter types), a × 2, and the literals false and true once
3581        // each. Before #1296 the two names were operands instead:
3582        // (9, 16, 4, 9). A blanket exclusion of the leaf would give
3583        // (9, 16, 4, 7), and no guard at all (11, 18, 4, 9).
3584        let source = "class A {\n    public static bool operator true(A a) => false;\n    public static bool operator false(A a) => true;\n}\n";
3585        assert_halstead_counts::<CsharpParser>(source, "foo.cs", [11, 18, 4, 7], "operator true");
3586
3587        // `true` and `false` are each an operator *and* an operand here,
3588        // once apiece: the name and the literal land in different halves.
3589        let ops = ops_of::<CsharpParser>(source, "foo.cs");
3590        for keyword in ["true", "false"] {
3591            for (half, list) in [("operators", &ops.operators), ("operands", &ops.operands)] {
3592                assert_eq!(
3593                    list.iter().filter(|o| *o == keyword).count(),
3594                    1,
3595                    "`{keyword}` must appear once among the {half}; they were {list:?}"
3596                );
3597            }
3598        }
3599
3600        // The fallback arm: outside both positions — here a bare `true`
3601        // under error recovery, the only other parent the grammar
3602        // produces — the keyword keeps its role as a value.
3603        assert_halstead_counts::<CsharpParser>("true\n", "foo.cs", [0, 0, 1, 1], "bare true");
3604    }
3605
3606    #[cfg(feature = "csharp")]
3607    #[test]
3608    fn csharp_predefined_types_keyed_by_lexeme() {
3609        // Regression: issue #286. The C# grammar emits one `PredefinedType`
3610        // kind_id for every keyword type (`int`, `string`, `bool`, …).
3611        // Without keying by source text the entire family collapses into
3612        // a single Halstead operator (n1 += 1) instead of one per distinct
3613        // keyword. This test pins the post-fix behaviour using four
3614        // distinct primitives — `int`, `string`, `bool`, `object` —
3615        // appearing as parameter types so no other operators interact
3616        // with the count.
3617        //
3618        // expected: operators are `class`, `void`, `M`, `{}`, `()`, `,`
3619        // (×3 between 4 params), plus the four distinct predefined types
3620        // → u_operators = 5 + 4 = 9. Without the fix the four primitives
3621        // collapse to one entry, giving u_operators = 6.
3622        check_metrics::<CsharpParser>(
3623            "class C { void M(int a, string b, bool c, object d) {} }",
3624            "foo.cs",
3625            |metric| {
3626                // The headline assertion: four distinct primitive
3627                // keywords contribute four distinct operators, not one.
3628                assert_eq!(metric.halstead.unique_operators(), 9);
3629            },
3630        );
3631    }
3632
3633    #[cfg(feature = "csharp")]
3634    #[test]
3635    fn csharp_interpolated_string_no_double_count() {
3636        // Regression: issue #183. A C# `$"Hi {name}!"` used to be
3637        // classified as a Halstead operand (the wrapping
3638        // `InterpolatedStringExpression`) AND have its inner
3639        // `Interpolation`'s identifier classified as an operand too.
3640        // The fix routes `InterpolatedStringExpression` through a
3641        // conditional: when it has an `Interpolation` child, the inner
3642        // identifier already carries the operand contribution and the
3643        // wrapper is treated as `Unknown`; when it does not (static
3644        // `$"hello"`), the wrapper still counts as one operand.
3645        //
3646        // expected: operand contributions for
3647        //   `class C { void M(string name) { string s = $"Hi {name}!"; } }`
3648        // — `C` (class), `M` (method), `name` (param), `s` (local),
3649        // and the inner `name` (inside `{...}`). With the fix,
3650        // u_operands = 4 (C, M, name, s); N2 = 5 (`name` twice).
3651        // Without the fix, the wrapping `$"Hi {name}!"` would also
3652        // count → u_operands = 5, N2 = 6.
3653        check_metrics::<CsharpParser>(
3654            "class C { void M(string name) { string s = $\"Hi {name}!\"; } }",
3655            "foo.cs",
3656            |metric| {
3657                assert_eq!(metric.halstead.unique_operands(), 4);
3658                assert_eq!(metric.halstead.total_operands(), 5);
3659            },
3660        );
3661    }
3662
3663    #[cfg(feature = "csharp")]
3664    #[test]
3665    fn csharp_static_interpolated_string_is_operand() {
3666        // Regression: issue #183. A `$"..."` with no `{...}` is
3667        // semantically identical to `"..."` and must still contribute
3668        // exactly one operand — the conditional `is_child(Interpolation)`
3669        // check distinguishes it from a true interpolation. expected:
3670        // operands are `C`, `M`, `s`, `$"hello"` → u_operands = 4, N2 = 4.
3671        // A naive "always Unknown" fix would yield u_operands = 3, N2 = 3,
3672        // diverging from the plain-string equivalent below.
3673        check_metrics::<CsharpParser>(
3674            "class C { void M() { string s = $\"hello\"; } }",
3675            "foo.cs",
3676            |metric| {
3677                assert_eq!(metric.halstead.unique_operands(), 4);
3678                assert_eq!(metric.halstead.total_operands(), 4);
3679            },
3680        );
3681    }
3682
3683    #[cfg(feature = "csharp")]
3684    #[test]
3685    fn csharp_plain_string_still_operand() {
3686        // The fix for #183 only changes how `InterpolatedStringExpression`
3687        // is classified; plain `StringLiteral` (and `VerbatimStringLiteral`
3688        // / `RawStringLiteral`) must still contribute exactly one operand
3689        // each. expected: operands are `C`, `M`, `s`, `"hi"` →
3690        // u_operands = 4, N2 = 4.
3691        check_metrics::<CsharpParser>(
3692            "class C { void M() { string s = \"hi\"; } }",
3693            "foo.cs",
3694            |metric| {
3695                assert_eq!(metric.halstead.unique_operands(), 4);
3696                assert_eq!(metric.halstead.total_operands(), 4);
3697            },
3698        );
3699    }
3700
3701    // C# half of #1380 — see `java_self_and_super_references_are_operands`
3702    // for the structural argument. `base` moves with `this`: both are
3703    // receivers of a member access.
3704    #[cfg(feature = "csharp")]
3705    #[test]
3706    fn csharp_self_and_base_references_are_operands() {
3707        let source = "class T {\n    int F() { return this.x + base.y; }\n}";
3708        // Operators (n1=8, N1=10): class, {} x2, int, (), return, . x2, +, ;
3709        // Operands (n2=6, N2=6): T, F, this, x, base, y
3710        // Before the fix this read (10, 12, 4, 4).
3711        assert_halstead_counts::<CsharpParser>(
3712            source,
3713            "foo.cs",
3714            [8, 10, 6, 6],
3715            "csharp self/base references",
3716        );
3717        assert_keywords_are_operands_only::<CsharpParser>(source, "foo.cs", &["this", "base"]);
3718    }
3719
3720    // An `indexer_declaration` spells the member's *name* with the same
3721    // `this` token kind the receiver uses (both are kind 91), so a
3722    // blanket move would have billed a declarator keyword as a value.
3723    // The declaration keeps the operator classification that the
3724    // `operator` keyword of an overload declaration already has (#1380).
3725    //
3726    // The fixture carries both uses, which is what makes the gate
3727    // observable: `this` has to reach *both* vocabularies from one file.
3728    // Asserting only the operand side would pass with the gate deleted,
3729    // and only the operator side would pass with the whole #1380 change
3730    // reverted (grammar-dispatch section 11).
3731    #[cfg(feature = "csharp")]
3732    #[test]
3733    fn csharp_indexer_declaration_keyword_is_not_a_self_reference() {
3734        let source = "class C {\n    int[] _a;\n    \
3735                      public int this[int i] { get { return this._a[i]; } }\n}";
3736
3737        // Asserted per space, not through the root. The root merges
3738        // every space's vocabulary, and the fixture has one declarator
3739        // `this` and one receiver `this`, so at the root `this` is a
3740        // member of *both* vocabularies whether the gate is right or
3741        // exactly backwards — and every Halstead count is bit-identical
3742        // under the swap, one occurrence on each side either way. The
3743        // space tree is where the two uses stay apart: the declarator
3744        // sits in the class's own vocabulary and the receiver in the
3745        // accessor's.
3746        let mut roles = Vec::new();
3747        collect_keyword_roles(
3748            &ops_of::<CsharpParser>(source, "foo.cs"),
3749            "this",
3750            &mut roles,
3751        );
3752
3753        // `Both` can only arise from two `this` nodes in one space that
3754        // the getter classifies differently — it is the gate's whole
3755        // effect, and it disappears the moment `this` gets one role.
3756        // `OperandOnly` is the accessor, the innermost space and the one
3757        // holding the receiver alone; it is what flips when the gate is
3758        // inverted rather than dropped. Correct gives
3759        // `[Both, Both, OperandOnly]`; inverting gives
3760        // `[Both, Both, OperatorOnly]`; dropping the gate gives three
3761        // `OperandOnly`; reverting #1380 gives three `OperatorOnly`.
3762        // Asserting the whole sequence rather than two `contains` calls:
3763        // the comment above already derives it, and a `contains` pair
3764        // leaves the length unasserted, so a space appearing or vanishing
3765        // goes unnoticed and `[Both, X, OperandOnly]` passes for any `X`.
3766        assert_eq!(
3767            roles,
3768            vec![Role::Both, Role::Both, Role::OperandOnly],
3769            "the declarator and the receiver must disagree in the two outer spaces \
3770             and the accessor must bill `this` as an operand alone",
3771        );
3772    }
3773
3774    /// One `this` and one `base` per container the pinned
3775    /// `tree-sitter-c-sharp` can put them in: a delegating constructor
3776    /// initializer, a base-constructor initializer, a member access, an
3777    /// element access, and a call argument. No `indexer_declaration` —
3778    /// that position is the gated one and is an operator.
3779    #[cfg(feature = "csharp")]
3780    const CSHARP_SELF_POSITIONS: &str = "class Pos : B {
3781        int[] _a;
3782        public Pos() : this(1) { }
3783        public Pos(int x) : base(x) { }
3784        int A() { return this._a[0]; }
3785        int C() { return base.H(); }
3786        int E() { return base[0]; }
3787        int F() { return this[0]; }
3788        void G() { M(this); }
3789        void M(object o) { }
3790    }";
3791
3792    #[cfg(feature = "csharp")]
3793    #[test]
3794    fn csharp_self_and_base_leaves_are_unaliased_and_unconditional() {
3795        assert_self_reference_leaves::<CsharpCode>(
3796            CSHARP_SELF_POSITIONS,
3797            &[
3798                ("this", Csharp::This as u16, 4),
3799                ("base", Csharp::Base as u16, 3),
3800            ],
3801            "csharp",
3802        );
3803    }
3804
3805    /// Every spelling of the six keywords `_parameter_type_with_modifiers`
3806    /// aliases to `modifier`, plus two controls that were billed before
3807    /// #1418: the bare `params` of a parameter array (kind 94, never
3808    /// aliased) and the bare `out` of a call argument (kind 56).
3809    ///
3810    /// `D` is declared *and* called so the argument `out` has a caller
3811    /// to sit in, which is what makes the two spellings of one keyword
3812    /// meet in one file.
3813    #[cfg(feature = "csharp")]
3814    const CSHARP_PARAMETER_MODIFIERS: &str = "static class E {
3815    static void A(this Foo f) { }
3816    static void B(scoped ref Foo f) { }
3817    static void C(ref readonly Foo f) { }
3818    static void D(out int x) { x = 1; }
3819    static void G(in Foo f) { }
3820    static void H(params int[] xs) { }
3821    static void I(ref int y) { D(out y); }
3822}";
3823
3824    /// Each of the five operator keywords in *both* of its kind
3825    /// spellings — the childless `modifier` of a parameter, and the
3826    /// keyword's own token elsewhere: `ref` as a ref local, ref
3827    /// initializer and ref argument (kind 28); `readonly` as the leaf
3828    /// under a field's `modifier` wrapper (48); `in` as the `foreach`
3829    /// separator and an `in` argument (55); `out` as an out argument
3830    /// (56); `scoped` as a scoped ref local (93).
3831    ///
3832    /// Separate from [`CSHARP_PARAMETER_MODIFIERS`] because that fixture
3833    /// pairs the two spellings for `out` alone, which left four of the
3834    /// five bare kinds `CsharpCode::is_primitive` lists with no input
3835    /// (#1418).
3836    #[cfg(feature = "csharp")]
3837    const CSHARP_MODIFIER_BOTH_SPELLINGS: &str = "class S {
3838    static readonly int q = 0;
3839    static void M(scoped ref int a, ref readonly int b, in int c, out int d) { d = q; }
3840    static void N(int[] xs) {
3841        ref int r = ref xs[0];
3842        scoped ref int s = ref xs[1];
3843        foreach (var e in xs) { }
3844        M(ref r, in s, in s, out r);
3845    }
3846}";
3847
3848    /// The six aliased parameter modifiers reach a Halstead half, and
3849    /// each reaches the same one its bare token reaches elsewhere
3850    /// (#1418).
3851    #[cfg(feature = "csharp")]
3852    #[test]
3853    fn csharp_parameter_modifiers_are_classified_1418() {
3854        // Operators (n1 = 15, N1 = 48): class 1; {} 8 (the class body
3855        // and seven method bodies); static 8 (the class and seven
3856        // methods); void 7; () 8 (seven parameter lists and the call);
3857        // int 3 (`out int x`, `int[]`, `ref int y`); [] 1; = 1; ; 2;
3858        // scoped 1; ref 3; readonly 1; out 2 (the parameter and the
3859        // argument); in 1; params 1.
3860        //
3861        // Operands (n2 = 15, N2 = 24): E 1; A/B/C/G/H/I 1 each; D 2
3862        // (declaration and call); this 1; Foo 4; f 4; x 2; 1 1; xs 1;
3863        // y 2.
3864        //
3865        // Before the fix this read (11, 41, 14, 23). The eight childless
3866        // `modifier` nodes were billed nowhere, which cost seven
3867        // operator occurrences and one operand occurrence, and emptied
3868        // `scoped`, `ref`, `readonly` and `in` out of the operator
3869        // vocabulary entirely — `out` survived only because the call
3870        // argument spells it with the unaliased kind.
3871        assert_halstead_counts::<CsharpParser>(
3872            CSHARP_PARAMETER_MODIFIERS,
3873            "foo.cs",
3874            [15, 48, 15, 24],
3875            "csharp parameter modifiers",
3876        );
3877
3878        // The receiver is an operand, matching every other C# `this`
3879        // (#1380). Both halves matter: an arm listing it twice would
3880        // still put it among the operands.
3881        assert_keywords_are_operands_only::<CsharpParser>(
3882            CSHARP_PARAMETER_MODIFIERS,
3883            "foo.cs",
3884            &["this"],
3885        );
3886
3887        let ops = ops_of::<CsharpParser>(CSHARP_PARAMETER_MODIFIERS, "foo.cs");
3888        for keyword in ["ref", "out", "in", "scoped", "readonly", "params"] {
3889            assert!(
3890                ops.operators.iter().any(|o| o == keyword),
3891                "`{keyword}` must be an operator; operators were {:?}",
3892                ops.operators
3893            );
3894            assert!(
3895                !ops.operands.iter().any(|o| o == keyword),
3896                "`{keyword}` must not be an operand; operands were {:?}",
3897                ops.operands
3898            );
3899        }
3900
3901        // The vocabulary is not deduplicated — it is the concatenation
3902        // of the kind-keyed and the lexeme-keyed operator maps — so a
3903        // keyword appearing twice is the #453 shape: one keyword split
3904        // across the two maps and counted twice in `n1`. Keeping both
3905        // spellings in one map is what `CsharpCode::is_primitive`
3906        // listing the five bare kinds buys, and this is the only
3907        // assertion that can see it.
3908        //
3909        // It needs a fixture carrying *both* spellings of each keyword,
3910        // which `CSHARP_PARAMETER_MODIFIERS` does for `out` alone: its
3911        // `ref`, `in`, `scoped` and `readonly` are all in parameter
3912        // position, so for those the count can only ever be one and the
3913        // assertion is dead. That left four of the five bare kinds with
3914        // no input at all — dropping `Csharp::Ref`, `In`, `Readonly` or
3915        // `Scoped` from `is_primitive` failed no test in the workspace.
3916        assert_modifier_spellings_are_one_operator(
3917            CSHARP_MODIFIER_BOTH_SPELLINGS,
3918            &[
3919                ("ref", Csharp::Ref as u16),
3920                ("out", Csharp::Out as u16),
3921                ("in", Csharp::In as u16),
3922                ("scoped", Csharp::Scoped as u16),
3923                ("readonly", Csharp::Readonly as u16),
3924            ],
3925        );
3926    }
3927
3928    /// Asserts each keyword in `spellings` appears in `source` both as
3929    /// its own `kind` and as a childless aliased `modifier`, and that the
3930    /// two spellings render as exactly one entry in the operator
3931    /// vocabulary.
3932    ///
3933    /// The vocabulary is the concatenation of the kind-keyed and the
3934    /// lexeme-keyed operator maps, so a keyword listed twice is one
3935    /// keyword split across both maps and counted twice in `n1`.
3936    #[cfg(feature = "csharp")]
3937    #[track_caller]
3938    fn assert_modifier_spellings_are_one_operator(source: &str, spellings: &[(&str, u16)]) {
3939        let code = source.as_bytes();
3940        // (bare, aliased) occurrences per keyword.
3941        let mut seen = vec![(0_usize, 0_usize); spellings.len()];
3942
3943        for_each_node_with_chain::<CsharpCode>(code, |node, _| {
3944            let text = &code[node.start_byte()..node.end_byte()];
3945            let Some(index) = spellings.iter().position(|(kw, _)| kw.as_bytes() == text) else {
3946                return;
3947            };
3948            if node.kind_id() == Csharp::Modifier as u16 && node.child_count() == 0 {
3949                seen[index].1 += 1;
3950            } else if node.kind_id() == spellings[index].1 {
3951                seen[index].0 += 1;
3952            }
3953        });
3954
3955        // Without this the vocabulary count below is vacuous for any
3956        // keyword the fixture stopped spelling both ways — the decay
3957        // that once left four of #1418's bare kinds with no input.
3958        for ((keyword, _), (bare, aliased)) in spellings.iter().zip(&seen) {
3959            assert!(
3960                *bare > 0 && *aliased > 0,
3961                "fixture must spell `{keyword}` both ways; it has {bare} bare and {aliased} aliased"
3962            );
3963        }
3964
3965        let ops = ops_of::<CsharpParser>(source, "both.cs");
3966        for (keyword, _) in spellings {
3967            assert_eq!(
3968                ops.operators.iter().filter(|o| o == keyword).count(),
3969                1,
3970                "`{keyword}` must be one operator across both of its kind spellings; \
3971                 operators were {:?}",
3972                ops.operators
3973            );
3974        }
3975    }
3976
3977    /// Pins the grammar shape the #1418 arm gates on, in both
3978    /// directions.
3979    ///
3980    /// The arm reads `modifier` nodes with no children as aliased bare
3981    /// keywords and leaves the rest to their classified keyword leaf.
3982    /// That is a claim about two populations, and a grammar bump can
3983    /// break either: giving an alias a token child would double-count
3984    /// it, and flattening a declaration modifier to a childless node
3985    /// would bill `public` / `static` as an unknown keyword. Both are
3986    /// checked here, so neither can move quietly.
3987    ///
3988    /// Childlessness says "aliased", *not* "parameter modifier" —
3989    /// `_lambda_expression_init` and `anonymous_method_expression` alias
3990    /// a bare `static` / `async` onto the same kind, which the second
3991    /// walk below pins so the distinction cannot be forgotten. Both are
3992    /// operators, as their declaration spelling is (#1482).
3993    #[cfg(feature = "csharp")]
3994    #[test]
3995    fn csharp_aliased_modifier_is_childless() {
3996        let code = CSHARP_PARAMETER_MODIFIERS.as_bytes();
3997        let (mut aliased, mut declaration) = (0_usize, 0_usize);
3998
3999        for_each_node_with_chain::<CsharpCode>(code, |node, chain| {
4000            if node.kind_id() != Csharp::Modifier as u16 {
4001                return;
4002            }
4003            let role = CsharpCode::get_op_type_with_code(node, code, Ancestors::known(chain));
4004            let text = &code[node.start_byte()..node.end_byte()];
4005
4006            if node.child_count() == 0 {
4007                aliased += 1;
4008                // Spelled out rather than `this`-versus-everything-else,
4009                // so a text this fixture does not carry fails loudly;
4010                // the lambda spellings are owned by the second walk.
4011                let (correct, expected) = match text {
4012                    b"this" => (matches!(role, TokenRole::Operand), "an operand"),
4013                    b"scoped" | b"ref" | b"out" | b"in" | b"readonly" => {
4014                        (matches!(role, TokenRole::Operator), "an operator")
4015                    }
4016                    other => panic!(
4017                        "`{}` is not one of the six parameter spellings this fixture carries",
4018                        String::from_utf8_lossy(other)
4019                    ),
4020                };
4021                assert!(
4022                    correct,
4023                    "`{}` is an aliased parameter modifier and must be {expected}",
4024                    String::from_utf8_lossy(text)
4025                );
4026                // Grammar-dispatch section 5: nothing containing the
4027                // token is classified, so billing the token cannot
4028                // count the same source text twice. Every ancestor,
4029                // not just `parameter` — `parameter_list` and the
4030                // declaration above it contain it too.
4031                for (depth, ancestor) in chain.iter().enumerate() {
4032                    assert!(
4033                        matches!(
4034                            CsharpCode::get_op_type_with_code(
4035                                ancestor,
4036                                code,
4037                                Ancestors::known(&chain[..depth])
4038                            ),
4039                            TokenRole::Unknown
4040                        ),
4041                        "`{}` contains an aliased modifier and is itself classified, so the \
4042                         keyword now counts twice",
4043                        ancestor.kind()
4044                    );
4045                }
4046            } else {
4047                declaration += 1;
4048                assert_eq!(
4049                    node.child_count(),
4050                    1,
4051                    "a declaration `modifier` wraps exactly one keyword leaf"
4052                );
4053                assert!(
4054                    matches!(role, TokenRole::Unknown),
4055                    "the `{}` wrapper must stay unclassified; its leaf carries the operator",
4056                    String::from_utf8_lossy(text)
4057                );
4058                let leaf = node.child(0).expect("child_count is 1");
4059                let mut leaf_chain = chain.to_vec();
4060                leaf_chain.push(*node);
4061                assert!(
4062                    matches!(
4063                        CsharpCode::get_op_type_with_code(
4064                            &leaf,
4065                            code,
4066                            Ancestors::known(&leaf_chain)
4067                        ),
4068                        TokenRole::Operator
4069                    ),
4070                    "the leaf under a declaration `modifier` must carry the operator"
4071                );
4072            }
4073        });
4074
4075        // Eight of each: `this`, `scoped`, `ref` x3, `readonly`, `out`,
4076        // `in` in parameter position, and the `static` of the class and
4077        // of its seven methods. Asserted so a fixture that stopped
4078        // carrying one population cannot pass having checked nothing.
4079        assert_eq!(aliased, 8, "fixture lost an aliased parameter modifier");
4080        assert_eq!(declaration, 8, "fixture lost a declaration modifier");
4081
4082        // The second alias site. `static (int x) => …` and `async
4083        // delegate (int y) { … }` put a bare keyword under the same
4084        // childless `modifier`, so `csharp_is_aliased_modifier` holds
4085        // for a node that is not in a parameter at all — which is why
4086        // the getter dispatches on the text and not on the kind.
4087        let lambda = "static class L {
4088    static void M() {
4089        var a = static (int x) => x + 1;
4090        var b = async delegate (int y) { return y; };
4091    }
4092}"
4093        .as_bytes();
4094        let mut lambda_aliases = 0_usize;
4095
4096        for_each_node_with_chain::<CsharpCode>(lambda, |node, chain| {
4097            if node.kind_id() != Csharp::Modifier as u16 || node.child_count() != 0 {
4098                return;
4099            }
4100            lambda_aliases += 1;
4101            let text = &lambda[node.start_byte()..node.end_byte()];
4102            assert!(
4103                matches!(text, b"static" | b"async"),
4104                "`{}` is not a lambda modifier the grammar aliases",
4105                String::from_utf8_lossy(text)
4106            );
4107            assert!(
4108                matches!(
4109                    CsharpCode::get_op_type_with_code(node, lambda, Ancestors::known(chain)),
4110                    TokenRole::Operator
4111                ),
4112                "`{}` on a lambda must be an operator, as its declaration spelling is (#1482)",
4113                String::from_utf8_lossy(text)
4114            );
4115        });
4116
4117        assert_eq!(
4118            lambda_aliases, 2,
4119            "fixture lost a lambda / anonymous-method modifier alias"
4120        );
4121    }
4122
4123    /// `static` and `async` in both of their kind spellings: the keyword
4124    /// leaf under a method's `modifier` wrapper, and the childless
4125    /// `modifier` the grammar aliases a lambda's keyword to (#1482).
4126    #[cfg(feature = "csharp")]
4127    const CSHARP_LAMBDA_MODIFIER_BOTH_SPELLINGS: &str = "class L {
4128    static void N() { }
4129    async void M() { }
4130    void P() {
4131        var a = static (int x) => x;
4132        var b = async (int y) => y;
4133    }
4134}";
4135
4136    /// A C# lambda's bare parameter (`x => x`) is the operand its
4137    /// parenthesised twin (`(x) => x`) bills (#1544). Each row is one
4138    /// lambda in a method body; the twins differ only by the `()` the
4139    /// parenthesised spelling writes, so the operand halves must match
4140    /// and the operator halves differ by exactly that one `()`.
4141    #[cfg(feature = "csharp")]
4142    #[test]
4143    fn csharp_implicit_lambda_parameter_is_an_operand() {
4144        let wrap = |lambda: &str| {
4145            format!("class C {{\n    void M() {{\n        var f = {lambda};\n    }}\n}}\n")
4146        };
4147        // Operators: class, {} x2, void, () x1 (M's parameter list), =,
4148        // =>, ; — n1 = 7, N1 = 8 — plus the lambda's own `()` in the
4149        // parenthesised spelling. Operands: C, M, f, and the parameter
4150        // twice; `var` is an unclassified `implicit_type`. `_ => 0`
4151        // names `_` once and adds the literal `0`.
4152        //
4153        // Before #1544 the bare rows read (7, 8, 4, 4) for both: `x`
4154        // lost one occurrence, and `_` left the vocabulary entirely.
4155        for (lambda, expected) in [
4156            ("x => x", [7, 8, 4, 5]),
4157            ("(x) => x", [7, 9, 4, 5]),
4158            ("_ => 0", [7, 8, 5, 5]),
4159            ("(_) => 0", [7, 9, 5, 5]),
4160        ] {
4161            assert_halstead_counts::<CsharpParser>(&wrap(lambda), "foo.cs", expected, lambda);
4162        }
4163
4164        // The bare parameter is billed under its own text, not under
4165        // the kind name, and as an operand only.
4166        assert_keywords_are_operands_only::<CsharpParser>(&wrap("_ => 0"), "foo.cs", &["_"]);
4167    }
4168
4169    /// A lambda's `static` / `async` is the same operator as the
4170    /// declaration's, counted once in `n1` across both spellings
4171    /// (#1482).
4172    #[cfg(feature = "csharp")]
4173    #[test]
4174    fn csharp_lambda_modifiers_are_operators_1482() {
4175        // Operators (n1 = 10, N1 = 25): class 1; {} 4 (the class body
4176        // and three method bodies); static 2; async 2; void 3; () 5
4177        // (three method parameter lists and two lambda ones); int 2;
4178        // = 2; ; 2; => 2.
4179        //
4180        // Operands (n2 = 8, N2 = 10): L, N, M, P, a, b once each; x 2
4181        // and y 2 (the parameter and the lambda body). `var` is an
4182        // unclassified `implicit_type` wrapper.
4183        //
4184        // Before the fix this read (10, 23, 8, 10): the two lambda
4185        // keywords were billed nowhere. Billing them without listing
4186        // `Static` / `Async` in `CsharpCode::is_primitive` reads
4187        // (12, 25, 8, 10) — each keyword split across the kind-keyed
4188        // and the lexeme-keyed operator map.
4189        assert_halstead_counts::<CsharpParser>(
4190            CSHARP_LAMBDA_MODIFIER_BOTH_SPELLINGS,
4191            "foo.cs",
4192            [10, 25, 8, 10],
4193            "csharp lambda modifiers",
4194        );
4195
4196        assert_modifier_spellings_are_one_operator(
4197            CSHARP_LAMBDA_MODIFIER_BOTH_SPELLINGS,
4198            &[
4199                ("static", Csharp::Static as u16),
4200                ("async", Csharp::Async as u16),
4201            ],
4202        );
4203    }
4204
4205    #[cfg(feature = "go")]
4206    #[test]
4207    fn go_operators_and_operands() {
4208        check_metrics::<GoParser>(
4209            "package main
4210            func sum(a, b int) int {
4211                return a + b
4212            }",
4213            "foo.go",
4214            |metric| {
4215                insta::assert_json_snapshot!(
4216                    metric.halstead,
4217                    @r#"
4218                {
4219                  "unique_operators": 7,
4220                  "total_operators": 7,
4221                  "unique_operands": 5,
4222                  "total_operands": 8,
4223                  "length": 15,
4224                  "estimated_program_length": 31.26112492884004,
4225                  "purity_ratio": 2.0840749952560027,
4226                  "vocabulary": 12,
4227                  "volume": 53.77443751081734,
4228                  "difficulty": 5.6,
4229                  "level": 0.17857142857142858,
4230                  "effort": 301.1368500605771,
4231                  "time": 16.729825003365395,
4232                  "bugs": 0.014975730436275946
4233                }
4234                "#
4235                );
4236            },
4237        );
4238    }
4239
4240    #[cfg(feature = "perl")]
4241    #[test]
4242    fn perl_operators_and_operands() {
4243        check_metrics::<PerlParser>(
4244            "sub sum {
4245                my ($a, $b) = @_;
4246                return $a + $b;
4247            }",
4248            "foo.pl",
4249            |metric| {
4250                insta::assert_json_snapshot!(
4251                    metric.halstead,
4252                    @r#"
4253                {
4254                  "unique_operators": 10,
4255                  "total_operators": 14,
4256                  "unique_operands": 4,
4257                  "total_operands": 6,
4258                  "length": 20,
4259                  "estimated_program_length": 41.219280948873624,
4260                  "purity_ratio": 2.0609640474436812,
4261                  "vocabulary": 14,
4262                  "volume": 76.14709844115208,
4263                  "difficulty": 7.5,
4264                  "level": 0.13333333333333333,
4265                  "effort": 571.1032383086406,
4266                  "time": 31.727957683813365,
4267                  "bugs": 0.02294502281013948
4268                }
4269                "#
4270                );
4271            },
4272        );
4273    }
4274
4275    #[cfg(feature = "perl")]
4276    #[test]
4277    fn perl_interpolated_string_no_double_count() {
4278        // Regression: issue #199. A `string_double_quoted` (and
4279        // `string_qq_quoted` / `backtick_quoted` / `command_qx_quoted`)
4280        // wrapping an `interpolation` child used to be counted as a
4281        // Halstead operand while the inner scalar/array/hash variable
4282        // was also walked and counted — double-counting the inner
4283        // variable's contribution to `N2`. Mirrors #180 (Bash/Elixir),
4284        // #183 (C#), #184 (PHP), #191 (Kotlin).
4285        //
4286        // expected: for
4287        //   sub greet { my $name = shift; my $msg = "Hi $name"; return $msg; }
4288        // — operands are `greet`, `$name`, `shift`, `$msg`. With the
4289        // fix the wrapping `"Hi $name"` is skipped (has `Interpolation`
4290        // child), so u_operands = 4 and N2 = 6 (`$name` x2 from the
4291        // `my` binding and the interpolation; `$msg` x2 from the `my`
4292        // binding and `return`; `greet`, `shift` once each). Without
4293        // the fix the wrapping literal would also be counted, lifting
4294        // u_operands to 5 and N2 to 7.
4295        check_metrics::<PerlParser>(
4296            "sub greet { my $name = shift; my $msg = \"Hi $name\"; return $msg; }",
4297            "foo.pl",
4298            |metric| {
4299                assert_eq!(metric.halstead.unique_operands(), 4);
4300                assert_eq!(metric.halstead.total_operands(), 6);
4301                insta::assert_json_snapshot!(metric.halstead);
4302            },
4303        );
4304    }
4305
4306    #[cfg(feature = "perl")]
4307    #[test]
4308    fn perl_plain_string_still_operand() {
4309        // The fix for #199 only skips wrapping literals that carry an
4310        // `Interpolation` child; a plain `"hello"` (no `$…` inside)
4311        // must still contribute exactly one operand. expected: operands
4312        // `greet`, `$msg`, `"hello"` → u_operands = 3, N2 = 4 (`$msg`
4313        // appears in the `my` binding and the `return`).
4314        check_metrics::<PerlParser>(
4315            "sub greet { my $msg = \"hello\"; return $msg; }",
4316            "foo.pl",
4317            |metric| {
4318                assert_eq!(metric.halstead.unique_operands(), 3);
4319                assert_eq!(metric.halstead.total_operands(), 4);
4320            },
4321        );
4322    }
4323
4324    #[cfg(feature = "perl")]
4325    #[test]
4326    fn perl_single_quoted_string_never_interpolates() {
4327        // Single-quoted (`'…'`) and `q{…}` literals are not subject to
4328        // interpolation in Perl, so even when their text contains a
4329        // `$name`-shaped sequence the wrapper is still counted as one
4330        // operand and the inner text is not parsed as a variable.
4331        // expected: operands `greet`, `$msg`, `'Hi $name'` →
4332        // u_operands = 3, N2 = 4 (`$msg` x2).
4333        check_metrics::<PerlParser>(
4334            "sub greet { my $msg = 'Hi $name'; return $msg; }",
4335            "foo.pl",
4336            |metric| {
4337                assert_eq!(metric.halstead.unique_operands(), 3);
4338                assert_eq!(metric.halstead.total_operands(), 4);
4339            },
4340        );
4341    }
4342
4343    #[cfg(feature = "perl")]
4344    #[test]
4345    fn perl_plain_heredoc_counts_as_one_operand() {
4346        // Regression: issue #287. A plain (non-interpolating) Perl
4347        // heredoc body used to be classified `TokenRole::Unknown`,
4348        // so its visible `HeredocBodyStatement` node contributed
4349        // nothing to N2 even though it is a string literal. The fix
4350        // adds `HeredocBodyStatement` to the interpolation-aware
4351        // operand arm, so an inert heredoc counts as one operand.
4352        //
4353        // Source (heredoc body lives at the source_file level, not
4354        // inside any sub):
4355        //   my $msg = <<END;
4356        //   hello world
4357        //   END
4358        //
4359        // Operands traversed:
4360        //   * `$msg` (`scalar_variable`)                    × 1
4361        //   * heredoc body (`heredoc_body_statement`)       × 1
4362        // expected: u_operands = 2, N2 = 2.
4363        check_metrics::<PerlParser>("my $msg = <<END;\nhello world\nEND\n", "foo.pl", |metric| {
4364            assert_eq!(metric.halstead.unique_operands(), 2);
4365            assert_eq!(metric.halstead.total_operands(), 2);
4366        });
4367    }
4368
4369    #[cfg(feature = "perl")]
4370    #[test]
4371    fn perl_interpolated_heredoc_no_double_count() {
4372        // Regression: issue #287. An interpolating Perl heredoc
4373        // (`<<"TAG"` or bare `<<TAG`) carries an `Interpolation` child
4374        // when its body contains a `$var`. The wrapper must drop to
4375        // `Unknown` so the inner scalar variable carries the operand
4376        // count — same dispatch as the existing double-quoted /
4377        // backtick / qx wrappers (issue #199) and the PHP heredoc fix
4378        // (issue #184).
4379        //
4380        // Source:
4381        //   my $name = "x";
4382        //   my $msg = <<"END";
4383        //   hi $name
4384        //   END
4385        //
4386        // Operands by text key:
4387        //   * `$name` × 2 (my-binding + interpolation inside heredoc)
4388        //   * `"x"`  × 1 (inert double-quoted string)
4389        //   * `$msg` × 1
4390        // expected: u_operands = 3, N2 = 4. Without the
4391        // interpolation-aware drop the wrapping heredoc body would
4392        // also count, lifting u_operands to 4 and N2 to 5.
4393        check_metrics::<PerlParser>(
4394            "my $name = \"x\";\nmy $msg = <<\"END\";\nhi $name\nEND\n",
4395            "foo.pl",
4396            |metric| {
4397                assert_eq!(metric.halstead.unique_operands(), 3);
4398                assert_eq!(metric.halstead.total_operands(), 4);
4399            },
4400        );
4401    }
4402
4403    #[cfg(feature = "perl")]
4404    #[test]
4405    fn perl_bare_pattern_delimiters_are_not_operators() {
4406        // Regression: issue #1312, the Perl sibling of Elixir #1256.
4407        // The bare match form is the only one of Perl's regex literals
4408        // whose delimiters are spelled with an operator token kind —
4409        // `bca dump` shows `/abc/` emitting two `SLASH` under
4410        // `PatternMatcher` — so `$s =~ /abc/;` reported a `/` operator
4411        // with no division in the source.
4412        //
4413        // expected: operators `$` (the `scalar_variable` sigil), `=~`
4414        // and `;` → n1 = N1 = 3. Operands `$s` and the `/abc/` pattern
4415        // → n2 = N2 = 2. Before the guard the two delimiters added `/`
4416        // → n1 = 4, N1 = 5; the pattern operand arrived with #1314,
4417        // which promoted all three pattern spellings together (see
4418        // `perl_every_pattern_value_spelling_scores_alike`).
4419        check_metrics::<PerlParser>("$s =~ /abc/;\n", "foo.pl", |metric| {
4420            assert_eq!(metric.halstead.unique_operators(), 3);
4421            assert_eq!(metric.halstead.total_operators(), 3);
4422            assert_eq!(metric.halstead.unique_operands(), 2);
4423            assert_eq!(metric.halstead.total_operands(), 2);
4424        });
4425    }
4426
4427    #[cfg(feature = "perl")]
4428    #[test]
4429    fn perl_every_pattern_value_spelling_scores_alike() {
4430        // Companion to the test above (#1312, extended by #1314).
4431        // `m/abc/` is exactly `/abc/` in Perl and `qr/abc/` is the same
4432        // pattern as a value, so the three spellings of a pattern
4433        // *value* must score identically whatever delimiters they use.
4434        //
4435        // Until #1314 they scored alike at *zero*: no Perl pattern
4436        // wrapper was in the operand arm, so the literal never counted,
4437        // unlike Ruby's `Regex` and Elixir's `Sigil`. #1312 declined to
4438        // promote the bare form on its own precisely because that would
4439        // have scored `/abc/` at one operand and its synonyms at zero,
4440        // reintroducing the spelling sensitivity this test pins.
4441        // Promoting all three together closes the gap and keeps the
4442        // equality, which is what this test now asserts.
4443        //
4444        // `s///` and `tr///` are deliberately *not* rows here any more.
4445        // They are operations applied to a target rather than pattern
4446        // values, so #1314 made them operators; their own equality is
4447        // pinned by `perl_every_pattern_operation_spelling_scores_alike`
4448        // below. Splitting the one loop in two is the substantive
4449        // disagreement #1314 had with the reasoning recorded here: this
4450        // test governs *synonyms*, and `s///` is not a synonym of
4451        // `/abc/`.
4452        //
4453        // The fixture matches twice against one variable so that no two
4454        // of `[n1, N1, n2, N2]` are equal. A square tuple would leave
4455        // `assert_halstead_counts`' unique-vs-total axes unpinned —
4456        // transposing n1 with N1 inside the helper failed no test while
4457        // all three of its callers expected a square tuple.
4458        //
4459        // expected per variant: operators `$` × 2 (one per
4460        // `scalar_variable`), `=~` × 2, `and`, `;` → n1 = 4, N1 = 6.
4461        // The pattern contributes no *operator* — that is #1312's half
4462        // — and one operand, so with `$s` twice and the pattern twice
4463        // → n2 = 2, N2 = 4.
4464        for pattern in ["/abc/", "m/abc/", "m{abc}", "qr/abc/"] {
4465            assert_halstead_counts::<PerlParser>(
4466                &format!("$s =~ {pattern} and $s =~ {pattern};\n"),
4467                "foo.pl",
4468                [4, 6, 2, 4],
4469                &format!("pattern {pattern}"),
4470            );
4471        }
4472    }
4473
4474    #[cfg(feature = "perl")]
4475    #[test]
4476    fn perl_comment_leader_is_not_an_operator_1549() {
4477        // tree-sitter-perl emits a comment's leading `#` as a `#` token
4478        // under `comments`, and the getter listed it as an operator, so
4479        // every comment billed one `#` (#1549). The oracle is the same
4480        // source with its comments deleted, which must score identically.
4481        // `$#a` and `qw#…#` are kept to pin that dropping `#` leaves the
4482        // distinct `$#` operator and the `qw` delimiters alone.
4483        //
4484        // expected: operators `sub`, `{}`, `my`, `=`, `;` × 2, `return`,
4485        // `$#` × 2, `+` → n1 = 8, N1 = 10. Operands `f`, `@a`, `x`, `y`,
4486        // `a` × 2 → n2 = 5, N2 = 6. Before the fix the commented row
4487        // added `#` → n1 = 9, N1 = 13. Once excluded, a comment feeds no
4488        // axis to anchor on, so only the revert run guards the fixture:
4489        // keep its comments.
4490        for (source, label) in [
4491            (
4492                "sub f {\n  # leading\n  my @a = qw#x y#; # tail\n  return $#a + $#a; # tail\n}\n",
4493                "commented",
4494            ),
4495            (
4496                "sub f {\n  my @a = qw#x y#;\n  return $#a + $#a;\n}\n",
4497                "comment-free twin",
4498            ),
4499        ] {
4500            assert_halstead_counts::<PerlParser>(source, "foo.pl", [8, 10, 5, 6], label);
4501        }
4502    }
4503
4504    #[cfg(feature = "perl")]
4505    #[test]
4506    fn perl_bare_pattern_text_bills_no_operator_1549() {
4507        // The bare `/…/` form keeps its pattern text in a
4508        // `regex_pattern_content` that spells `#`, `[`, `]` and `\` as
4509        // anonymous tokens, three of which an operator arm listed, so
4510        // `/a#b[c]\#/` billed `#`, `[]` and `\` operators. `m//` and
4511        // `qr//` emit no node for the same text, so they are the oracle:
4512        // all three spellings must score as `/abc/` does in
4513        // `perl_every_pattern_value_spelling_scores_alike`.
4514        //
4515        // expected per variant: operators `$` × 2, `=~` × 2, `and`, `;`
4516        // → n1 = 4, N1 = 6; operands `$s` × 2 and the pattern × 2 →
4517        // n2 = 2, N2 = 4. Before the fix the bare row scored n1 = 7,
4518        // N1 = 14. The `#`, `[c]` and `\#` feed no axis once excluded,
4519        // so nothing anchors them and only the revert run guards the
4520        // fixture: keep them in the bare row.
4521        for pattern in [r"/a#b[c]\#/", r"m/a#b[c]\#/", r"qr/a#b[c]\#/"] {
4522            assert_halstead_counts::<PerlParser>(
4523                &format!("$s =~ {pattern} and $s =~ {pattern};\n"),
4524                "foo.pl",
4525                [4, 6, 2, 4],
4526                &format!("pattern {pattern}"),
4527            );
4528        }
4529    }
4530
4531    #[cfg(feature = "perl")]
4532    #[test]
4533    fn perl_every_pattern_operation_spelling_scores_alike() {
4534        // The other half of the split (#1314). Substitution and
4535        // transliteration are operations applied to a target, so they
4536        // are operators — and like the value spellings, their delimiter
4537        // choice must not move the count. `y///` is a synonym of
4538        // `tr///` and shares `TransliterationTrOrY`, so the two fold to
4539        // one operator entry, which is why all four rows agree on n1.
4540        //
4541        // expected per variant: operators `$` × 2, `=~` × 2, `and`,
4542        // `;`, and the operation itself × 2 → n1 = 5, N1 = 8. The
4543        // pattern and replacement text is invisible to this grammar —
4544        // `substitution_pattern_s` emits only its keyword and
4545        // delimiters, no content node — so the sole operand is `$s`,
4546        // twice → n2 = 1, N2 = 2.
4547        for pattern in ["s/a/b/", "s{a}{b}", "tr/a/b/", "y/a/b/"] {
4548            assert_halstead_counts::<PerlParser>(
4549                &format!("$s =~ {pattern} and $s =~ {pattern};\n"),
4550                "foo.pl",
4551                [5, 8, 1, 2],
4552                &format!("pattern {pattern}"),
4553            );
4554        }
4555    }
4556
4557    #[cfg(feature = "perl")]
4558    #[test]
4559    fn perl_interpolated_pattern_operands_agree_but_operators_do_not() {
4560        // Two things at once (#1314), because they are the same
4561        // measurement: why the three pattern-value spellings route
4562        // through `string_operand_type` rather than a plain operand
4563        // arm, and what that routing does *not* fix.
4564        //
4565        // `m/$x/` and `qr/$x/` emit a real `Interpolation` wrapping a
4566        // `scalar_variable`, while the bare form keeps its `$x` inside
4567        // an unclassified `regex_pattern_content`. So:
4568        //
4569        // * Operands agree at n2 = N2 = 2 (`$s` plus one contribution
4570        //   from the pattern) only because of the interpolation guard.
4571        //   A plain operand arm would count the wrapper *and* the inner
4572        //   `$x` for the suffixed forms — n2 = 3 — reintroducing
4573        //   through the back door the divergence
4574        //   `perl_every_pattern_value_spelling_scores_alike` exists to
4575        //   prevent. Which node carries the one operand still differs
4576        //   by spelling: the wrapper for the bare form, the inner `$x`
4577        //   for the other two.
4578        // * Operators do *not* agree: the exposed `scalar_variable`
4579        //   brings a `$` sigil, an operator here, that the bare form
4580        //   has no node for. Over two matches N1 is 6 for `/$x/` and
4581        //   8 for the other two.
4582        //
4583        // The operator asymmetry is a grammar gap this classifier
4584        // cannot repair — there is nothing to classify in the bare
4585        // form — so it is pinned rather than papered over, the same
4586        // treatment `perl_division_emits_no_slash_token` gives the
4587        // missing division token. A bump that starts exposing the bare
4588        // form's interpolation turns this red, at which point the
4589        // expectations above need re-deriving.
4590        // Each fixture matches twice, so `N1 > n1` and `N2 > n2` and no
4591        // row is a square tuple that a transposition inside
4592        // `assert_halstead_counts` could pass (#1312).
4593        assert_halstead_counts::<PerlParser>(
4594            "$s =~ /$x/ and $s =~ /$x/;\n",
4595            "foo.pl",
4596            [4, 6, 2, 4],
4597            "bare /$x/",
4598        );
4599        for pattern in ["m/$x/", "qr/$x/"] {
4600            assert_halstead_counts::<PerlParser>(
4601                &format!("$s =~ {pattern} and $s =~ {pattern};\n"),
4602                "foo.pl",
4603                [4, 8, 2, 4],
4604                &format!("suffixed {pattern}"),
4605            );
4606        }
4607    }
4608
4609    #[cfg(feature = "perl")]
4610    #[test]
4611    fn perl_division_emits_no_slash_token() {
4612        // Drift marker, not an endorsement. Ruby's counterpart
4613        // (`ruby_division_survives_the_regex_guard`) proves #1312's
4614        // guard cannot swallow a real division; Perl has no such
4615        // fixture to write, because at the pinned grammar `$a / $b`
4616        // emits *no* `SLASH` token at all — `binary_expression`'s
4617        // children skip straight from one `scalar_variable` to the
4618        // other. Perl division therefore counts zero operators today,
4619        // a pre-existing grammar gap this fix neither causes nor
4620        // repairs.
4621        //
4622        // Pinning it keeps the gap in CI: a bump that starts emitting
4623        // the token turns this red, at which point the division would
4624        // begin counting (its parent is `BinaryExpression`, not
4625        // `PatternMatcher`, so the guard leaves it alone) and the
4626        // expectations above need re-deriving.
4627        //
4628        // The same gap is why no Perl test can distinguish the
4629        // parent-scoped guard from an ancestor-scoped one: with no
4630        // `SLASH` reachable below a `PatternMatcher`, that mutant is
4631        // unobservable here — measured, not assumed. Perl's guard is
4632        // parent-scoped for correctness by construction and for
4633        // symmetry with Ruby's, where the distinction *is* observable
4634        // and is pinned by
4635        // `ruby_regex_guard_is_parent_scoped_not_ancestor_scoped`.
4636        let path = PathBuf::from("foo.pl");
4637        let source = "my $z = $a / $b;\n";
4638        let parser = PerlParser::new(source.as_bytes().to_vec(), &path, None);
4639        assert!(
4640            !ast_has_kind_id(&parser, Perl::SLASH as u16),
4641            "tree-sitter-perl still emits no SLASH for `{source}`"
4642        );
4643        // Anchor the negative assertion to *this* fixture. Without it
4644        // the test stays green when `source` is edited to something
4645        // containing no division at all — measured: swapping in
4646        // `my $z = 1;` failed nothing.
4647        //
4648        // expected: operators `my`, `=`, `$` × 3 (one per
4649        // `scalar_variable`), `;` → n1 = 4, N1 = 6, with no `/` among
4650        // them. Operands `$z`, `$a`, `$b` → n2 = N2 = 3.
4651        check_metrics::<PerlParser>(source, "foo.pl", |metric| {
4652            assert_eq!(metric.halstead.unique_operators(), 4);
4653            assert_eq!(metric.halstead.total_operators(), 6);
4654            assert_eq!(metric.halstead.unique_operands(), 3);
4655            assert_eq!(metric.halstead.total_operands(), 3);
4656        });
4657        // Positive control: the same kind *is* reachable in this
4658        // grammar, so the assertion above is about division and not
4659        // about `Perl::SLASH` being enum-only dead weight.
4660        let matcher = PerlParser::new(b"$s =~ /abc/;\n".to_vec(), &path, None);
4661        assert!(
4662            ast_has_kind_id(&matcher, Perl::SLASH as u16),
4663            "Perl::SLASH must be the bare pattern delimiter kind"
4664        );
4665    }
4666
4667    #[cfg(feature = "perl")]
4668    #[test]
4669    fn perl_readline_closer_alias_never_reaches_kind_id() {
4670        // Drift marker for `Perl::GT2` (#1395). The grammar closes
4671        // `<FH>` / `<$fh>` with `token.immediate('>')`, which the
4672        // generated parser gives its own symbol — hence the `GT2`
4673        // variant sitting next to `FileHandle` in the enum — but whose
4674        // `public_symbol_map` entry collapses it onto `GT` before
4675        // `kind_id()`, exactly like `LPAREN2` in #768 and Ruby's
4676        // `SLASH2` in #1312. Nothing in `getter/perl.rs` names it, and
4677        // nothing should: `compute_halstead` keys a non-primitive
4678        // operator on `kind_id()`, so two live kinds both rendering
4679        // `">"` would split one `>` across two `n1` entries. If a bump
4680        // ever starts emitting it, the repair is to route `>` through
4681        // the lexeme-keyed map, not to list both — and this test is
4682        // what says so.
4683        //
4684        // The C++ grammars' template-list closer collapses the same way;
4685        // `cpp_template_closer_alias_never_reaches_kind_id` pins that.
4686        let path = PathBuf::from("foo.pl");
4687        for source in ["my $l = <FH>;\n", "my $l = <$fh>;\n", "my $b = $x > $y;\n"] {
4688            let parser = PerlParser::new(source.as_bytes().to_vec(), &path, None);
4689            assert!(
4690                !ast_has_kind_id(&parser, Perl::GT2 as u16),
4691                "Perl::GT2 must stay collapsed to Perl::GT for `{source}`"
4692            );
4693            // Positive control: the id the operator arm fires on is
4694            // present, so the assertion above cannot pass merely
4695            // because the fixture grew no `>` at all.
4696            assert!(
4697                ast_has_kind_id(&parser, Perl::GT as u16),
4698                "Perl::GT must be the `>` kind for `{source}`"
4699            );
4700        }
4701    }
4702
4703    /// Drift marker for `Cpp::GT2` / `Mozcpp::GT2`. Both grammars close
4704    /// a template parameter or argument list with
4705    /// `alias(token(prec(1, '>')), '>')`, which the generated parser
4706    /// numbers separately, but `ts_symbol_map` folds that symbol onto
4707    /// `GT` before `kind_id()`, as with Perl's readline closer above. The
4708    /// `GT2` arms in `getter/cpp.rs` and `getter/mozcpp.rs` are therefore
4709    /// defensive; this fails if a grammar bump makes them live, at which
4710    /// point the two kinds would split one `>` across two `n1` entries.
4711    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
4712    fn assert_template_closer_folds_to_gt<P: crate::ParserTrait>(gt: u16, gt2: u16, label: &str) {
4713        // One parameter list (`S<T>`'s declaration) and two argument
4714        // lists, the inner one closed by the first half of a `>>`.
4715        let source = "template <typename T> struct S {};\n\
4716                      std::vector<std::vector<int>> v;\n\
4717                      bool b = x > y;\n";
4718        let parser = P::new(source.as_bytes().to_vec(), &PathBuf::from("foo.cpp"), None);
4719        assert!(
4720            !ast_has_kind_id(&parser, gt2),
4721            "{label}::GT2 must stay collapsed to {label}::GT",
4722        );
4723        // Positive control: every template-list closer is present and
4724        // carries `GT`, so the assertion above cannot pass merely
4725        // because the fixture stopped producing template lists.
4726        let closers = parser
4727            .root()
4728            .preorder()
4729            .filter(|n| {
4730                n.kind_id() == gt
4731                    && n.parent().is_some_and(|p| {
4732                        matches!(
4733                            p.kind(),
4734                            "template_parameter_list" | "template_argument_list"
4735                        )
4736                    })
4737            })
4738            .count();
4739        assert_eq!(
4740            closers, 3,
4741            "{label}: each template-list closer must be a `GT`"
4742        );
4743    }
4744
4745    #[cfg(feature = "cpp")]
4746    #[test]
4747    fn cpp_template_closer_alias_never_reaches_kind_id() {
4748        assert_template_closer_folds_to_gt::<CppParser>(Cpp::GT as u16, Cpp::GT2 as u16, "Cpp");
4749    }
4750
4751    #[cfg(feature = "mozcpp")]
4752    #[test]
4753    fn mozcpp_template_closer_alias_never_reaches_kind_id() {
4754        assert_template_closer_folds_to_gt::<MozcppParser>(
4755            Mozcpp::GT as u16,
4756            Mozcpp::GT2 as u16,
4757            "Mozcpp",
4758        );
4759    }
4760
4761    /// Asserts a grammar's wrapped generic-list opener, `LT2`, never
4762    /// reaches a node, so the `LT` its getter pairs as `<>` is the only
4763    /// `<` a list opens with (#1559, grammar-dispatch §2). `lists` are the
4764    /// lists the wrapped token opens; the positive control counts their
4765    /// `LT` openers, so the assertion cannot pass on a fixture that
4766    /// stopped producing those lists.
4767    #[cfg(any(feature = "groovy", feature = "objc", feature = "rust"))]
4768    #[track_caller]
4769    fn assert_generic_opener_folds_to_lt<P: crate::ParserTrait>(
4770        source: &str,
4771        [lt, lt2]: [u16; 2],
4772        lists: &[&str],
4773        openers: usize,
4774        label: &str,
4775    ) {
4776        let parser = P::new(source.as_bytes().to_vec(), &PathBuf::from("foo"), None);
4777        assert!(
4778            !ast_has_kind_id(&parser, lt2),
4779            "{label}::LT2 must stay collapsed to {label}::LT",
4780        );
4781        let found = parser
4782            .root()
4783            .preorder()
4784            .filter(|n| n.kind_id() == lt && n.parent().is_some_and(|p| lists.contains(&p.kind())))
4785            .count();
4786        assert_eq!(
4787            found, openers,
4788            "{label}: each generic-list opener must be an `LT`"
4789        );
4790    }
4791
4792    #[cfg(feature = "rust")]
4793    #[test]
4794    fn rust_generic_opener_alias_never_reaches_kind_id() {
4795        assert_generic_opener_folds_to_lt::<RustParser>(
4796            "fn u<T>(x: Vec<T>) -> impl Sized + use<T> { x }\n",
4797            [Rust::LT as u16, Rust::LT2 as u16],
4798            &["type_arguments", "use_bounds"],
4799            2,
4800            "Rust",
4801        );
4802    }
4803
4804    #[cfg(feature = "groovy")]
4805    #[test]
4806    fn groovy_generic_opener_alias_never_reaches_kind_id() {
4807        assert_generic_opener_folds_to_lt::<GroovyParser>(
4808            "class A<T> { List<T> v }\n",
4809            [Groovy::LT as u16, Groovy::LT2 as u16],
4810            &["type_parameters", "type_arguments"],
4811            2,
4812            "Groovy",
4813        );
4814    }
4815
4816    #[cfg(feature = "objc")]
4817    #[test]
4818    fn objc_generic_opener_alias_never_reaches_kind_id() {
4819        assert_generic_opener_folds_to_lt::<ObjcParser>(
4820            "int f(void) { return g(NSArray<NSString *>); }\n",
4821            [Objc::LT as u16, Objc::LT2 as u16],
4822            &["argument_list"],
4823            1,
4824            "Objc",
4825        );
4826    }
4827
4828    #[cfg(feature = "perl")]
4829    #[test]
4830    fn perl_readline_brackets_are_operators() {
4831        // #1395: a readline's `<` / `>` delimit syntax, not a literal,
4832        // so under the policy on `Getter::get_op_type` they are
4833        // ordinary operators — even though ABC's `conditions` excludes
4834        // them (#1297). `<STDIN>` is the one spelling that cannot show
4835        // this: the grammar lexes it as a single `standard_input`
4836        // token, so there are no brackets to classify.
4837        //
4838        // expected: operators `sub`, `{}`, `my`, `$` × 2 (one per
4839        // `scalar_variable`), `=`, `<`, `>`, `;` × 2, `return` →
4840        // n1 = 9, N1 = 11. Operands `r`, `$l` × 2, `FH` →
4841        // n2 = 3, N2 = 4.
4842        check_metrics::<PerlParser>("sub r { my $l = <FH>; return $l; }\n", "foo.pl", |metric| {
4843            assert_eq!(metric.halstead.unique_operators(), 9);
4844            assert_eq!(metric.halstead.total_operators(), 11);
4845            assert_eq!(metric.halstead.unique_operands(), 3);
4846            assert_eq!(metric.halstead.total_operands(), 4);
4847        });
4848    }
4849
4850    /// Every (name wrapper, contained operand) pairing tree-sitter-perl's
4851    /// node-types.json admits, and the single source of truth for both
4852    /// halves of #1355's guard: its parent set is the distinct first
4853    /// components, the kinds it subsumes the distinct second ones.
4854    /// `perl_name_wrappers_bill_the_name_once_1355` witnesses every row
4855    /// and fails on an eleventh pairing.
4856    #[cfg(feature = "perl")]
4857    const PERL_NAME_WRAPPER_PAIRINGS: [(Perl, Perl); 10] = [
4858        (Perl::PackageName, Perl::Identifier),
4859        (Perl::PackageName, Perl::ScalarVariable),
4860        (Perl::PackageName, Perl::ArrayVariable),
4861        (Perl::PackageName, Perl::HashVariable),
4862        (Perl::PackageName, Perl::SpecialScalarVariable),
4863        (Perl::PackageName, Perl::Typeglob),
4864        (Perl::PackageName, Perl::PackageVariable),
4865        (Perl::PackageVariable, Perl::PackageName),
4866        (Perl::PackageVariable, Perl::ScalarVariable),
4867        (Perl::Typeglob, Perl::Identifier),
4868    ];
4869
4870    /// The anonymous tokens those wrappers also hold. `::` and `*` are
4871    /// operators (matched above the guard, so they keep that reading);
4872    /// `{` folds into the `{}` glyph and `}` has never been classified
4873    /// at all. Listing them is what lets
4874    /// `perl_name_wrappers_bill_the_name_once_1355` police *every*
4875    /// child rather than only the named ones — the operand arm carries
4876    /// token-shaped kinds too (`True`, `FILE`, `SUB`, …), and a bump
4877    /// that let one of those inside a wrapper would otherwise be
4878    /// silenced with nothing failing.
4879    #[cfg(feature = "perl")]
4880    const PERL_NAME_WRAPPER_TOKENS: [Perl; 4] =
4881        [Perl::COLONCOLON, Perl::STAR, Perl::LBRACE, Perl::RBRACE];
4882
4883    /// The occurrences #1355's guard suppresses in `source`, paired
4884    /// with the (wrapper kind, child kind) pairings they witness.
4885    ///
4886    /// Walks with `for_each_node_with_chain`, which maintains the
4887    /// ancestor chain exactly as `spaces::compute` does, so "parent"
4888    /// here means what `Ancestors::parent` means inside the guard
4889    /// rather than what a differently-built chain would say.
4890    #[cfg(feature = "perl")]
4891    fn perl_subsumed_operands(source: &str) -> (Vec<String>, HashSet<(u16, u16)>) {
4892        let wrappers: HashSet<u16> = PERL_NAME_WRAPPER_PAIRINGS
4893            .map(|(wrapper, _)| wrapper as u16)
4894            .into();
4895        let subsumed: HashSet<u16> = PERL_NAME_WRAPPER_PAIRINGS
4896            .map(|(_, child)| child as u16)
4897            .into();
4898        let tokens: HashSet<u16> = PERL_NAME_WRAPPER_TOKENS.map(|kind| kind as u16).into();
4899        let code = source.as_bytes();
4900        let mut hidden = Vec::new();
4901        let mut pairings = HashSet::new();
4902        for_each_node_with_chain::<PerlCode>(code, |node, chain| {
4903            let Some(parent) = chain.last() else { return };
4904            if !wrappers.contains(&parent.kind_id()) {
4905                return;
4906            }
4907            assert!(
4908                subsumed.contains(&node.kind_id()) || tokens.contains(&node.kind_id()),
4909                "`{source}`: a `{}` inside a `{}` is a child this guard was not \
4910                 derived against; re-read node-types.json before trusting it",
4911                node.kind(),
4912                parent.kind(),
4913            );
4914            if subsumed.contains(&node.kind_id()) {
4915                pairings.insert((parent.kind_id(), node.kind_id()));
4916                hidden.push(
4917                    node.utf8_text(code)
4918                        .expect("fixture is valid UTF-8")
4919                        .to_owned(),
4920                );
4921            }
4922        });
4923        (hidden, pairings)
4924    }
4925
4926    /// One row of `perl_name_wrappers_bill_the_name_once_1355`'s
4927    /// table: a fixture, what it must measure now, what it measured
4928    /// before #1355, and the operand text behind the counts.
4929    struct PerlNameWrapperCase {
4930        source: &'static str,
4931        /// `[n1, N1, n2, N2]` with the guard in place.
4932        counts: [u64; 4],
4933        /// `[n2, N2]` without it. Re-derived by the loop rather than
4934        /// trusted, so a stale row fails instead of misinforming.
4935        before: [u64; 2],
4936        operands: &'static [&'static str],
4937    }
4938
4939    /// Regression for #1355. `package_name`, `package_variable` and
4940    /// `typeglob` are operands spanning a whole name, so every
4941    /// operand-classified node *inside* one was billed a second time:
4942    /// `use strict;` scored `N2` 2 for one name, `our $Foo::count = 3;`
4943    /// n2 5 / N2 6 for two, and the vocabulary grew a bare `::` entry
4944    /// because a `package_variable`'s qualifier slot is itself a
4945    /// childless `package_name`.
4946    ///
4947    /// Each row's `before` column is what it measured without the
4948    /// guard, and the loop re-derives both halves from the current
4949    /// parse rather than trusting the column — the guard is the only
4950    /// difference between the two classifications, so the occurrences
4951    /// it removes are exactly the subsumed-kind children of a wrapper:
4952    ///
4953    /// - `N2` before minus `N2` after must equal how many of those
4954    ///   there are. That identity *is* the defect: one spurious operand
4955    ///   per contained name part.
4956    /// - `n2` before is the post-fix vocabulary unioned with their
4957    ///   texts. It exceeds `n2` after wherever a part's spelling is not
4958    ///   already an operand on its own (`Data`, `::`, `count`).
4959    ///
4960    /// The walk doubles as the grammar-dispatch §1 / §2 drift marker.
4961    /// It asserts that every *named* child of a wrapper is one of the
4962    /// eight subsumed kinds — which is what makes keying the arm on the
4963    /// parent alone safe — and that all ten pairings node-types.json
4964    /// admits are exercised here, so a bump that renumbers or re-parents
4965    /// one fails loudly instead of leaving the counts below measuring a
4966    /// construct the arm no longer reaches.
4967    ///
4968    /// Two mutants this does *not* catch, measured rather than assumed.
4969    /// Widening the guard from parent- to ancestor-scoped fails nothing,
4970    /// for the reason `perl_division_emits_no_slash_token` already
4971    /// records about the other guard in this getter: every operand-kinded
4972    /// descendant of a wrapper is also a direct child of one, and the
4973    /// intervening sigil tokens are matched by the operator arm above
4974    /// before the guard is reached. Parent-scoping stands on
4975    /// grammar-dispatch §5 and on symmetry with that guard, not on a
4976    /// test. What *is* pinned is the arm's position: moving it above the
4977    /// operator arm swallows `::`, `*` and the typeglob's opening brace,
4978    /// and the operator columns below fail.
4979    #[cfg(feature = "perl")]
4980    #[test]
4981    fn perl_name_wrappers_bill_the_name_once_1355() {
4982        let cases: [PerlNameWrapperCase; 12] = [
4983            // identifier under package_name, the single-segment form.
4984            PerlNameWrapperCase {
4985                source: "use strict;\n",
4986                counts: [2, 2, 1, 1],
4987                before: [1, 2],
4988                operands: &["strict"],
4989            },
4990            // …and the multi-segment one, twice over.
4991            PerlNameWrapperCase {
4992                source: "require Data::Dumper;\n",
4993                counts: [3, 3, 1, 1],
4994                before: [3, 3],
4995                operands: &["Data::Dumper"],
4996            },
4997            PerlNameWrapperCase {
4998                source: "package Foo::Bar;\n",
4999                counts: [3, 3, 1, 1],
5000                before: [3, 3],
5001                operands: &["Foo::Bar"],
5002            },
5003            // The `bar` of a qualified call is a *sibling* of the
5004            // `package_name`, not a child, so it still counts while the
5005            // `Foo` inside the wrapper does not. That is the row saying
5006            // the guard reads position and not kind: keying it on the
5007            // child kinds instead fails 14 tests here, this one among
5008            // them.
5009            PerlNameWrapperCase {
5010                source: "Foo::bar();\n",
5011                counts: [3, 3, 2, 2],
5012                before: [2, 3],
5013                operands: &["Foo", "bar"],
5014            },
5015            // identifier under typeglob, bare and brace-delimited.
5016            PerlNameWrapperCase {
5017                source: "my $g = *STDOUT;\n",
5018                counts: [5, 5, 2, 2],
5019                before: [3, 3],
5020                operands: &["$g", "*STDOUT"],
5021            },
5022            PerlNameWrapperCase {
5023                source: "my $t = *{Foo};\n",
5024                counts: [6, 6, 2, 2],
5025                before: [3, 3],
5026                operands: &["$t", "*{Foo}"],
5027            },
5028            // package_name and scalar_variable under package_variable,
5029            // and scalar_variable under package_name — the reported
5030            // fixture, where `$Foo` was billed twice and `::` once.
5031            PerlNameWrapperCase {
5032                source: "our $Foo::count = 3;\n",
5033                counts: [4, 4, 2, 2],
5034                before: [5, 6],
5035                operands: &["$Foo::count", "3"],
5036            },
5037            // array_variable / hash_variable / special_scalar_variable
5038            // under package_name: the same shape with the other sigils.
5039            PerlNameWrapperCase {
5040                source: "my @l = @Foo::list;\n",
5041                counts: [3, 3, 2, 2],
5042                before: [5, 6],
5043                operands: &["@l", "@Foo::list"],
5044            },
5045            PerlNameWrapperCase {
5046                source: "my %h = %Foo::hash;\n",
5047                counts: [3, 3, 2, 2],
5048                before: [5, 6],
5049                operands: &["%h", "%Foo::hash"],
5050            },
5051            PerlNameWrapperCase {
5052                source: "my $z = $_::x;\n",
5053                counts: [4, 5, 2, 2],
5054                before: [5, 6],
5055                operands: &["$z", "$_::x"],
5056            },
5057            // typeglob under package_name.
5058            PerlNameWrapperCase {
5059                source: "*Foo::glob = 1;\n",
5060                counts: [3, 3, 2, 2],
5061                before: [6, 7],
5062                operands: &["*Foo::glob", "1"],
5063            },
5064            // package_variable under package_name: the nesting that
5065            // makes qualifier depth unbounded. One variable reference
5066            // used to spell seven vocabulary entries.
5067            PerlNameWrapperCase {
5068                source: "my $x = $Foo::Bar::baz;\n",
5069                counts: [4, 5, 2, 2],
5070                before: [7, 10],
5071                operands: &["$x", "$Foo::Bar::baz"],
5072            },
5073        ];
5074
5075        let mut witnessed: HashSet<(u16, u16)> = HashSet::new();
5076        for PerlNameWrapperCase {
5077            source,
5078            counts,
5079            before: [n2_before, total_before],
5080            operands,
5081        } in cases
5082        {
5083            let (hidden, pairings) = perl_subsumed_operands(source);
5084            assert!(
5085                !hidden.is_empty(),
5086                "row {source:?} contains no name-wrapper child, so it witnesses nothing",
5087            );
5088            witnessed.extend(pairings);
5089
5090            // Phrased as an addition rather than a subtraction so a
5091            // future edit that inverts the two underflows nothing and
5092            // fails with the message below.
5093            assert_eq!(
5094                total_before,
5095                counts[3] + hidden.len() as u64,
5096                "row {source:?} must shed exactly one operand per contained \
5097                 name part; recorded N2_before {total_before}, parts {hidden:?}",
5098            );
5099            let mut vocabulary: HashSet<&str> = operands.iter().copied().collect();
5100            vocabulary.extend(hidden.iter().map(String::as_str));
5101            assert_eq!(
5102                vocabulary.len() as u64,
5103                n2_before,
5104                "row {source:?}: n2 before the fix is the post-fix vocabulary \
5105                 plus the contained name parts; got {vocabulary:?}",
5106            );
5107
5108            assert_halstead_counts::<PerlParser>(source, "foo.pl", counts, source);
5109            assert_ops_operands::<PerlParser>(source, "foo.pl", operands.len(), operands.to_vec());
5110        }
5111
5112        let mut got: Vec<(u16, u16)> = witnessed.into_iter().collect();
5113        got.sort_unstable();
5114        let mut expected_pairings: Vec<(u16, u16)> = PERL_NAME_WRAPPER_PAIRINGS
5115            .map(|(wrapper, child)| (wrapper as u16, child as u16))
5116            .into();
5117        expected_pairings.sort_unstable();
5118        assert_eq!(
5119            got, expected_pairings,
5120            "the table must exercise every (wrapper, child) pairing \
5121             node-types.json admits, and no other",
5122        );
5123    }
5124
5125    /// The over-suppression half of #1355 (grammar-dispatch §6 and §11).
5126    /// The guard is keyed on the parent, so the same kinds it silences
5127    /// inside a name wrapper have to keep counting everywhere else —
5128    /// otherwise "one operand per name" would have been bought by
5129    /// zeroing ordinary variables and calls.
5130    ///
5131    /// `module_name` rides along because the issue asserted it shares
5132    /// the `identifier` leaf and would be collateral damage. It does
5133    /// not: `use 'Foo.pm'` parses to a leaf holding only its two quote
5134    /// tokens, so it wraps nothing and is untouched either way.
5135    #[cfg(feature = "perl")]
5136    #[test]
5137    fn perl_qw_list_bills_one_operand_per_element() {
5138        // `qw(a b c)` was invisible to Halstead — neither the elements,
5139        // the wrapper nor the `qw` keyword had an arm — so it billed
5140        // nothing where its synonym `("a", "b", "c")` billed three
5141        // operands. Each `list_item` is now one operand and the
5142        // `word_list_qw` wrapper is gated on holding one, the #1353
5143        // Ruby `%w[]` rule: one operand per element, or one for the
5144        // empty literal.
5145        assert_ops_operands::<PerlParser>(
5146            "my @a = qw(a b c);\n",
5147            "foo.pl",
5148            4,
5149            vec!["@a", "a", "b", "c"],
5150        );
5151        assert_ops_operands::<PerlParser>("my @a = qw();\n", "foo.pl", 2, vec!["@a", "qw()"]);
5152        assert_ops_operands::<PerlParser>(
5153            "use POSIX qw(floor ceil);\n",
5154            "foo.pl",
5155            3,
5156            vec!["POSIX", "floor", "ceil"],
5157        );
5158        // expected: [n1, N1, n2, N2] = [3, 3, 4, 4] for every delimiter —
5159        // operators `my`, `=`, `;`; the `qw` keyword and its delimiters
5160        // are unclassified, as Ruby's `%w[` is, so the choice of
5161        // delimiter cannot move the score (#1312).
5162        for spelling in [
5163            "qw(a b c)",
5164            "qw/a b c/",
5165            "qw{a b c}",
5166            "qw[a b c]",
5167            "qw<a b c>",
5168        ] {
5169            assert_halstead_counts::<PerlParser>(
5170                &format!("my @a = {spelling};\n"),
5171                "foo.pl",
5172                [3, 3, 4, 4],
5173                spelling,
5174            );
5175        }
5176        // The synonym: the same four operands, plus the `()` and `,`
5177        // operators the list spelling carries.
5178        assert_halstead_counts::<PerlParser>(
5179            "my @a = (\"a\", \"b\", \"c\");\n",
5180            "foo.pl",
5181            [5, 6, 4, 4],
5182            "list literal",
5183        );
5184    }
5185
5186    #[cfg(feature = "perl")]
5187    #[test]
5188    fn perl_qualified_name_leaves_still_count_elsewhere_1355() {
5189        // expected: operators `my` × 4, `$` × 3 (one per `$`-sigilled
5190        // variable), `=` × 4, `;` × 5, `()` × 3 and the fat comma
5191        // → n1 = 6, N1 = 20. Operands are the four declared variables,
5192        // the three integers, the hash key, `$_` and the call target
5193        // → n2 = N2 = 10. Five of the eight kinds the guard silences
5194        // under a name wrapper appear among them — `scalar_variable`,
5195        // `array_variable`, `hash_variable`, `special_scalar_variable`
5196        // and `identifier` — and all five still count here.
5197        let bare = "my $x = 1;\nmy @a = (2);\nmy %h = (k => 3);\nmy $u = $_;\nfoo();\n";
5198        assert_halstead_counts::<PerlParser>(bare, "foo.pl", [6, 20, 10, 10], bare);
5199        assert_ops_operands::<PerlParser>(
5200            bare,
5201            "foo.pl",
5202            10,
5203            vec!["$x", "1", "@a", "2", "%h", "k", "3", "$u", "$_", "foo"],
5204        );
5205        let (hidden, _) = perl_subsumed_operands(bare);
5206        assert!(
5207            hidden.is_empty(),
5208            "no name wrapper appears here, so the guard must be inert; got {hidden:?}",
5209        );
5210
5211        // expected: operators `use`, `;` → n1 = N1 = 2; the quoted
5212        // module name is the sole operand → n2 = N2 = 1.
5213        let quoted = "use 'Some.pm';\n";
5214        assert_halstead_counts::<PerlParser>(quoted, "foo.pl", [2, 2, 1, 1], quoted);
5215        assert_ops_operands::<PerlParser>(quoted, "foo.pl", 1, vec!["'Some.pm'"]);
5216        assert!(
5217            ast_has_kind_id(
5218                &PerlParser::new(quoted.as_bytes().to_vec(), &PathBuf::from("foo.pl"), None),
5219                Perl::ModuleName as u16,
5220            ),
5221            "the quoted `use` form no longer parses to `module_name`, so this \
5222             row no longer says anything about it",
5223        );
5224    }
5225
5226    #[cfg(feature = "lua")]
5227    #[test]
5228    fn lua_operators_and_operands() {
5229        check_metrics::<LuaParser>(
5230            "local function add(a, b)
5231  local result = a + b
5232  if result > 0 then
5233    return result
5234  end
5235  return 0
5236end",
5237            "foo.lua",
5238            |metric| {
5239                // n1=11: local,function,(,,,=,+,if,>,then,return,end
5240                // (after #695 the `)` closer no longer counts — only the
5241                // folded `(` opener does; was n1=12).
5242                // n2=5: add,a,b,result,0
5243                insta::assert_json_snapshot!(metric.halstead, @r#"
5244                {
5245                  "unique_operators": 11,
5246                  "total_operators": 14,
5247                  "unique_operands": 5,
5248                  "total_operands": 10,
5249                  "length": 24,
5250                  "estimated_program_length": 49.66338827944708,
5251                  "purity_ratio": 2.0693078449769615,
5252                  "vocabulary": 16,
5253                  "volume": 96.0,
5254                  "difficulty": 11.0,
5255                  "level": 0.09090909090909091,
5256                  "effort": 1056.0,
5257                  "time": 58.666666666666664,
5258                  "bugs": 0.03456644293839657
5259                }
5260                "#);
5261            },
5262        );
5263    }
5264
5265    /// Regression for #695. Lua/Bash/Tcl/iRules/PHP/Ruby/Elixir used to
5266    /// classify the *closing* delimiter (`)`/`]`/`}`) as a separate
5267    /// operator, while the C-family majority folds each balanced pair to a
5268    /// single glyph via `get_operator_id_as_str` and counts only the
5269    /// opener. A balanced `(1)` therefore double-counted as `()` + `)`,
5270    /// inflating n1 and N1. With the fix only the folded `(` opener counts:
5271    /// `local x = (1)` yields operators `local`, `=`, `()` — n1 = N1 = 3,
5272    /// with no standalone `)`.
5273    #[cfg(feature = "lua")]
5274    #[test]
5275    fn lua_balanced_paren_counts_opener_only() {
5276        let source = "local x = (1)\n";
5277        let path = PathBuf::from("foo.lua");
5278        let parser = LuaParser::new(source.as_bytes().to_vec(), &path, None);
5279        let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
5280        let paren = ops.operators.iter().filter(|o| o.as_str() == "()").count();
5281        assert_eq!(
5282            paren, 1,
5283            "balanced `(1)` must be one `()` operator; operators were {:?}",
5284            ops.operators
5285        );
5286        assert!(
5287            !ops.operators.iter().any(|o| o.as_str() == ")"),
5288            "the closing `)` must not be a separate operator; operators were {:?}",
5289            ops.operators
5290        );
5291    }
5292
5293    /// Guard for #768. Several `get_op_type` impls (Cpp/C/Objc/Mozcpp/
5294    /// Tcl/iRules/Php/Elixir/Ruby) classify a grammar's *second-alias*
5295    /// opener — `LPAREN2`, and for Elixir/Ruby `LBRACK2`/`LBRACK3` — as a
5296    /// Halstead operator alongside the base `LPAREN`/`LBRACK`. #768 worried
5297    /// that an alias opener would reach `compute_halstead` with a kind_id
5298    /// distinct from the base, inflating n1 (a second `()` entry) and
5299    /// rendering a bare `"("` instead of the folded `"()"`.
5300    ///
5301    /// That cannot happen: tree-sitter's runtime collapses each alias to
5302    /// its base via the grammar's `public_symbol_map` *before*
5303    /// `Node::kind_id()` (`ts_node_symbol`) ever returns. So the alias
5304    /// kind_id is unobservable to the metric layer and the alias match arms
5305    /// are defensive — they only fire if a future grammar bump drops that
5306    /// collapse. This test pins the invariant: parsing the exact
5307    /// constructs each grammar produces the alias for internally
5308    /// (pp-conditional `defined(...)` for Cpp; call arg-list / subscript /
5309    /// constant-array-pattern for Ruby) must yield **no** node carrying the
5310    /// alias kind_id, and the balanced opener must count once and render as
5311    /// the pair glyph. If a grammar bump makes an alias id observable, this
5312    /// goes red and signals that the alias arms must additionally fold to
5313    /// the base in `get_operator_id_as_str` (the fix #768 proposed).
5314    #[cfg(all(feature = "c", feature = "cpp", feature = "elixir", feature = "ruby"))]
5315    #[test]
5316    fn second_alias_opener_collapses_to_base_kind_id() {
5317        #[cfg(any(feature = "c", feature = "cpp", feature = "elixir", feature = "ruby"))]
5318        fn assert_no_alias<T: crate::ParserTrait>(
5319            source: &str,
5320            file: &str,
5321            alias_id: u16,
5322            alias_name: &str,
5323        ) {
5324            let path = PathBuf::from(file);
5325            let parser = T::new(source.as_bytes().to_vec(), &path, None);
5326            let mut stack = vec![parser.root()];
5327            while let Some(node) = stack.pop() {
5328                assert_ne!(
5329                    node.kind_id(),
5330                    alias_id,
5331                    "{alias_name} (kind_id {alias_id}) must never reach kind_id() \
5332                     for `{source}`; the runtime public_symbol_map should have \
5333                     collapsed it to the base opener. If this fires after a \
5334                     grammar bump, fold {alias_name} to its pair glyph in \
5335                     get_operator_id_as_str (issue #768)."
5336                );
5337                for child in node.children() {
5338                    stack.push(child);
5339                }
5340            }
5341        }
5342
5343        // Balanced openers must count once and render folded (no bare
5344        // `(`/`[`, no n1 inflation) — the property #768 feared was broken.
5345        #[cfg(any(feature = "c", feature = "cpp", feature = "elixir", feature = "ruby"))]
5346        fn assert_folded_openers<T: crate::MetricSuite>(source: &str, file: &str) {
5347            let path = PathBuf::from(file);
5348            let parser = T::new(source.as_bytes().to_vec(), &path, None);
5349            let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
5350            assert!(
5351                !ops.operators.iter().any(|o| o.as_str() == "("),
5352                "no bare `(` operator (must fold to `()`); operators were {:?}",
5353                ops.operators
5354            );
5355            assert!(
5356                !ops.operators.iter().any(|o| o.as_str() == "["),
5357                "no bare `[` operator (must fold to `[]`); operators were {:?}",
5358                ops.operators
5359            );
5360            // Each pair glyph appears at most once — the alias does not add
5361            // a second `()`/`[]` entry to n1.
5362            assert!(
5363                ops.operators.iter().filter(|o| o.as_str() == "()").count() <= 1,
5364                "`()` must be a single n1 entry; operators were {:?}",
5365                ops.operators
5366            );
5367            assert!(
5368                ops.operators.iter().filter(|o| o.as_str() == "[]").count() <= 1,
5369                "`[]` must be a single n1 entry; operators were {:?}",
5370                ops.operators
5371            );
5372        }
5373
5374        // Cpp/C/Mozcpp: LPAREN2 = 20. The grammar emits it internally only
5375        // inside preprocessor-conditional expressions (`#if defined(FOO)`).
5376        assert_no_alias::<crate::CppParser>(
5377            "#if defined(FOO)\n#endif\n",
5378            "a.cpp",
5379            20,
5380            "Cpp::LPAREN2",
5381        );
5382        assert_no_alias::<crate::CParser>("#if defined(FOO)\n#endif\n", "a.c", 20, "C::LPAREN2");
5383
5384        // Ruby: LPAREN2 = 47 (call arg-list), LBRACK3 = 155 (element-
5385        // reference subscript), LBRACK2 = 46 (constant array pattern).
5386        assert_no_alias::<crate::RubyParser>("f(1)\n", "a.rb", 47, "Ruby::LPAREN2");
5387        assert_no_alias::<crate::RubyParser>("a[0]\n", "a.rb", 155, "Ruby::LBRACK3");
5388        assert_no_alias::<crate::RubyParser>(
5389            "case p\nin Point[1, 2] then 1\nend\n",
5390            "a.rb",
5391            46,
5392            "Ruby::LBRACK2",
5393        );
5394
5395        // Elixir: LPAREN2 = 95 (immediate call paren), LBRACK2 = 96
5396        // (access / subscript).
5397        assert_no_alias::<crate::ElixirParser>("f(1)\n", "a.ex", 95, "Elixir::LPAREN2");
5398        assert_no_alias::<crate::ElixirParser>("x[0]\n", "a.ex", 96, "Elixir::LBRACK2");
5399
5400        assert_folded_openers::<crate::CppParser>("int main(){ int a[3]; return a[0]; }", "b.cpp");
5401        assert_folded_openers::<crate::RubyParser>("f(1)\nb = [1]\nb[0]\n", "b.rb");
5402    }
5403
5404    #[cfg(feature = "kotlin")]
5405    #[test]
5406    fn kotlin_halstead_basic() {
5407        check_metrics::<KotlinParser>(
5408            "fun add(a: Int, b: Int): Int {
5409                val result = a + b
5410                return result
5411            }",
5412            "foo.kt",
5413            |metric| {
5414                insta::assert_json_snapshot!(
5415                    metric.halstead,
5416                    @r#"
5417                {
5418                  "unique_operators": 9,
5419                  "total_operators": 11,
5420                  "unique_operands": 5,
5421                  "total_operands": 10,
5422                  "length": 21,
5423                  "estimated_program_length": 40.13896548741762,
5424                  "purity_ratio": 1.9113793089246487,
5425                  "vocabulary": 14,
5426                  "volume": 79.9544533632097,
5427                  "difficulty": 9.0,
5428                  "level": 0.1111111111111111,
5429                  "effort": 719.5900802688873,
5430                  "time": 39.97722668160485,
5431                  "bugs": 0.026767153565498338
5432                }
5433                "#
5434                );
5435            },
5436        );
5437    }
5438
5439    #[cfg(feature = "kotlin")]
5440    #[test]
5441    fn kotlin_string_template_no_double_count() {
5442        // Re-anchored for issue #454. The pre-#454 comment claimed
5443        // kotlin-ng emits an `identifier` node for the short `$name`
5444        // form whose bytes include the leading `$`. That is factually
5445        // false: AST dump shows the short form produces bare
5446        // `string_content` tokens (`$`, then `name`) with **no**
5447        // structured node. The old assertion (u_operands = 4, N2 = 5)
5448        // passed for the wrong reason (lesson 6): the wrapping literal
5449        // was counted (+1) and the inner `name` was dropped (-1), and
5450        // the two errors cancelled. The `$name!` it used also defeats
5451        // recovery because the grammar glues the trailing `!` onto the
5452        // name token.
5453        //
5454        // Correct mechanism (clean end-of-segment short form):
5455        // `fun greet(name: String): String {\n    return "Hi $name"\n}\n`
5456        //   operators: fun, (, ), :, {}, return → as classified.
5457        //   operands by token text:
5458        //     `greet` × 1, `name` × 2 (param + recovered short-interp),
5459        //     `String` × 2 (param type + return type).
5460        //   The wrapping `"Hi $name"` literal is suppressed and the
5461        //   inner `name` recovered → u_operands = 3 (`greet`, `name`,
5462        //   `String`), N2 = 5. Pre-#454: wrapper counted, inner dropped
5463        //   → u_operands = 4, N2 = 6.
5464        check_metrics::<KotlinParser>(
5465            "fun greet(name: String): String {\n    return \"Hi $name\"\n}\n",
5466            "foo.kt",
5467            |metric| {
5468                assert_eq!(metric.halstead.unique_operands(), 3);
5469                assert_eq!(metric.halstead.total_operands(), 5);
5470            },
5471        );
5472        // Lesson 4: the ops store agrees on n2 and the exact operand set
5473        // (inner `name` present, wrapper absent).
5474        assert_ops_operands::<KotlinParser>(
5475            "fun greet(name: String): String {\n    return \"Hi $name\"\n}\n",
5476            "foo.kt",
5477            3,
5478            vec!["greet", "name", "String"],
5479        );
5480    }
5481
5482    #[cfg(feature = "kotlin")]
5483    #[test]
5484    fn kotlin_short_interpolation_counts_inner_not_wrapper() {
5485        // Issue #454: the short `$name` template — distinct from the
5486        // long `${expr}` form, which the kotlin-ng grammar gives a
5487        // structured `interpolation` node (see
5488        // `kotlin_string_template_long_form_no_double_count`). The short
5489        // form has no such node; the variable arrives as a bare
5490        // `string_content` token preceded by a `$` `string_content`.
5491        // The fix recovers the clean-identifier variable as an operand
5492        // and suppresses the opaque wrapper.
5493        //
5494        // `fun f() { val x = 1; println("v=$x") }\n`
5495        //   operands by token text: `f`, `x` × 2 (decl + recovered),
5496        //   `println`, `1`. The wrapping `"v=$x"` is suppressed →
5497        //   u_operands = 4 (`f`, `x`, `println`, `1`), N2 = 5.
5498        // Pre-#454 the wrapper `"v=$x"` counted and the inner `x` was
5499        // dropped → u_operands = 4 but the wrapper, not `x`, was the
5500        // fourth operand, and N2 = 5 with the wrong member — the ops
5501        // assertion below pins the exact set so the cancellation cannot
5502        // hide it.
5503        let src = "fun f() { val x = 1; println(\"v=$x\") }\n";
5504        check_metrics::<KotlinParser>(src, "foo.kt", |metric| {
5505            assert_eq!(metric.halstead.unique_operands(), 4);
5506            assert_eq!(metric.halstead.total_operands(), 5);
5507        });
5508        assert_ops_operands::<KotlinParser>(src, "foo.kt", 4, vec!["f", "x", "println", "1"]);
5509    }
5510
5511    #[cfg(feature = "kotlin")]
5512    #[test]
5513    fn kotlin_short_interpolation_space_separated() {
5514        // Issue #454 follow-up: tree-sitter-kotlin-ng splits the literal
5515        // only at each `$`, so a `$name` segment's name token absorbs any
5516        // trailing inter-segment text into its byte range. For `"$a $b"`
5517        // the token after the first `$` is `"a "` (with the trailing
5518        // space). Pre-fix `kotlin_is_identifier("a ")` returned false and
5519        // the leading variable `a` was silently dropped, yielding
5520        // operands `{b, f, s}` (verified: `a` missing) — breaking parity
5521        // with the long form `"${a} ${b}"`, which recovers `{a, b, f, s}`.
5522        //
5523        // The fix takes the maximal leading-identifier prefix of the name
5524        // token, recovering `a` and keying it as the bare `"a"` (not
5525        // `"a "`). Short and long forms must now agree exactly.
5526        //
5527        // `fun f() { val s = "$a $b" }\n`
5528        //   operands by token text: `f`, `s`, `a` (recovered), `b`
5529        //   (recovered). Wrapper suppressed → u_operands = 4, N2 = 4.
5530        let short = "fun f() { val s = \"$a $b\" }\n";
5531        let long = "fun f() { val s = \"${a} ${b}\" }\n";
5532        check_metrics::<KotlinParser>(short, "foo.kt", |metric| {
5533            assert_eq!(metric.halstead.unique_operands(), 4);
5534            assert_eq!(metric.halstead.total_operands(), 4);
5535        });
5536        // Both `a` and `b` present, wrapper absent, n2 == dedupe(operands).
5537        assert_ops_operands::<KotlinParser>(short, "foo.kt", 4, vec!["f", "s", "a", "b"]);
5538        // Exact parity with the long `${a} ${b}` form.
5539        assert_ops_operands::<KotlinParser>(long, "foo.kt", 4, vec!["f", "s", "a", "b"]);
5540
5541        // Comma after the name (`"$a, $b"`): the first name token is
5542        // `"a, "`; its leading identifier prefix is `a`.
5543        let comma = "fun f() { val s = \"$a, $b\" }\n";
5544        assert_ops_operands::<KotlinParser>(comma, "foo.kt", 4, vec!["f", "s", "a", "b"]);
5545
5546        // Name preceded by literal text and at end-of-segment (`"x=$a"`):
5547        // the `a` token has no trailing text, so recovery is unchanged.
5548        let prefixed = "fun f() { val s = \"x=$a\" }\n";
5549        assert_ops_operands::<KotlinParser>(prefixed, "foo.kt", 3, vec!["f", "s", "a"]);
5550
5551        // Mid-prose `"$x is "`: the name token is `"x is "`. The leading
5552        // identifier prefix is `x`, matching the long form `"${x} is "`,
5553        // which also recovers `x` and treats `" is "` as literal text.
5554        let prose_short = "fun f() { val s = \"$x is \" }\n";
5555        let prose_long = "fun f() { val s = \"${x} is \" }\n";
5556        assert_ops_operands::<KotlinParser>(prose_short, "foo.kt", 3, vec!["f", "s", "x"]);
5557        assert_ops_operands::<KotlinParser>(prose_long, "foo.kt", 3, vec!["f", "s", "x"]);
5558    }
5559
5560    #[cfg(feature = "kotlin")]
5561    #[test]
5562    fn kotlin_dollar_non_identifier_stays_literal() {
5563        // Issue #454 boundary: a `$` not followed by a clean identifier
5564        // is literal text, not an interpolation. `"price: $5"` (digit
5565        // after `$`) must keep the wrapping literal as a single operand
5566        // and recover nothing.
5567        //
5568        // `fun f() { val a = "price: $5" }\n`
5569        //   operands: `f`, `a`, `"price: $5"` → u_operands = 3, N2 = 3.
5570        let src = "fun f() { val a = \"price: $5\" }\n";
5571        check_metrics::<KotlinParser>(src, "foo.kt", |metric| {
5572            assert_eq!(metric.halstead.unique_operands(), 3);
5573            assert_eq!(metric.halstead.total_operands(), 3);
5574        });
5575        assert_ops_operands::<KotlinParser>(src, "foo.kt", 3, vec!["f", "a", "\"price: $5\""]);
5576    }
5577
5578    #[cfg(feature = "kotlin")]
5579    #[test]
5580    fn kotlin_string_template_long_form_no_double_count() {
5581        // The `${expr}` long form of a Kotlin string template also
5582        // produces an `Interpolation` child. The fix must apply to it
5583        // identically.
5584        //
5585        // Source: `fun f(x: Int): String { return "v=${x}" }\n`
5586        // Operands by source-byte key:
5587        //   `f` × 1, `x` × 2 (param + inside `${x}`),
5588        //   `Int` × 1, `String` × 1.
5589        // With the fix u_operands = 4 (`f`, `x`, `Int`, `String`),
5590        // N2 = 5. Without the fix the wrapping `"v=${x}"` would also
5591        // count → u_operands = 5, N2 = 6.
5592        check_metrics::<KotlinParser>(
5593            "fun f(x: Int): String { return \"v=${x}\" }\n",
5594            "foo.kt",
5595            |metric| {
5596                assert_eq!(metric.halstead.unique_operands(), 4);
5597                assert_eq!(metric.halstead.total_operands(), 5);
5598            },
5599        );
5600    }
5601
5602    #[cfg(feature = "kotlin")]
5603    #[test]
5604    fn kotlin_plain_string_still_operand() {
5605        // The fix for #191 only skips wrapping templates that contain
5606        // an `Interpolation` child; a plain `"hello"` (no `$` interp)
5607        // must still contribute exactly one operand.
5608        //
5609        // Source: `fun f(): String { return "hello" }\n`
5610        // Operands: `f` × 1, `String` × 1, `"hello"` × 1 →
5611        // u_operands = 3, N2 = 3.
5612        check_metrics::<KotlinParser>(
5613            "fun f(): String { return \"hello\" }\n",
5614            "foo.kt",
5615            |metric| {
5616                assert_eq!(metric.halstead.unique_operands(), 3);
5617                assert_eq!(metric.halstead.total_operands(), 3);
5618            },
5619        );
5620    }
5621
5622    // Kotlin half of #1380 — see
5623    // `java_self_and_super_references_are_operands` for the structural
5624    // argument.
5625    #[cfg(feature = "kotlin")]
5626    #[test]
5627    fn kotlin_self_and_super_references_are_operands() {
5628        let source = "class T {\n    fun f() = this.x + super.y\n}";
5629        // Operators (n1=7, N1=8): class, {}, fun, (), =, . x2, +
5630        // Operands (n2=6, N2=6): T, f, this, x, super, y
5631        // Before the fix this read (9, 10, 4, 4).
5632        assert_halstead_counts::<KotlinParser>(
5633            source,
5634            "foo.kt",
5635            [7, 8, 6, 6],
5636            "kotlin self/super references",
5637        );
5638        assert_keywords_are_operands_only::<KotlinParser>(source, "foo.kt", &["this", "super"]);
5639    }
5640
5641    // The label-qualified spellings are their own leaf kinds — `this@`
5642    // (106) and `super@` (107) — carrying no `this` / `super` leaf, so
5643    // listing only the two bare kinds would score every `this@Outer`
5644    // reference zero (grammar-dispatch section 1). The labels `Outer`
5645    // and `Inner` stay separate operands: the `this_expression` wrapper
5646    // whose span would have swallowed them is deliberately unclassified
5647    // (section 5).
5648    #[cfg(feature = "kotlin")]
5649    #[test]
5650    fn kotlin_labelled_self_and_super_references_are_operands() {
5651        let source = "class Outer {\n    inner class Inner : A() {\n        \
5652                      fun f() = this@Outer.x + super@Inner.y\n    }\n}";
5653        assert_keywords_are_operands_only::<KotlinParser>(source, "foo.kt", &["this@", "super@"]);
5654        assert_ops_operands::<KotlinParser>(
5655            source,
5656            "foo.kt",
5657            8,
5658            vec!["A", "Inner", "Outer", "f", "super@", "this@", "x", "y"],
5659        );
5660        // The metrics store is a second, independent walk — it keys
5661        // operands by `get_operand_id` where `ops_inner` keys by text —
5662        // so `assert_ops_operands` on its own pins the ops store against
5663        // a literal rather than against the metric (lesson 4).
5664        //
5665        // `N2` is what makes the "the labels stay separate operands"
5666        // claim above testable. Both labels duplicate an enclosing class
5667        // name, so dropping their contribution entirely leaves `n2` at 8
5668        // with the same eight strings; only `N2` falls, 10 to 8.
5669        check_metrics::<KotlinParser>(source, "foo.kt", |metric| {
5670            assert_eq!(metric.halstead.unique_operands(), 8);
5671            assert_eq!(metric.halstead.total_operands(), 10);
5672        });
5673    }
5674
5675    // The keeper question of grammar-dispatch section 6: a
5676    // `constructor_delegation_call` emits a bare `this` / `super` leaf
5677    // with no `this_expression` / `super_expression` wrapper around it,
5678    // so classifying the wrapper instead would score constructor
5679    // delegation zero. This is the only spelling that can tell the two
5680    // choices apart — every other `this` carries both nodes — so it is
5681    // the independent path grammar-dispatch section 11 asks for.
5682    #[cfg(feature = "kotlin")]
5683    #[test]
5684    fn kotlin_constructor_delegation_self_reference_is_an_operand() {
5685        let source = "class C(val n: Int) {\n    constructor() : this(0)\n}";
5686        assert_keywords_are_operands_only::<KotlinParser>(source, "foo.kt", &["this"]);
5687        assert_ops_operands::<KotlinParser>(
5688            source,
5689            "foo.kt",
5690            5,
5691            vec!["0", "C", "Int", "n", "this"],
5692        );
5693        check_metrics::<KotlinParser>(source, "foo.kt", |metric| {
5694            assert_eq!(metric.halstead.unique_operands(), 5);
5695            assert_eq!(metric.halstead.total_operands(), 5);
5696        });
5697    }
5698
5699    /// One `this` / `super` per container the pinned
5700    /// `tree-sitter-kotlin-ng` can put them in: a bare expression, a
5701    /// call argument, a navigation receiver, a type-argument-qualified
5702    /// `super<P>`, both label-qualified forms, and the
5703    /// `constructor_delegation_call` that carries no wrapper.
5704    #[cfg(feature = "kotlin")]
5705    const KOTLIN_SELF_POSITIONS: &str = "class P {
5706        fun h() = 1
5707    }
5708    class Outer : P() {
5709        val x = 1
5710        fun c() = super.h()
5711        fun d() = this
5712        fun e(o: Any): Any = e(this)
5713        inner class Inner : P() {
5714            fun a() = this@Outer.x
5715            fun b() = this.hashCode()
5716            fun f() = super@Inner.h()
5717            fun g() = super<P>.h()
5718        }
5719    }
5720    class Del(val n: Int) {
5721        constructor() : this(0)
5722    }
5723";
5724
5725    #[cfg(feature = "kotlin")]
5726    #[test]
5727    fn kotlin_self_and_super_leaves_are_unaliased_and_unconditional() {
5728        assert_self_reference_leaves::<KotlinCode>(
5729            KOTLIN_SELF_POSITIONS,
5730            &[
5731                ("this", Kotlin::This as u16, 4),
5732                ("super", Kotlin::Super as u16, 2),
5733                ("this@", Kotlin::ThisAT as u16, 1),
5734                ("super@", Kotlin::SuperAT as u16, 1),
5735            ],
5736            "kotlin",
5737        );
5738    }
5739
5740    #[cfg(feature = "python")]
5741    #[test]
5742    fn python_fstring_no_double_count() {
5743        // Regression: issue #191. A Python f-string (`f"Hi {name}!"`)
5744        // wraps an `Interpolation` child whose inner identifier
5745        // `name` is walked and counted as its own operand. Without
5746        // the `is_child(Interpolation)` guard the wrapping `String`
5747        // would also count, double-counting `name`'s contribution to
5748        // `N2`. Same pattern as #180 (Bash/Elixir) and #184 (PHP).
5749        //
5750        // Source: `def greet(name):\n    return f"Hi {name}!"\n`
5751        // Operands by source-byte key:
5752        //   `greet` × 1, `name` × 2 (param + inside `{name}`).
5753        // With the fix the wrapping `f"Hi {name}!"` is skipped →
5754        // u_operands = 2 (`greet`, `name`), N2 = 3. Without the fix
5755        // the wrapping literal would also count → u_operands = 3,
5756        // N2 = 4.
5757        check_metrics::<PythonParser>(
5758            "def greet(name):\n    return f\"Hi {name}!\"\n",
5759            "foo.py",
5760            |metric| {
5761                assert_eq!(metric.halstead.unique_operands(), 2);
5762                assert_eq!(metric.halstead.total_operands(), 3);
5763            },
5764        );
5765    }
5766
5767    #[cfg(feature = "python")]
5768    #[test]
5769    fn python_plain_string_still_operand() {
5770        // The fix for #191 only skips wrapping `String` nodes that
5771        // contain an `Interpolation` child; a plain `"hi"` must still
5772        // contribute exactly one operand.
5773        //
5774        // Source: `def f():\n    return "hi"\n`
5775        // Operands: `f` × 1, `"hi"` × 1 → u_operands = 2, N2 = 2.
5776        // (The previous documentation-string filter is preserved:
5777        // a bare `"hi"` as a top-level `expression_statement` would
5778        // be skipped, but here it appears as `return "hi"`.)
5779        check_metrics::<PythonParser>("def f():\n    return \"hi\"\n", "foo.py", |metric| {
5780            assert_eq!(metric.halstead.unique_operands(), 2);
5781            assert_eq!(metric.halstead.total_operands(), 2);
5782        });
5783    }
5784
5785    #[cfg(feature = "python")]
5786    #[test]
5787    fn python_concatenated_docstring_suppressed() {
5788        // Regression for #695. An implicit-concatenation docstring
5789        // (`"""doc""" "more"`) parses as `expression_statement >
5790        // concatenated_string > [string, string]`. The single-literal
5791        // docstring guard (`parent == expression_statement &&
5792        // child_count == 1`) never fired here, so each fragment counted
5793        // as a separate operand and the docstring's N2 contribution
5794        // depended on how many literals it was split into. With the fix,
5795        // every fragment of such a docstring is suppressed.
5796        //
5797        // Source: `def f():\n    """doc""" "more"\n    return 1\n`
5798        // Operands: `f`, `1` only — both docstring fragments suppressed →
5799        // u_operands = 2, N2 = 2.
5800        check_metrics::<PythonParser>(
5801            "def f():\n    \"\"\"doc\"\"\" \"more\"\n    return 1\n",
5802            "foo.py",
5803            |metric| {
5804                assert_eq!(metric.halstead.unique_operands(), 2);
5805                assert_eq!(metric.halstead.total_operands(), 2);
5806            },
5807        );
5808    }
5809
5810    #[cfg(feature = "python")]
5811    #[test]
5812    fn python_concatenated_non_docstring_still_counts() {
5813        // The #695 fix must only suppress concatenated literals in the
5814        // *docstring* position (sole child of an `expression_statement`).
5815        // A concatenated string used as a value (`x = "a" "b"`) is not a
5816        // docstring — its `concatenated_string` parent's grandparent is
5817        // an assignment, not a single-child statement — so both fragments
5818        // must still be operands.
5819        //
5820        // Source: `def f():\n    x = "a" "b"\n    return x\n`
5821        // Operands: `f`, `x` (twice: assign + return), `"a"`, `"b"` →
5822        // u_operands = 4, N2 = 5.
5823        check_metrics::<PythonParser>(
5824            "def f():\n    x = \"a\" \"b\"\n    return x\n",
5825            "foo.py",
5826            |metric| {
5827                assert_eq!(metric.halstead.unique_operands(), 4);
5828                assert_eq!(metric.halstead.total_operands(), 5);
5829            },
5830        );
5831    }
5832
5833    #[cfg(feature = "python")]
5834    #[test]
5835    fn python_empty_file_halstead() {
5836        check_metrics::<PythonParser>("", "empty.py", |metric| {
5837            let h = &metric.halstead;
5838            assert_eq!(h.unique_operators(), 0);
5839            assert_eq!(h.total_operands(), 0);
5840            assert_eq!(h.estimated_program_length(), 0.0);
5841            assert_eq!(h.purity_ratio(), 0.0);
5842            assert_eq!(h.volume(), 0.0);
5843            assert_eq!(h.difficulty(), 0.0);
5844            assert_eq!(h.level(), 0.0);
5845            assert_eq!(h.effort(), 0.0);
5846            assert_eq!(h.time(), 0.0);
5847            assert_eq!(h.bugs(), 0.0);
5848        });
5849    }
5850
5851    /// Regression #413, sub-fix (1): `await` was double-counted because the
5852    /// operator arm listed both the await-expression node (Await=237) and the
5853    /// nested `await` keyword token (Await2=95). Only the node should count,
5854    /// mirroring how `yield` counts only the Yield node.
5855    #[cfg(feature = "python")]
5856    #[test]
5857    fn python_await_counted_once_per_use() {
5858        check_metrics::<PythonParser>(
5859            "async def f():\n    await a()\n    await b()\n    await c()\n",
5860            "foo.py",
5861            |metric| {
5862                // expected operators: async, def, (), :, await  (5 unique)
5863                //   N1: async(1) def(1) await(3) = 5, plus the four call
5864                //   and parameter-list `(` and the block `:` (#1486) = 10.
5865                //   Before #413, Await + Await2 both matched, so `await` was a
5866                //   distinct operator twice: one more unique and three more total.
5867                assert_eq!(metric.halstead.unique_operators(), 5);
5868                assert_eq!(metric.halstead.total_operators(), 10);
5869            },
5870        );
5871    }
5872
5873    /// Regression #413, sub-fix (3): `lambda` was dropped entirely. Only the
5874    /// `lambda` keyword token (Lambda3=73) is classified, not the wrapping
5875    /// Lambda/Lambda2 expression nodes, to avoid an await-style double count.
5876    #[cfg(feature = "python")]
5877    #[test]
5878    fn python_lambda_counted_once() {
5879        check_metrics::<PythonParser>("g = lambda x: x + 1\n", "foo.py", |metric| {
5880            // expected operators: =, lambda, :, +  (4 unique, each used
5881            // once; the parameter-list `:` since #1486).
5882            // Before #413, lambda was absent: only =, + were counted.
5883            assert_eq!(metric.halstead.unique_operators(), 4);
5884            assert_eq!(metric.halstead.total_operators(), 4);
5885        });
5886    }
5887
5888    /// Regression #413, sub-fix (2): `match` / `case` keyword tokens
5889    /// (Match=26, Case=27) were dropped. Each should now count as an operator,
5890    /// matching the cyclomatic metric which already counts every `case`.
5891    #[cfg(feature = "python")]
5892    #[test]
5893    fn python_match_case_counted() {
5894        check_metrics::<PythonParser>(
5895            "match x:\n    case 1:\n        pass\n    case _:\n        pass\n",
5896            "foo.py",
5897            |metric| {
5898                // expected operators: match, case, pass, :  (4 unique)
5899                //   match(1) + case(2) + pass(2) + one block `:` per
5900                //   header (3, #1486) = 8 total occurrences.
5901                // Before #413, neither match nor case was counted (only pass).
5902                assert_eq!(metric.halstead.unique_operators(), 4);
5903                assert_eq!(metric.halstead.total_operators(), 8);
5904            },
5905        );
5906    }
5907
5908    /// Regression #413, sub-fix (2): `nonlocal` (Nonlocal=41) was dropped while
5909    /// `global` was already classified. Both should count, for parity.
5910    #[cfg(feature = "python")]
5911    #[test]
5912    fn python_nonlocal_and_global_counted() {
5913        check_metrics::<PythonParser>(
5914            "def f():\n    global a\n    nonlocal b\n",
5915            "foo.py",
5916            |metric| {
5917                // expected operators: def, (), :, global, nonlocal
5918                //   (5 unique, each once; `()` and `:` since #1486)
5919                // Before #413, nonlocal was absent: only def, global counted.
5920                assert_eq!(metric.halstead.unique_operators(), 5);
5921                assert_eq!(metric.halstead.total_operators(), 5);
5922            },
5923        );
5924    }
5925
5926    /// Regression #413, sub-fix (4): `not in` (Notin=193) and `is not`
5927    /// (Isnot=194) are single compound operators. The parent-guard suppresses
5928    /// the inner Not/In/Is leaves only under those compounds, so standalone
5929    /// `not x`, `a in b`, `a is b`, and `for x in y` still count their leaves.
5930    #[cfg(feature = "python")]
5931    #[test]
5932    fn python_not_in_is_not_counted_as_single_operator() {
5933        check_metrics::<PythonParser>(
5934            "a not in b\na is not b\nnot c\nd in e\nf is g\nfor h in i:\n    pass\n",
5935            "foo.py",
5936            |metric| {
5937                // expected operators (8 unique):
5938                //   "not in" (compound, once), "is not" (compound, once),
5939                //   "not" (standalone `not c`, once),
5940                //   "in" (standalone `d in e` + `for h in i` = twice),
5941                //   "is" (standalone `f is g`, once),
5942                //   "for" (once), "pass" (once), and the `for` header's
5943                //   block ":" (once, #1486)
5944                // Total occurrences: 1+1+1+2+1+1+1+1 = 9.
5945                // Before #413, `a not in b` counted not+in (two) and
5946                // `a is not b` counted is+not (two); the compounds were
5947                // never classified.
5948                assert_eq!(metric.halstead.unique_operators(), 8);
5949                assert_eq!(metric.halstead.total_operators(), 9);
5950            },
5951        );
5952    }
5953
5954    /// Asserts `count(halstead)` for each `(label, source, expected)`
5955    /// row, one parse per row so a row's counts are its own.
5956    #[cfg(any(
5957        feature = "c",
5958        feature = "cpp",
5959        feature = "groovy",
5960        feature = "java",
5961        feature = "javascript",
5962        feature = "kotlin",
5963        feature = "mozcpp",
5964        feature = "mozjs",
5965        feature = "objc",
5966        feature = "python",
5967        feature = "rust",
5968        feature = "typescript"
5969    ))]
5970    fn assert_halstead_rows<T: crate::MetricSuite>(
5971        file: &str,
5972        rows: &[(&str, &str, [u64; 2])],
5973        count: fn(&Stats) -> [u64; 2],
5974    ) {
5975        for (label, source, expected) in rows {
5976            crate::test_support::check_func_space_only::<T, _>(
5977                source,
5978                file,
5979                &[crate::Metric::Halstead],
5980                |space| {
5981                    assert_eq!(
5982                        count(&space.metrics.halstead),
5983                        *expected,
5984                        "{label}: {source:?}"
5985                    );
5986                },
5987            );
5988        }
5989    }
5990
5991    /// Asserts `[n1, N1]` for each row.
5992    #[cfg(any(
5993        feature = "c",
5994        feature = "cpp",
5995        feature = "groovy",
5996        feature = "java",
5997        feature = "javascript",
5998        feature = "kotlin",
5999        feature = "mozcpp",
6000        feature = "mozjs",
6001        feature = "objc",
6002        feature = "python",
6003        feature = "rust",
6004        feature = "typescript"
6005    ))]
6006    fn assert_operator_rows<T: crate::MetricSuite>(file: &str, rows: &[(&str, &str, [u64; 2])]) {
6007        assert_halstead_rows::<T>(file, rows, |h| [h.unique_operators(), h.total_operators()]);
6008    }
6009
6010    /// Asserts `[n2, N2]` for each row.
6011    #[cfg(any(feature = "java", feature = "typescript"))]
6012    fn assert_operand_rows<T: crate::MetricSuite>(file: &str, rows: &[(&str, &str, [u64; 2])]) {
6013        assert_halstead_rows::<T>(file, rows, |h| [h.unique_operands(), h.total_operands()]);
6014    }
6015
6016    /// #1486: Python's bracket openers, `;` and `:` are vocabulary, as
6017    /// they are in every other getter (#1395). Each row isolates one
6018    /// position, so the expected `[n1, N1]` is derivable from the row
6019    /// alone; a bracket pair bills its opener once, as `()` / `[]` / `{}`.
6020    #[cfg(feature = "python")]
6021    #[test]
6022    fn python_punctuation_is_vocabulary_1486() {
6023        const ROWS: &[(&str, &str, [u64; 2])] = &[
6024            // `()`
6025            ("call", "f(x)\n", [1, 1]),
6026            // `()`, `=`
6027            ("keyword argument", "f(a=1)\n", [2, 2]),
6028            // `()`, `*`, `,`, `**`
6029            ("splat arguments", "f(*a, **k)\n", [4, 4]),
6030            // `[]`
6031            ("index", "a[1]\n", [1, 1]),
6032            // `[]`, `:` twice
6033            ("slice", "a[1:2:3]\n", [2, 3]),
6034            // `{}`, `:`
6035            ("dict", "{k: v}\n", [2, 2]),
6036            // `{}`, `,`
6037            ("set", "{1, 2}\n", [2, 2]),
6038            // `[]`, `,` twice, `()`
6039            ("list and tuple", "[1, (2, 3)]\n", [3, 4]),
6040            // `()`, `for`, `in`
6041            ("generator", "(x for x in y)\n", [3, 3]),
6042            // `{}`, `:`, `for`, `in`
6043            ("dict comprehension", "{k: v for k in y}\n", [4, 4]),
6044            // `()`, `:=` — the walrus is its own token, not a `:`
6045            ("walrus", "(n := 3)\n", [2, 2]),
6046            // `lambda`, `:`
6047            ("lambda", "lambda x: x\n", [2, 2]),
6048            // `def`, `()`, `:` twice, `->`, `pass`
6049            ("annotations", "def f(x: int) -> int:\n    pass\n", [5, 6]),
6050            // `:`, `=`
6051            ("annotated assignment", "x: int = 1\n", [2, 2]),
6052            // `@`, `def`, `()`, `:`, `pass`
6053            ("decorator", "@d\ndef f():\n    pass\n", [5, 5]),
6054            // `if`, `:`, `pass`
6055            ("if block", "if x:\n    pass\n", [3, 3]),
6056            // `while`, `:`, `pass`
6057            ("while block", "while x:\n    pass\n", [3, 3]),
6058            // `for`, `in`, `:`, `pass`
6059            ("for block", "for i in y:\n    pass\n", [4, 4]),
6060            // `with`, `:`, `pass`
6061            ("with block", "with o:\n    pass\n", [3, 3]),
6062            // `try`, `except`, `:` twice, `pass` twice
6063            ("try block", "try:\n    pass\nexcept E:\n    pass\n", [4, 6]),
6064            // `class`, `:`, `pass` — the keyword is billed like `def`
6065            // (#1550)
6066            ("class block", "class C:\n    pass\n", [3, 3]),
6067            // `class` twice, `()`, `:` twice, `pass` — one `class` per
6068            // keyword, nothing for the `ClassDefinition` wrapper
6069            (
6070                "nested class",
6071                "class C(B):\n    class D:\n        pass\n",
6072                [4, 6],
6073            ),
6074            // `;`
6075            ("statement separator", "a; b\n", [1, 1]),
6076        ];
6077        assert_operator_rows::<PythonParser>("foo.py", ROWS);
6078    }
6079
6080    /// #1486: an f-string replacement field's `{` and format-spec `:` are
6081    /// spelling, not operations, like every other language's
6082    /// interpolation opener (#1314) — the f-string rows bill nothing.
6083    /// #1551 adds the self-documenting `=` (`f"{z=}"`), a directive of
6084    /// the same field. The guard reads the token's *parent*, so a dict,
6085    /// a keyword argument or a walrus written inside a replacement field
6086    /// keeps its own operators.
6087    #[cfg(feature = "python")]
6088    #[test]
6089    fn python_f_string_delimiters_are_not_operators_1486() {
6090        const ROWS: &[(&str, &str, [u64; 2])] = &[
6091            ("interpolation opener", "f\"{x}\"\n", [0, 0]),
6092            ("format spec colon", "f\"{x:>10}\"\n", [0, 0]),
6093            ("nested format expression", "f\"{a:{w}}\"\n", [0, 0]),
6094            // `{}`, `:`, `[]` of the dict and its subscript, nothing of
6095            // the replacement field around them
6096            ("dict inside a field", "f\"{ {k: v}[k] }\"\n", [3, 3]),
6097            ("self-documenting =", "f\"{z=}\"\n", [0, 0]),
6098            // the `=` keeps its spaces in the output; `!r` and `:` follow it
6099            ("spaced = with conversion", "f\"{z = !r:>10}\"\n", [0, 0]),
6100            // a `=` in a nested format expression (Python 3.12+)
6101            ("nested self-documenting =", "f\"{a:{w=}}\"\n", [0, 0]),
6102            // `()`, `=` — a keyword argument's `=` is a real operator
6103            ("keyword argument inside a field", "f\"{g(a=1)}\"\n", [2, 2]),
6104            // `()`, `:=`
6105            ("walrus inside a field", "f\"{(y := 1)}\"\n", [2, 2]),
6106        ];
6107        assert_operator_rows::<PythonParser>("foo.py", ROWS);
6108    }
6109
6110    /// #1552: a JS-family `class` keyword bills as `function` does, once
6111    /// per keyword. Shared by the four grammars of the macro; each runs
6112    /// its own call so a grammar whose `Class2` stops being the keyword
6113    /// leaf fails alone. Before #1552 every row billed one fewer.
6114    #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
6115    const JS_FAMILY_CLASS_ROWS: &[(&str, &str, [u64; 2])] = &[
6116        // `class`, `{}`
6117        ("declaration", "class C {}\n", [2, 2]),
6118        // `const`, `=`, `class`, `{}`, `;` — the class *expression*
6119        // node wraps the keyword and bills nothing of its own
6120        ("expression", "const D = class {};\n", [5, 5]),
6121        // `class` twice, `extends`, `{}` twice
6122        ("two classes", "class A {}\nclass B extends A {}\n", [3, 5]),
6123    ];
6124
6125    #[cfg(any(feature = "javascript", feature = "mozjs"))]
6126    #[test]
6127    fn js_class_keyword_is_an_operator_1552() {
6128        #[cfg(feature = "javascript")]
6129        assert_operator_rows::<JavascriptParser>("foo.js", JS_FAMILY_CLASS_ROWS);
6130        #[cfg(feature = "mozjs")]
6131        assert_operator_rows::<MozjsParser>("foo.js", JS_FAMILY_CLASS_ROWS);
6132    }
6133
6134    /// #1554: a function *expression* bills its `function` keyword once,
6135    /// as a declaration does — the `function_expression` wrapper bills
6136    /// nothing of its own. Before #1554 the plain and `async` rows billed
6137    /// one extra unique and total operator, and the nested row one extra
6138    /// unique and two extra totals. The declaration and generator rows
6139    /// are controls: neither node was ever in the operator arm.
6140    #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
6141    const JS_FAMILY_FUNCTION_ROWS: &[(&str, &str, [u64; 2])] = &[
6142        // `function`, `()`, `{}`
6143        ("declaration", "function h() {}\n", [3, 3]),
6144        // `const`, `=`, `function`, `()`, `{}`, `;`
6145        ("expression", "const f = function () {};\n", [6, 6]),
6146        // `const`, `=`, `async`, `function`, `()`, `{}`, `;`
6147        (
6148            "async expression",
6149            "const f = async function () {};\n",
6150            [7, 7],
6151        ),
6152        // `const`, `=`, `function`, `*`, `()`, `{}`, `;` — a generator
6153        // expression is its own `generator_function` node, never billed
6154        (
6155            "generator expression",
6156            "const f = function* () {};\n",
6157            [7, 7],
6158        ),
6159        // `const`, `=`, `return`, `;` x2, and `function`, `()`, `{}`
6160        // twice each
6161        (
6162            "nested expressions",
6163            "const f = function () { return function () {}; };\n",
6164            [7, 11],
6165        ),
6166    ];
6167
6168    #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
6169    #[test]
6170    fn js_family_function_expression_bills_function_once_1554() {
6171        #[cfg(feature = "javascript")]
6172        assert_operator_rows::<JavascriptParser>("foo.js", JS_FAMILY_FUNCTION_ROWS);
6173        #[cfg(feature = "mozjs")]
6174        assert_operator_rows::<MozjsParser>("foo.js", JS_FAMILY_FUNCTION_ROWS);
6175        #[cfg(feature = "typescript")]
6176        {
6177            assert_operator_rows::<TypescriptParser>("foo.ts", JS_FAMILY_FUNCTION_ROWS);
6178            assert_operator_rows::<TsxParser>("foo.tsx", JS_FAMILY_FUNCTION_ROWS);
6179        }
6180    }
6181
6182    /// #1552 for the two TypeScript grammars: the shared `class` rows,
6183    /// plus their own `interface` and `enum` declaration keywords.
6184    #[cfg(feature = "typescript")]
6185    #[test]
6186    fn typescript_declaration_keywords_are_operators_1552() {
6187        const ROWS: &[(&str, &str, [u64; 2])] = &[
6188            // `interface`, `{}`
6189            ("interface", "interface I {}\n", [2, 2]),
6190            // `enum`, `{}`
6191            ("enum", "enum E { A }\n", [2, 2]),
6192        ];
6193        assert_operator_rows::<TypescriptParser>("foo.ts", JS_FAMILY_CLASS_ROWS);
6194        assert_operator_rows::<TypescriptParser>("foo.ts", ROWS);
6195        assert_operator_rows::<TsxParser>("foo.tsx", JS_FAMILY_CLASS_ROWS);
6196        assert_operator_rows::<TsxParser>("foo.tsx", ROWS);
6197    }
6198
6199    /// #1552: Java's type-declaration keywords are operators, as C#'s
6200    /// are. The class-literal row pins the guard: `C.class` is one
6201    /// operand, and its `class` leaf bills nothing of its own.
6202    #[cfg(feature = "java")]
6203    #[test]
6204    fn java_declaration_keywords_are_operators_1552() {
6205        const ROWS: &[(&str, &str, [u64; 2])] = &[
6206            // `class`, `{}`
6207            ("class", "class C {}\n", [2, 2]),
6208            // `interface`, `{}`
6209            ("interface", "interface I {}\n", [2, 2]),
6210            // `enum`, `{}`
6211            ("enum", "enum E { A }\n", [2, 2]),
6212            // `record`, `()`, `int`, `{}`
6213            ("record", "record R(int x) {}\n", [4, 4]),
6214            // `@interface`, `{}`
6215            ("annotation type", "@interface N {}\n", [2, 2]),
6216            // `class`, `{}`, `=`, `.`, `;` — one `class`: the literal's
6217            // is part of the `C.class` operand
6218            ("class literal", "class U { Object o = C.class; }\n", [5, 5]),
6219        ];
6220        assert_operator_rows::<JavaParser>("Foo.java", ROWS);
6221    }
6222
6223    /// #1560: a Java type name is an operand, as in C#, Rust, the C
6224    /// family, Go, Groovy and TypeScript. Each row isolates one position
6225    /// of the single `type_identifier` kind. The class-literal rows pin
6226    /// that a type inside `Foo.class` is not billed again on top of the
6227    /// literal's own operand, whether it sits there directly or under an
6228    /// array or scoped type; the `var` rows, that the inferred-type
6229    /// keyword bills nothing while a variable named `var` stays an
6230    /// operand.
6231    #[cfg(feature = "java")]
6232    #[test]
6233    fn java_type_names_are_operands_1560() {
6234        const ROWS: &[(&str, &str, [u64; 2])] = &[
6235            // `C`, `I` x2, `Foo`, `f`
6236            (
6237                "field type and implements",
6238                "class C implements I { Foo f; }\ninterface I {}\n",
6239                [4, 5],
6240            ),
6241            // `C`, `java`, `util`, `List`, `Foo`, `l`
6242            (
6243                "scoped and generic",
6244                "class C { java.util.List<Foo> l; }\n",
6245                [6, 6],
6246            ),
6247            // `C`, `T` x2, `Comparable`
6248            (
6249                "type parameter",
6250                "class C<T extends Comparable<T>> {}\n",
6251                [3, 4],
6252            ),
6253            // `C`, `m`, `Ex`
6254            ("throws", "class C { void m() throws Ex {} }\n", [3, 3]),
6255            // `C`, `Object`, `a`, `Foo.class`
6256            (
6257                "class literal",
6258                "class C { Object a = Foo.class; }\n",
6259                [4, 4],
6260            ),
6261            // `C`, `Object`, `a`, `Foo[].class`
6262            (
6263                "array class literal",
6264                "class C { Object a = Foo[].class; }\n",
6265                [4, 4],
6266            ),
6267            // `C`, `Object`, `a`, `java.util.List.class`
6268            (
6269                "scoped class literal",
6270                "class C { Object a = java.util.List.class; }\n",
6271                [4, 4],
6272            ),
6273            // `C`, `m`, `x`, `1` — no `var`
6274            (
6275                "inferred type",
6276                "class C { void m() { var x = 1; } }\n",
6277                [4, 4],
6278            ),
6279            // `C`, `m`, `var`, `1` — the variable's name
6280            (
6281                "variable named var",
6282                "class C { void m() { int var = 1; } }\n",
6283                [4, 4],
6284            ),
6285        ];
6286        assert_operand_rows::<JavaParser>("Foo.java", ROWS);
6287    }
6288
6289    /// #1552: Groovy's type-declaration keywords are operators, as
6290    /// Java's are. `String.class` is a `field_access` whose `class` is a
6291    /// plain identifier, so it stays an operand with no guard needed.
6292    #[cfg(feature = "groovy")]
6293    #[test]
6294    fn groovy_declaration_keywords_are_operators_1552() {
6295        const ROWS: &[(&str, &str, [u64; 2])] = &[
6296            // `class`, `{}`
6297            ("class", "class C {}\n", [2, 2]),
6298            // `interface`, `{}`
6299            ("interface", "interface I {}\n", [2, 2]),
6300            // `trait`, `{}`
6301            ("trait", "trait T {}\n", [2, 2]),
6302            // `enum`, `{}`
6303            ("enum", "enum E { A }\n", [2, 2]),
6304            // `record`, `()`, `{}`
6305            ("record", "record R(int x) {}\n", [3, 3]),
6306            // `def`, `=`, `.` — no `class`
6307            ("class reference", "def a = String.class\n", [3, 3]),
6308            // `=` twice: a script-level `record` / `trait` used as a name
6309            // falls into error recovery with the keyword leaf, which is
6310            // not a declaration and bills nothing
6311            ("contextual names", "record = 3\ntrait = 4\n", [1, 2]),
6312        ];
6313        assert_operator_rows::<GroovyParser>("foo.groovy", ROWS);
6314    }
6315
6316    /// #1552 rows shared by the two C++ clones. The template-parameter
6317    /// rows pin the synonym: `class T` is `typename T`, and the two
6318    /// spellings score alike. Both were unbilled until #1557 billed
6319    /// `typename`, and the parameter `class` with it.
6320    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
6321    const CPP_DECLARATION_KEYWORD_ROWS: &[(&str, &str, [u64; 2])] = &[
6322        // `class`, `{}`, `;`
6323        ("class", "class C {};\n", [3, 3]),
6324        // `struct`, `{}`, `;`
6325        ("struct", "struct S {};\n", [3, 3]),
6326        // `union`, `{}`, `int`, `;` twice
6327        ("union", "union U { int a; };\n", [4, 5]),
6328        // `enum`, `{}`, `;`
6329        ("enum", "enum E { A };\n", [3, 3]),
6330        // `enum`, `class`, `{}`, `;`
6331        ("enum class", "enum class F { B };\n", [4, 4]),
6332        // `struct`, `;` — an elaborated type bills its keyword
6333        ("elaborated type", "struct S s;\n", [2, 2]),
6334        // `template`, `<>`, `class`, `struct`, `{}`, `;` — the parameter
6335        // list's brackets are one pair (#1559)
6336        (
6337            "class parameter",
6338            "template <class T> struct W {};\n",
6339            [6, 6],
6340        ),
6341        // as above, `typename` in place of `class`
6342        (
6343            "typename parameter",
6344            "template <typename T> struct W {};\n",
6345            [6, 6],
6346        ),
6347        // `template`, `<>`, `class`, `=`, `int`, `struct`, `{}`, `;`
6348        (
6349            "optional class parameter",
6350            "template <class T = int> struct W {};\n",
6351            [8, 8],
6352        ),
6353        // `template`, `<>`, `class`, `struct`, `{}`, `;` — `...` is an
6354        // operand
6355        (
6356            "variadic class parameter",
6357            "template <class... T> struct W {};\n",
6358            [6, 6],
6359        ),
6360    ];
6361
6362    /// One call per clone, so reverting either arm fails its own row;
6363    /// Mozcpp owns no file extension, so this is its only coverage.
6364    #[cfg(all(feature = "cpp", feature = "mozcpp"))]
6365    #[test]
6366    fn cpp_declaration_keywords_are_operators_1552() {
6367        assert_operator_rows::<CppParser>("foo.cpp", CPP_DECLARATION_KEYWORD_ROWS);
6368        assert_operator_rows::<MozcppParser>("foo.cpp", CPP_DECLARATION_KEYWORD_ROWS);
6369    }
6370
6371    /// #1552: C bills `struct` / `union` / `enum` as C++ does, so a
6372    /// header scores the same whichever grammar parses it.
6373    #[cfg(feature = "c")]
6374    #[test]
6375    fn c_declaration_keywords_are_operators_1552() {
6376        const ROWS: &[(&str, &str, [u64; 2])] = &[
6377            // `struct`, `{}`, `int`, `;` twice
6378            ("struct", "struct S { int a; };\n", [4, 5]),
6379            // `union`, `{}`, `int`, `;` twice
6380            ("union", "union U { int a; };\n", [4, 5]),
6381            // `enum`, `{}`, `;`
6382            ("enum", "enum E { A };\n", [3, 3]),
6383            // `struct`, `;`
6384            ("elaborated type", "struct S s;\n", [2, 2]),
6385        ];
6386        assert_operator_rows::<CParser>("foo.c", ROWS);
6387    }
6388
6389    /// #1552: ObjC bills C's `struct` and its own class and protocol
6390    /// declaration keywords. `@class` already billed its `@` and stays
6391    /// as it was.
6392    #[cfg(feature = "objc")]
6393    #[test]
6394    fn objc_declaration_keywords_are_operators_1552() {
6395        const ROWS: &[(&str, &str, [u64; 2])] = &[
6396            // `@interface`, `:`
6397            ("interface", "@interface C : NSObject\n@end\n", [2, 2]),
6398            // `@implementation`
6399            ("implementation", "@implementation C\n@end\n", [1, 1]),
6400            // `@protocol`
6401            ("protocol", "@protocol P\n@end\n", [1, 1]),
6402            // `struct`, `{}`, `int`, `;` twice
6403            ("struct", "struct S { int a; };\n", [4, 5]),
6404            // `@`, `;` — unchanged by #1552
6405            ("forward class", "@class Fwd;\n", [2, 2]),
6406        ];
6407        assert_operator_rows::<ObjcParser>("foo.m", ROWS);
6408    }
6409
6410    /// #1556: Kotlin's `interface` bills as its `class` and `object`
6411    /// do. The modifier rows score one above the bare keyword since
6412    /// #1558: `enum` in `enum class` is a `class_modifier` like `data`
6413    /// and `sealed`, and Kotlin bills its modifiers as C# does.
6414    #[cfg(feature = "kotlin")]
6415    #[test]
6416    fn kotlin_declaration_keywords_are_operators_1556() {
6417        const ROWS: &[(&str, &str, [u64; 2])] = &[
6418            // `class`, `{}`
6419            ("class", "class C {}\n", [2, 2]),
6420            // `interface`, `{}`
6421            ("interface", "interface I {}\n", [2, 2]),
6422            // `object`, `{}`
6423            ("object", "object O {}\n", [2, 2]),
6424            // `enum`, `class`, `{}`
6425            ("enum class", "enum class E { A }\n", [3, 3]),
6426            // `data`, `class`, `{}`
6427            ("data class", "data class D {}\n", [3, 3]),
6428            // `sealed`, `interface`, `{}`
6429            ("sealed interface", "sealed interface S {}\n", [3, 3]),
6430            // `interface`, `class`, `:`, `{}` twice
6431            (
6432                "implementing class",
6433                "interface J {}\nclass K : J {}\n",
6434                [4, 5],
6435            ),
6436        ];
6437        assert_operator_rows::<KotlinParser>("foo.kt", ROWS);
6438    }
6439
6440    /// #1558: Kotlin bills its modifier keywords as C# bills its own,
6441    /// one family per row, each keyword the leaf and never
6442    /// its `*_modifier` wrapper. The "names" row is the control: the same
6443    /// words used as names are operands, and add no operator. The rows
6444    /// after it are names the grammar fails to parse, whose recovery keeps
6445    /// the keyword leaf, and their control.
6446    #[cfg(feature = "kotlin")]
6447    #[test]
6448    fn kotlin_modifiers_are_operators_1558() {
6449        const ROWS: &[(&str, &str, [u64; 2])] = &[
6450            // `public`, `private`, `protected`, `internal`, `val` x4,
6451            // `=` x4, `class`, `{}`
6452            (
6453                "visibility",
6454                "class C {\n    public val a = 1\n    private val b = 1\n    \
6455                 protected val c = 1\n    internal val d = 1\n}\n",
6456                [8, 14],
6457            ),
6458            // `abstract` x2, `open`, `final`, `override`, `class`, `{}` x2,
6459            // `fun` x3, `()` x3, `=`
6460            (
6461                "inheritance and member",
6462                "abstract class A {\n    abstract fun f()\n    open fun g() {}\n    \
6463                 final override fun toString() = \"\"\n}\n",
6464                [9, 15],
6465            ),
6466            // `inner`, `value`, `annotation`, `class` x3, `()`, `val`, `:`
6467            (
6468                "class",
6469                "inner class I\nvalue class V(val x: Int)\nannotation class N\n",
6470                [7, 9],
6471            ),
6472            // `lateinit`, `companion`, `const`, `class`, `object`, `var`,
6473            // `val`, `:`, `=`, `{}` x2
6474            (
6475                "property and companion",
6476                "class C {\n    lateinit var a: String\n    companion object {\n        \
6477                 const val b = 1\n    }\n}\n",
6478                [10, 11],
6479            ),
6480            // six modifiers, `fun` x6, `()` x6, `{}` x5
6481            (
6482                "function",
6483                "tailrec fun a() {}\noperator fun b() {}\ninfix fun c() {}\n\
6484                 inline fun d() {}\nexternal fun e()\nsuspend fun f() {}\n",
6485                [9, 23],
6486            ),
6487            // `inline`, `noinline`, `crossinline`, `vararg`, `fun`,
6488            // `()` x3, `:` x3, `->` x2, `,` x2, `{}`
6489            (
6490                "parameter",
6491                "inline fun f(noinline a: () -> Unit, crossinline b: () -> Unit, \
6492                 vararg c: Int) {}\n",
6493                [10, 16],
6494            ),
6495            // `inline`, `reified`, `in`, `out`, `fun`, `class`, `<>` x2,
6496            // `,`, `()`, `{}`
6497            (
6498                "type parameter",
6499                "inline fun <reified T> f() {}\nclass Box<in A, out B>\n",
6500                [10, 11],
6501            ),
6502            // `expect`, `actual`, `fun` x2, `()` x2, `{}`
6503            ("platform", "expect fun f()\nactual fun g() {}\n", [5, 7]),
6504            // `fun`, `()`, `{}`: an `@N` use is the `annotation` node, not
6505            // the keyword, so it bills no `annotation` operator
6506            ("annotation use", "@N fun k() {}\n", [3, 3]),
6507            // `val` x3, `=` x3: `open`, `out` and `value` are names here
6508            (
6509                "names",
6510                "val open = 1\nval out = 2\nval value = 3\n",
6511                [2, 6],
6512            ),
6513            // The rows below are names the grammar fails to parse. Its
6514            // recovery keeps the keyword leaf, which must bill nothing;
6515            // each expected value is what the source scored before #1558.
6516            // `class`, `{}` x2, `var`, `=` x2, `fun`, `()`: `open = !open`
6517            // recovers into an `ERROR` holding `inheritance_modifier`.
6518            (
6519                "name stranded in an ERROR",
6520                "class Drawer {\n    var open = false\n    fun toggle() { open = !open }\n}\n",
6521                [6, 8],
6522            ),
6523            // `val`: `suspend` never parses as a name.
6524            ("suspend as a name", "val suspend = 1\n", [1, 1]),
6525            // `fun`, `()`, `:` x2, `=`: a modifier list followed by an
6526            // inserted zero-width parameter name.
6527            (
6528                "parameter named vararg",
6529                "fun f(vararg: Int): Int = vararg\n",
6530                [4, 5],
6531            ),
6532            // The same row with a comment before the inserted name: an
6533            // extra, so the name is still the next token. It scored
6534            // [5, 6], a phantom `vararg` operator.
6535            (
6536                "parameter named vararg, commented",
6537                "fun f(vararg /*note*/: Int): Int = vararg\n",
6538                [4, 5],
6539            ),
6540            // Control: a real `vararg` before a commented parameter keeps
6541            // its operator. `vararg`, `fun`, `()`, `:`, `{}`.
6542            (
6543                "vararg modifier, commented",
6544                "fun f(vararg /*note*/ x: Int) {}\n",
6545                [5, 5],
6546            ),
6547            // `class`, `()`, `:`
6548            ("class parameter named open", "class C(open: Int)\n", [3, 3]),
6549            // `val`, `=`, `{}`, `:`
6550            (
6551                "lambda parameter named open",
6552                "val l = { open: Int -> open }\n",
6553                [4, 4],
6554            ),
6555            // Control for the gate: a real `suspend` in a function type's
6556            // `type_modifiers` and in a declaration's `modifiers` bills
6557            // twice. `val`, `:`, `suspend` x2, `()` x2, `->`, `=`, `{}` x2,
6558            // `fun`.
6559            (
6560                "suspend modifiers",
6561                "val f: suspend () -> Unit = {}\nsuspend fun g() {}\n",
6562                [8, 11],
6563            ),
6564        ];
6565        assert_operator_rows::<KotlinParser>("foo.kt", ROWS);
6566    }
6567
6568    /// #1558: `reified` is the one Kotlin modifier spelled as an aliased
6569    /// named kind. `KotlinCode::is_primitive` keys it by its text, so
6570    /// `bca ops` lists the word written, not the grammar's
6571    /// `reification_modifier`. `n1` cannot see the difference — the kind
6572    /// has one spelling either way — so the vocabulary is the only place
6573    /// this is observable.
6574    #[cfg(feature = "kotlin")]
6575    #[test]
6576    fn kotlin_reified_is_listed_by_its_text_1558() {
6577        let ops = ops_of::<KotlinParser>("fun <reified T> f() {}\n", "foo.kt");
6578        assert!(
6579            ops.operators.iter().any(|op| op == "reified"),
6580            "{:?}",
6581            ops.operators
6582        );
6583        assert!(
6584            !ops.operators.iter().any(|op| op == "reification_modifier"),
6585            "{:?}",
6586            ops.operators
6587        );
6588    }
6589
6590    /// #1557 rows shared by the two C++ clones: the declaration keywords
6591    /// #1552 left out, one per row, plus the two disambiguator positions
6592    /// (`typename T::x`, `x.template f`) that bill the same leaf.
6593    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
6594    const CPP_MORE_DECLARATION_KEYWORD_ROWS: &[(&str, &str, [u64; 2])] = &[
6595        // `namespace`, `{}`
6596        ("namespace", "namespace N {}\n", [2, 2]),
6597        // `typedef`, `int`, `;`
6598        ("typedef", "typedef int T;\n", [3, 3]),
6599        // `using`, `=`, `int`, `;`
6600        ("alias declaration", "using U = int;\n", [4, 4]),
6601        // `using`, `namespace`, `;`
6602        ("using directive", "using namespace std;\n", [3, 3]),
6603        // `template`, `<>`, `typename`, `;` and the `struct` of `S`
6604        (
6605            "template declaration",
6606            "template <typename T> struct S;\n",
6607            [5, 5],
6608        ),
6609        // `typename`, `::`, `;`
6610        ("dependent type", "typename T::type x;\n", [3, 3]),
6611        // `void`, `()` twice, `{}`, `.`, `template`, `<>`, `int`, `;`
6612        (
6613            "template disambiguator",
6614            "void g() { x.template f<int>(); }\n",
6615            [8, 9],
6616        ),
6617    ];
6618
6619    /// One call per clone, as for the #1552 rows.
6620    #[cfg(all(feature = "cpp", feature = "mozcpp"))]
6621    #[test]
6622    fn cpp_more_declaration_keywords_are_operators_1557() {
6623        assert_operator_rows::<CppParser>("foo.cpp", CPP_MORE_DECLARATION_KEYWORD_ROWS);
6624        assert_operator_rows::<MozcppParser>("foo.cpp", CPP_MORE_DECLARATION_KEYWORD_ROWS);
6625    }
6626
6627    /// #1557: `typedef` is a C keyword, so C and ObjC bill it as C++
6628    /// does — a header scores alike whichever grammar parses it.
6629    #[cfg(any(feature = "c", feature = "objc"))]
6630    const C_TYPEDEF_ROWS: &[(&str, &str, [u64; 2])] = &[
6631        // `typedef`, `int`, `;`
6632        ("typedef", "typedef int T;\n", [3, 3]),
6633        // `typedef`, `struct`, `{}`, `int`, `;` twice
6634        ("typedef struct", "typedef struct S { int a; } S;\n", [5, 6]),
6635    ];
6636
6637    #[cfg(any(feature = "c", feature = "objc"))]
6638    #[test]
6639    fn c_family_typedef_is_an_operator_1557() {
6640        #[cfg(feature = "c")]
6641        assert_operator_rows::<CParser>("foo.c", C_TYPEDEF_ROWS);
6642        #[cfg(feature = "objc")]
6643        assert_operator_rows::<ObjcParser>("foo.m", C_TYPEDEF_ROWS);
6644    }
6645
6646    /// #1557: the TypeScript declaration keywords #1552 left out. The
6647    /// "keywords as names" row pins that each word, used as a *name*,
6648    /// stays an operand: the grammar emits an `identifier` there. The two
6649    /// exported-binding rows are the one place it emits the `type` leaf
6650    /// for a name, which must bill nothing; the last two rows are their
6651    /// controls, a type-only specifier and re-export whose `type` still
6652    /// bills.
6653    #[cfg(feature = "typescript")]
6654    const TS_DECLARATION_KEYWORD_ROWS: &[(&str, &str, [u64; 2])] = &[
6655        // `type`, `=`, `number`, `;`
6656        ("type alias", "type T = number;\n", [4, 4]),
6657        // `namespace`, `{}`
6658        ("namespace", "namespace N {}\n", [2, 2]),
6659        // `module`, `{}`. Billing the `module` declaration node instead
6660        // of its keyword leaf would score this row the same: every
6661        // spelling probed (bodiless, quoted, dotted, exported, ERROR
6662        // recovery) emits the two together, so only the getter's
6663        // choice of the leaf, not this row, keeps them from both billing.
6664        ("module", "module M {}\n", [2, 2]),
6665        // `declare`, `module`, `{}`
6666        ("ambient module", "declare module \"m\" {}\n", [3, 3]),
6667        // `declare`, `global`, `{}`
6668        ("global augmentation", "declare global {}\n", [3, 3]),
6669        // `import`, `type`, `{}`, `from`, `;`
6670        (
6671            "type-only import",
6672            "import type { A } from \"a\";\n",
6673            [5, 5],
6674        ),
6675        // `let`, `=` twice, `;` twice, `.` — no keyword
6676        (
6677            "keywords as names",
6678            "let type = 1;\nmodule.exports = declare;\n",
6679            [4, 6],
6680        ),
6681        // `const`, `=`, `;` twice, `export`, `{}` — no keyword. The
6682        // grammar recovers an exported binding named `type` as the
6683        // keyword leaf, under an `ERROR` or beside one in the specifier.
6684        (
6685            "exported binding named type",
6686            "const type = 1;\nexport { type };\n",
6687            [5, 6],
6688        ),
6689        (
6690            "renamed exported binding named type",
6691            "const type = 1;\nexport { type as kind };\n",
6692            [5, 6],
6693        ),
6694        // `export`, `{}`, `type`, `from`, `;` — a type-only specifier
6695        // parses cleanly, so its keyword still bills.
6696        (
6697            "type-only export specifier",
6698            "export { type Foo } from \"m\";\n",
6699            [5, 5],
6700        ),
6701        // `export`, `type`, `*`, `from`, `;` — the pinned grammar fails
6702        // TS 5.0's type-only re-export into an `ERROR` that hangs from the
6703        // statement, not from an export clause, and its keyword bills.
6704        (
6705            "type-only re-export",
6706            "export type * from \"./types\";\n",
6707            [5, 5],
6708        ),
6709    ];
6710
6711    /// #1557: a TypeScript type name is an operand, as C#, Rust and the
6712    /// C family bill theirs. Each row's names are `type_identifier`
6713    /// leaves except where noted, and before #1557 none of them billed.
6714    #[cfg(feature = "typescript")]
6715    const TS_TYPE_NAME_OPERAND_ROWS: &[(&str, &str, [u64; 2])] = &[
6716        // `T`
6717        ("alias name", "type T = number;\n", [1, 1]),
6718        // `I`
6719        ("interface name", "interface I {}\n", [1, 1]),
6720        // `C`
6721        ("class name", "class C {}\n", [1, 1]),
6722        // `x` (an identifier), `Foo`
6723        ("annotation", "let x: Foo;\n", [2, 2]),
6724        // `m`, `Map`, `K` twice
6725        ("generic", "let m: Map<K, K>;\n", [3, 4]),
6726        // `f`, `a` (identifiers), `U` twice
6727        ("type parameter", "function f<U>(a: U) {}\n", [3, 4]),
6728        // `q`, `ns` (identifiers), `T` — the `nested_type_identifier`
6729        // wrapper is unlisted, so its text is not a fourth operand
6730        ("qualified type", "let q: ns.T;\n", [3, 3]),
6731        // `C`, `I`
6732        ("implements", "class C implements I {}\n", [2, 2]),
6733    ];
6734
6735    #[cfg(feature = "typescript")]
6736    #[test]
6737    fn typescript_declaration_keywords_and_type_names_1557() {
6738        assert_operator_rows::<TypescriptParser>("foo.ts", TS_DECLARATION_KEYWORD_ROWS);
6739        assert_operator_rows::<TsxParser>("foo.tsx", TS_DECLARATION_KEYWORD_ROWS);
6740        assert_operand_rows::<TypescriptParser>("foo.ts", TS_TYPE_NAME_OPERAND_ROWS);
6741        assert_operand_rows::<TsxParser>("foo.tsx", TS_TYPE_NAME_OPERAND_ROWS);
6742    }
6743
6744    /// #1561: TypeScript's type-level operator keywords bill as their
6745    /// value-level siblings `as`, `typeof` and `extends` do, each as its
6746    /// keyword leaf under an unbilled wrapper. The `asserts a is Baz` row
6747    /// pins that the `asserts` node and its keyword token bill once
6748    /// between them, and the last row that the same words used as names
6749    /// are operands. Both TypeScript grammars carry the change.
6750    #[cfg(feature = "typescript")]
6751    #[test]
6752    fn typescript_type_operator_keywords_1561() {
6753        const ROWS: &[(&str, &str, [u64; 2])] = &[
6754            // `let`, `=`, `satisfies`, `;`
6755            ("satisfies", "let s = w satisfies Sat;\n", [4, 4]),
6756            // `type`, `=`, `keyof`, `;`
6757            ("keyof", "type K = keyof T;\n", [4, 4]),
6758            // `type`, `<>` x2, `=`, `extends`, `infer`, `?`, `:`, `;`
6759            (
6760                "infer",
6761                "type I<X> = X extends Array<infer E> ? E : X;\n",
6762                [8, 9],
6763            ),
6764            // `function`, `()`, `:` x2, `unknown`, `is`, `{}`, `return`, `;`
6765            (
6766                "type predicate",
6767                "function g(a: unknown): a is Baz { return true; }\n",
6768                [8, 9],
6769            ),
6770            // `function`, `()`, `:` x2, `unknown`, `asserts`, `is`, `{}`
6771            (
6772                "asserts predicate",
6773                "function h(a: unknown): asserts a is Baz {}\n",
6774                [7, 8],
6775            ),
6776            // `function`, `()`, `:` x2, `unknown`, `asserts`, `{}`
6777            (
6778                "bare asserts",
6779                "function k(a: unknown): asserts a {}\n",
6780                [6, 7],
6781            ),
6782            // `const`, `=` and `;` five times each: the words are names
6783            (
6784                "names",
6785                "const keyof = 1;\nconst infer = 2;\nconst is = 3;\nconst asserts = 4;\n\
6786                 const satisfies = 5;\n",
6787                [3, 15],
6788            ),
6789        ];
6790        assert_operator_rows::<TypescriptParser>("foo.ts", ROWS);
6791        assert_operator_rows::<TsxParser>("foo.tsx", ROWS);
6792    }
6793
6794    #[cfg(feature = "bash")]
6795    #[test]
6796    fn bash_operators_and_operands() {
6797        check_metrics::<BashParser>(
6798            "#!/bin/bash
6799f() {
6800    local x=1
6801    if [ $x -eq 1 ]; then
6802        echo 'one'
6803    fi
6804}",
6805            "foo.sh",
6806            |metric| {
6807                // Operators (9 unique, 9 occurrences): the opening
6808                // delimiters `()`/`{}`/`[]` (each folded to one glyph and
6809                // counted once per balanced pair, #695 — the closers no
6810                // longer add a second operator), `local`, `=`, `if`,
6811                // `then`, `fi`, `;`.
6812                // Operands (6 unique, 7 occurrences): `f`, `x` (the
6813                // assignment LHS `variable_name`, kind 160), `1` (twice:
6814                // `=1` and `-eq 1`), `$x` (the `simple_expansion` — its
6815                // inner `variable_name` leaf is now suppressed so `$x`
6816                // counts once, #695), `echo`, `'one'`.
6817                // N2 was 8 before #1351: `echo` counted twice, once as the
6818                // `command_name` wrapper and once as the `word` it wraps.
6819                assert_eq!(metric.halstead.unique_operators(), 9);
6820                assert_eq!(metric.halstead.total_operators(), 9);
6821                assert_eq!(metric.halstead.unique_operands(), 6);
6822                assert_eq!(metric.halstead.total_operands(), 7);
6823                insta::assert_json_snapshot!(metric.halstead);
6824            },
6825        );
6826    }
6827
6828    #[cfg(feature = "bash")]
6829    #[test]
6830    fn bash_interpolated_string_no_double_count() {
6831        // Regression: issue #180. A double-quoted Bash string containing
6832        // `$name`, `${name[…]}`, or `$(cmd)` used to be classified as a
6833        // Halstead operand AND have its inner `simple_expansion` /
6834        // `expansion` / `command_substitution` children classified as
6835        // operands too. We now skip the wrapping literal when it has an
6836        // expansion child so only the inner expansion contributes.
6837        //
6838        // expected: operands across `a="plain"\nb="$x"\n` —
6839        //   line 1: variable_name `a`, plain string `"plain"` (no
6840        //     expansion, still operand) → 2.
6841        //   line 2: variable_name `b`, wrapping `"$x"` skipped (has
6842        //     expansion), `simple_expansion` `$x` (its inner
6843        //     variable_name `x` leaf is suppressed under #695) → 2.
6844        // Total unique operands: 4 (`a`, `b`, `"plain"`, `$x`), each
6845        // appearing once → N2 = 4. Before #695 the inner `x` leaf of
6846        // `$x` was also counted (u_operands = 5, N2 = 5); before the
6847        // earlier #180 fix the wrapping `"$x"` literal was counted too.
6848        // The `=` is the only operator; appears twice (N1 = 2, n1 = 1).
6849        check_metrics::<BashParser>("a=\"plain\"\nb=\"$x\"\n", "foo.sh", |metric| {
6850            assert_eq!(metric.halstead.unique_operators(), 1);
6851            assert_eq!(metric.halstead.total_operators(), 2);
6852            assert_eq!(metric.halstead.unique_operands(), 4);
6853            assert_eq!(metric.halstead.total_operands(), 4);
6854            insta::assert_json_snapshot!(metric.halstead);
6855        });
6856    }
6857
6858    #[cfg(feature = "elixir")]
6859    #[test]
6860    fn elixir_interpolated_string_no_double_count() {
6861        // Regression: issue #180. Without the fix, an interpolated
6862        // Elixir `String` was classified as a single operand while its
6863        // inner `interpolation` identifier was also walked and
6864        // classified as its own operand — double-counting the
6865        // interpolated identifier's contribution to `N2`.
6866        //
6867        // expected: operand contributions for
6868        //   `def greet(name) do\n  msg = "Hi #{name}"\nend\n` —
6869        // `def`, `greet`, `name` (param), `msg`, and the inner `name`
6870        // (inside `#{...}`). With the fix, the wrapping
6871        // `"Hi #{name}"` literal is skipped (has `Interpolation`
6872        // child), so `name` is the only repeated operand:
6873        // u_operands = 4 (def, greet, name, msg), N2 = 5. Without the
6874        // fix, the wrapping literal would also count → u_operands = 5,
6875        // N2 = 6. Operators: `do`, `end`, `(`, `=` → u = N = 4.
6876        // Only the *opening* delimiters count after #695, so the `)`
6877        // and the `}` interpolation closer add no operator; #1314 then
6878        // dropped the `#{` opener too, on the rule that an
6879        // interpolation opener is spelling rather than an operation
6880        // (was 5 here, and 7 before #695).
6881        check_metrics::<ElixirParser>(
6882            "def greet(name) do\n  msg = \"Hi #{name}\"\nend\n",
6883            "foo.ex",
6884            |metric| {
6885                assert_eq!(metric.halstead.unique_operators(), 4);
6886                assert_eq!(metric.halstead.total_operators(), 4);
6887                assert_eq!(metric.halstead.unique_operands(), 4);
6888                assert_eq!(metric.halstead.total_operands(), 5);
6889                insta::assert_json_snapshot!(metric.halstead);
6890            },
6891        );
6892    }
6893
6894    #[cfg(feature = "elixir")]
6895    #[test]
6896    fn elixir_plain_string_still_operand() {
6897        // The fix for #180 only skips wrapping literals that contain
6898        // interpolation; a plain `"hello"` must still contribute exactly
6899        // one operand. expected: `def`, `f`, `"hello"` → 3 unique
6900        // operands (n2 = 3), each appearing once (N2 = 3).
6901        check_metrics::<ElixirParser>("def f do\n  \"hello\"\nend\n", "foo.ex", |metric| {
6902            assert_eq!(metric.halstead.unique_operands(), 3);
6903            assert_eq!(metric.halstead.total_operands(), 3);
6904        });
6905    }
6906
6907    #[cfg(feature = "elixir")]
6908    #[test]
6909    fn elixir_boolean_and_nil_literals_count_once() {
6910        // Regression: issue #1253. `boolean: choice("true", "false")`
6911        // and `nil: "nil"` each wrap a keyword leaf, and both the
6912        // wrapper and the leaf sat in the operand arm — so every
6913        // literal occurrence added +1 to N2. Operands are keyed by
6914        // source text, so the duplicate collapsed into the same
6915        // vocabulary entry and n2 stayed correct, which is why nothing
6916        // caught it.
6917        //
6918        // Source is the issue's reproducer plus a repeat of `true` and
6919        // `nil`, so N2 exceeds n2 and the assertions can tell "counted
6920        // once per occurrence" from "deduplicated into the vocabulary".
6921        // All three keywords appear, so restoring any one of `True`,
6922        // `False`, or `Nil2` to the operand arm trips this test.
6923        //
6924        // Operands by text key: `x`, `y`, `z`, `w`, `v`, `true` × 2,
6925        // `nil` × 2, `false` ⇒ n2 = 8, N2 = 10. Before the fix each of
6926        // the five literals counted twice ⇒ N2 = 15.
6927        //
6928        // This also guards the drift in the other direction. Elixir
6929        // classifies the wrapper and drops the leaf outright rather
6930        // than parent-guarding it, so a grammar bump that stopped
6931        // emitting `boolean` / `nil` would leave the leaves unclassified
6932        // and the literals would vanish from N2 entirely (⇒ 5 / 5)
6933        // rather than merely being miscounted.
6934        check_metrics::<ElixirParser>(
6935            "x = true\ny = nil\nz = false\nw = true\nv = nil\n",
6936            "foo.ex",
6937            |metric| {
6938                assert_eq!(metric.halstead.unique_operands(), 8);
6939                assert_eq!(metric.halstead.total_operands(), 10);
6940            },
6941        );
6942    }
6943
6944    #[cfg(feature = "elixir")]
6945    #[test]
6946    fn elixir_reserved_word_after_a_dot_stays_an_operand() {
6947        // Companion to the test above (#1253). Elixir drops `True` /
6948        // `False` / `Nil2` from the operand arm outright, which is only
6949        // safe because the one grammar position that accepts a reserved
6950        // word outside the `boolean` / `nil` wrapper — the right-hand
6951        // side of a remote dot — aliases it to `identifier`. This pins
6952        // that alias: if a grammar bump emitted the bare keyword there
6953        // instead, `Foo.nil` and `Foo.true` would silently stop
6954        // contributing an operand.
6955        //
6956        // Source: a = Foo.nil / b = Foo.true / c = nil
6957        //
6958        // Operands by text key: `a`, `Foo` × 2, `nil` × 2 (the aliased
6959        // identifier and the real literal, which share a text key),
6960        // `b`, `true`, `c` ⇒ n2 = 6, N2 = 8. Losing the alias drops the
6961        // two dotted references ⇒ N2 = 6.
6962        check_metrics::<ElixirParser>("a = Foo.nil\nb = Foo.true\nc = nil\n", "foo.ex", |metric| {
6963            assert_eq!(metric.halstead.unique_operands(), 6);
6964            assert_eq!(metric.halstead.total_operands(), 8);
6965        });
6966    }
6967
6968    #[cfg(feature = "elixir")]
6969    #[test]
6970    fn elixir_interpolated_sigil_no_double_count() {
6971        // Sigils mirror strings under #180. For `~r/foo#{name}/`, the
6972        // wrapping `Sigil` is skipped, but `SigilName` (`r`) and the
6973        // inner `name` identifier each contribute one operand.
6974        // expected: `def`, `f`, `name` (param), `re`, `r` (sigil name),
6975        // `name` (inside `#{...}`) → u_operands = 5, N2 = 6 (`name`
6976        // twice).
6977        check_metrics::<ElixirParser>(
6978            "def f(name) do\n  re = ~r/foo#{name}/\nend\n",
6979            "foo.ex",
6980            |metric| {
6981                assert_eq!(metric.halstead.unique_operands(), 5);
6982                assert_eq!(metric.halstead.total_operands(), 6);
6983            },
6984        );
6985    }
6986
6987    #[cfg(feature = "elixir")]
6988    #[test]
6989    fn elixir_interpolated_charlist_no_double_count() {
6990        // Charlists mirror strings and sigils under #180. The
6991        // `E::String | E::Charlist | E::Sigil` arm in `get_op_type`
6992        // skips any wrapping literal that has an `Interpolation`
6993        // child; this test exercises the `Charlist` branch
6994        // specifically.
6995        //
6996        // expected: for `def f(name) do\n  cl = 'Hi #{name}'\nend\n` —
6997        // `def`, `f`, `name` (param), `cl`, and the inner `name`
6998        // (inside `#{...}`). With the fix, the wrapping
6999        // `'Hi #{name}'` is skipped → u_operands = 4 (def, f, name,
7000        // cl), N2 = 5 (`name` twice).
7001        check_metrics::<ElixirParser>(
7002            "def f(name) do\n  cl = 'Hi #{name}'\nend\n",
7003            "foo.ex",
7004            |metric| {
7005                assert_eq!(metric.halstead.unique_operands(), 4);
7006                assert_eq!(metric.halstead.total_operands(), 5);
7007            },
7008        );
7009    }
7010
7011    #[cfg(feature = "elixir")]
7012    #[test]
7013    fn elixir_sigil_delimiters_are_not_operators() {
7014        // Regression: issue #1256. Sigil delimiter tokens share their
7015        // kind ids with real operators (`SLASH`, `LPAREN`, `LBRACE`,
7016        // …) and were classified unconditionally, so `~r/abc/`
7017        // fabricated two division operators and the author's delimiter
7018        // choice moved n1/N1. The parent guard suppresses them under
7019        // `Sigil`; `~` stays the single per-sigil operator.
7020        //
7021        // expected: operators `=` × 3 and `~` × 3 → n1 = 2, N1 = 6.
7022        // Without the guard the delimiters added `/` × 2, `(`, `{` →
7023        // n1 = 5, N1 = 10. Operands: `a`, `~r/abc/i`, `r`, `i` (sigil
7024        // modifiers), `b`, `~w(one two)`, `w`, `c`, `~s{hi}`, `s` →
7025        // n2 = N2 = 10.
7026        check_metrics::<ElixirParser>(
7027            "a = ~r/abc/i\nb = ~w(one two)\nc = ~s{hi}\n",
7028            "foo.ex",
7029            |metric| {
7030                assert_eq!(metric.halstead.unique_operators(), 2);
7031                assert_eq!(metric.halstead.total_operators(), 6);
7032                assert_eq!(metric.halstead.unique_operands(), 10);
7033                assert_eq!(metric.halstead.total_operands(), 10);
7034            },
7035        );
7036    }
7037
7038    #[cfg(feature = "elixir")]
7039    #[test]
7040    fn elixir_sigil_delimiter_choice_is_invariant() {
7041        // Companion to the test above (#1256): two sigils differing
7042        // only in delimiter are the same literal, so every delimiter
7043        // choice must produce identical Halstead counts. `(` `[` `{`
7044        // `<` `/` `|` are the operator-kind delimiters the guard
7045        // covers; `"` and `'` never had an operator arm and pin the
7046        // already-correct path.
7047        //
7048        // expected per variant: operators `=`, `~` → n1 = 2, N1 = 2;
7049        // operands `x`, the sigil literal text, `w` (sigil name) →
7050        // n2 = 3, N2 = 3.
7051        for (open, close) in [
7052            ("(", ")"),
7053            ("[", "]"),
7054            ("{", "}"),
7055            ("<", ">"),
7056            ("/", "/"),
7057            ("|", "|"),
7058            ("\"", "\""),
7059            ("'", "'"),
7060        ] {
7061            assert_halstead_counts::<ElixirParser>(
7062                &format!("x = ~w{open}one two{close}\n"),
7063                "foo.ex",
7064                [2, 2, 3, 3],
7065                &format!("delimiter pair {open} {close}"),
7066            );
7067        }
7068    }
7069
7070    #[cfg(feature = "elixir")]
7071    #[test]
7072    fn elixir_standalone_operators_survive_the_sigil_guard() {
7073        // Control for #1256: the guard is parent-scoped, so the same
7074        // token kinds outside a sigil still count. Covers every guarded
7075        // kind standalone: `/` (division), `<` / `>` (comparison), `[`
7076        // and `|` (list cons), `(` (call), `{` (map literal, with its
7077        // `%`).
7078        //
7079        // expected: operators `=` × 6, `/`, `<`, `>`, `[`, `|`, `(`,
7080        // `%`, `{` → n1 = 9, N1 = 14. Operands: `x`, `a`, `b`, `y`,
7081        // `c`, `d`, `z`, `e`, `f`, `q`, `h`, `t`, `p`, `g`, `1`, `m`,
7082        // the `k:` keyword, `2` → n2 = N2 = 18.
7083        check_metrics::<ElixirParser>(
7084            "x = a / b\ny = c < d\nz = e > f\nq = [h | t]\np = g(1)\nm = %{k: 2}\n",
7085            "foo.ex",
7086            |metric| {
7087                assert_eq!(metric.halstead.unique_operators(), 9);
7088                assert_eq!(metric.halstead.total_operators(), 14);
7089                assert_eq!(metric.halstead.unique_operands(), 18);
7090                assert_eq!(metric.halstead.total_operands(), 18);
7091            },
7092        );
7093    }
7094
7095    #[cfg(feature = "elixir")]
7096    #[test]
7097    fn elixir_interpolated_sigil_keeps_inner_nodes_counting() {
7098        // Interpolation inside a sigil after #1256: the `{` delimiter
7099        // is suppressed (its parent is the `Sigil`), while the
7100        // `interpolation` child is a separate node whose inner
7101        // identifier must still count — the guard must not reach past
7102        // the delimiter tokens.
7103        //
7104        // expected: operators `=`, `~` → n1 = N1 = 2. Operands:
7105        // `v`, `s` (sigil name), `b` (interpolated identifier); the
7106        // wrapping sigil is skipped (`Interpolation` child, #180) and
7107        // `quoted_content` is unclassified → n2 = N2 = 3. The `#{`
7108        // marker was a third operator until #1314 dropped it.
7109        check_metrics::<ElixirParser>("v = ~s{a#{b} c}\n", "foo.ex", |metric| {
7110            assert_eq!(metric.halstead.unique_operators(), 2);
7111            assert_eq!(metric.halstead.total_operators(), 2);
7112            assert_eq!(metric.halstead.unique_operands(), 3);
7113            assert_eq!(metric.halstead.total_operands(), 3);
7114        });
7115
7116        // A guarded kind *inside* the interpolation: the `/` in
7117        // `#{a / b}` has `binary_operator` as its parent but the
7118        // `Sigil` as a further ancestor, so this input is the one
7119        // discriminator between the correct parent-scoped guard and a
7120        // wrong ancestor-scoped one that would swallow it.
7121        //
7122        // expected: operators `=`, `~`, `/` → n1 = N1 = 3 (the `#{`
7123        // opener stopped counting with #1314); operands `v`, `s`, `a`,
7124        // `b` → n2 = N2 = 4. The division is what this row is for, and
7125        // it still counts — the ancestor-scoped mutant drops it.
7126        check_metrics::<ElixirParser>("v = ~s{x #{a / b} y}\n", "foo.ex", |metric| {
7127            assert_eq!(metric.halstead.unique_operators(), 3);
7128            assert_eq!(metric.halstead.total_operators(), 3);
7129            assert_eq!(metric.halstead.unique_operands(), 4);
7130            assert_eq!(metric.halstead.total_operands(), 4);
7131        });
7132    }
7133
7134    #[cfg(feature = "bash")]
7135    #[test]
7136    fn bash_all_expansion_kinds_skip_wrapper() {
7137        // Exercises every node kind tested by
7138        // `bash_string_has_expansion`: `simple_expansion` (`$v`),
7139        // `expansion` (`${v[0]}`), `command_substitution` (`$(date)`),
7140        // and `arithmetic_expansion` (`$((1+2))`). A typo replacing
7141        // one kind with an aliased neighbour in `language_bash.rs`
7142        // (e.g., `ExpansionBody` instead of `Expansion`) would leave
7143        // the corresponding wrapping string counted as an operand and
7144        // shift the totals.
7145        //
7146        // expected: operands across the four lines —
7147        //   line 1 `a="$v"`: var_name `a`, simple_expansion `$v` (its
7148        //     inner var_name `v` leaf is suppressed under #695; wrapper
7149        //     skipped) → 2
7150        //   line 2 `b="${v[0]}"`: var_name `b`, var_name `v` (inside
7151        //     subscript — parent is `expansion`, not `simple_expansion`,
7152        //     so it still counts), number `0` (wrapper skipped,
7153        //     `expansion` itself is not in the operand list) → 3
7154        //   line 3 `c="$(date)"`: var_name `c`, the `word` `date` under
7155        //     the `command_name` (wrapper skipped, `command_substitution`
7156        //     not in operand list, and since #1351 the `command_name`
7157        //     wrapper is not either) → 2
7158        //   line 4 `d="$((1+2))"`: var_name `d`, numbers `1` and `2`
7159        //     (wrapper skipped, `arithmetic_expansion` not in operand
7160        //     list) → 3
7161        // Unique operands: a, b, c, d, $v, v, 0, date, 1, 2 → 10. Total
7162        // occurrences: 10 (`v` appears once — only line 2's subscript
7163        // leaf; line 1's `$v` inner leaf is suppressed — and `date` once,
7164        // as the `word`; before #1351 the `command_name` wrapping it
7165        // added a second `date` and N2 was 11). Operators after
7166        // #695: only the openers `[` (folded `[]`) and `+`, plus `=` four
7167        // times — the `}`/`)`/`))`/`]` closers no longer count.
7168        check_metrics::<BashParser>(
7169            "a=\"$v\"\nb=\"${v[0]}\"\nc=\"$(date)\"\nd=\"$((1+2))\"\n",
7170            "foo.sh",
7171            |metric| {
7172                assert_eq!(metric.halstead.unique_operators(), 3);
7173                assert_eq!(metric.halstead.total_operators(), 6);
7174                assert_eq!(metric.halstead.unique_operands(), 10);
7175                assert_eq!(metric.halstead.total_operands(), 10);
7176            },
7177        );
7178    }
7179
7180    /// Regression for #695. A bare `$x` (outside any string) parses as a
7181    /// `simple_expansion` wrapping a `variable_name` leaf — and `$?` / `$1`
7182    /// as a `simple_expansion` wrapping a `special_variable_name` leaf. Both
7183    /// the wrapper and the inner leaf used to be classified as operands, so
7184    /// each bare variable reference double-counted (the same hazard Tcl
7185    /// guards with its `Id2` exclusion and iRules with a parent check). The
7186    /// `variable_name` / `special_variable_name` arm now yields `Unknown`
7187    /// when its parent is a `simple_expansion`, so `$x` contributes exactly
7188    /// one operand while the assignment LHS `variable_name` (`x` in `x=…`,
7189    /// parent is `variable_assignment`) still counts.
7190    #[cfg(feature = "bash")]
7191    #[test]
7192    fn bash_bare_variable_no_double_count() {
7193        let source = "x=1\necho $x\necho $?\n";
7194        let path = PathBuf::from("foo.sh");
7195        let parser = BashParser::new(source.as_bytes().to_vec(), &path, None);
7196        let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
7197        let bare_x = ops.operands.iter().filter(|o| o.as_str() == "$x").count();
7198        let special = ops.operands.iter().filter(|o| o.as_str() == "$?").count();
7199        // Each bare reference is exactly one operand; the inner leaf is not
7200        // double-counted. If the guard regressed, the inner `variable_name`
7201        // `x` would add a second `x` occurrence (text-colliding with the
7202        // assignment LHS) and the inner `special_variable_name` `?` would
7203        // appear as a standalone `?` operand.
7204        assert_eq!(
7205            bare_x, 1,
7206            "bare $x must be one operand; operands were {:?}",
7207            ops.operands
7208        );
7209        assert_eq!(
7210            special, 1,
7211            "bare $? must be one operand; operands were {:?}",
7212            ops.operands
7213        );
7214        assert!(
7215            !ops.operands.iter().any(|o| o.as_str() == "?"),
7216            "the inner special_variable_name `?` leaf must be suppressed; operands were {:?}",
7217            ops.operands
7218        );
7219        // The assignment LHS `variable_name` `x` (parent `variable_assignment`,
7220        // not `simple_expansion`) must still be an operand.
7221        assert!(
7222            ops.operands.iter().any(|o| o.as_str() == "x"),
7223            "assignment LHS `x` must still be an operand; operands were {:?}",
7224            ops.operands
7225        );
7226    }
7227
7228    /// Drives one row of a Bash wrapper/leaf double-count table: a grammar
7229    /// wrapper whose entire contribution to `N2` was its single,
7230    /// already-counted child (`command_name`, #1351; `translated_string`,
7231    /// #1358).
7232    ///
7233    /// `expected` is the post-fix `[n1, N1, n2, N2]`. `(n2_before,
7234    /// N2_before)` is what the same source scored while `wrapper` was still
7235    /// classified as an operand. Both columns are checked against the
7236    /// *current* parse under the model "one spurious operand per wrapper
7237    /// node" — they are not measured against the old code, which no longer
7238    /// exists to run:
7239    ///
7240    /// - `N2_before - N2` must equal the number of `wrapper` nodes. That
7241    ///   identity *is* the defect: one spurious operand per wrapper.
7242    /// - `n2_before` must equal the post-fix operand vocabulary unioned
7243    ///   with the wrapper spellings. It exceeds `n2` wherever the wrapper's
7244    ///   whole text is not already an operand in its own right — either
7245    ///   because it differs from its single child's (`"$cmd"`, `${cmd}`,
7246    ///   `$"ls"`) or because it spans several (`foo$x`, `$(which ls)`,
7247    ///   `{1..3}`).
7248    ///
7249    /// So a mistyped or stale column fails rather than misinforming the next
7250    /// reader — one did, during review of #1351 — but neither check can
7251    /// catch a *production* regression, because both hold whatever
7252    /// `get_op_type` answers. `assert_halstead_counts` below is the only
7253    /// assertion here that fails when the wrapper arm comes back; measured
7254    /// by neutralising it under the pre-fix arm, which leaves both tables
7255    /// green.
7256    #[cfg(feature = "bash")]
7257    #[track_caller]
7258    fn assert_bash_wrapper_sheds_one_operand(
7259        source: &str,
7260        expected: [u64; 4],
7261        (n2_before, total_before): (u64, u64),
7262        wrapper: Bash,
7263    ) {
7264        let name: &'static str = wrapper.clone().into();
7265        let kind = wrapper as u16;
7266        let code = source.as_bytes();
7267        let parser = BashParser::new(code.to_vec(), &PathBuf::from("foo.sh"), None);
7268        let spellings: Vec<&str> = parser
7269            .root()
7270            .preorder()
7271            .filter(|node| node.kind_id() == kind)
7272            .filter_map(|node| node.utf8_text(code))
7273            .collect();
7274        assert!(
7275            !spellings.is_empty(),
7276            "row {source:?} parses without a {name}, so it witnesses nothing",
7277        );
7278        // Phrased as an addition rather than `total_before - expected[3]`: a
7279        // future edit that inverts the two would underflow `u64` and panic
7280        // with a raw overflow message instead of the one below.
7281        assert_eq!(
7282            total_before,
7283            expected[3] + spellings.len() as u64,
7284            "row {source:?} must shed exactly one operand per {name}; \
7285             recorded N2_before {total_before}",
7286        );
7287
7288        let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
7289        let mut vocabulary: HashSet<&str> = ops.operands.iter().map(String::as_str).collect();
7290        vocabulary.extend(spellings);
7291        assert_eq!(
7292            vocabulary.len() as u64,
7293            n2_before,
7294            "row {source:?}: n2 before the fix is the post-fix vocabulary \
7295             plus the {name} spellings; got {vocabulary:?}",
7296        );
7297
7298        assert_halstead_counts::<BashParser>(source, "foo.sh", expected, source);
7299    }
7300
7301    /// Regression for #1351, the command-name sibling of #695's bare
7302    /// `$x`. `command_name` is a pure wrapper: the grammar gives it
7303    /// exactly one required child (a `_primary_expression` or a
7304    /// `concatenation`) and it adds no text of its own, so classifying it
7305    /// as an operand *and* letting the walk reach the child counted every
7306    /// command name twice in `N2`.
7307    ///
7308    /// The table is the grammar-dispatch §6 evidence that deleting the arm
7309    /// zeroes nothing: it names every kind `command_name` can wrap, and in
7310    /// every row the command name still contributes at least one operand
7311    /// once the trailing `arg` is discounted.
7312    ///
7313    /// `n2_before` / `N2_before` are what each row scores with the
7314    /// `command_name` arm restored *against today's tree*, not a historical
7315    /// measurement — the `$"ls" arg` row has since lost a
7316    /// `translated_string` wrapper to #1358 as well.
7317    /// `assert_bash_wrapper_sheds_one_operand` re-derives both from the
7318    /// current parse rather than trusting them.
7319    #[cfg(feature = "bash")]
7320    #[test]
7321    fn bash_command_name_wrapper_no_double_count() {
7322        // (source, [n1, N1, n2, N2], (n2_before, N2_before))
7323        let cases: [(&str, [u64; 4], (u64, u64)); 14] = [
7324            // word
7325            ("ls bar\n", [0, 0, 2, 2], (2, 3)),
7326            // number
7327            ("1 arg\n", [0, 0, 2, 2], (2, 3)),
7328            // string, inert
7329            ("\"ls\" arg\n", [0, 0, 2, 2], (2, 3)),
7330            // string wrapping an expansion: the wrapper string is already
7331            // skipped (#180), so before #1351 the `command_name` was the
7332            // only thing counting the quoted spelling — which also planted
7333            // a spurious `"$cmd"` entry in n2 beside `$cmd`.
7334            ("\"$cmd\" arg\n", [0, 0, 2, 2], (3, 3)),
7335            // raw_string
7336            ("'ls' arg\n", [0, 0, 2, 2], (2, 3)),
7337            // ansi_c_string
7338            ("$'ls' arg\n", [0, 0, 2, 2], (2, 3)),
7339            // translated_string. Two wrappers stacked here — `command_name`
7340            // over `translated_string` — so the pre-#1351 column below is
7341            // measured against a tree #1358 has already thinned to
7342            // `"ls"` + `arg`.
7343            ("$\"ls\" arg\n", [0, 0, 2, 2], (3, 3)),
7344            // simple_expansion
7345            ("$cmd arg\n", [0, 0, 2, 2], (2, 3)),
7346            // brace expansion: counts through its inner `variable_name`,
7347            // whose `SimpleExpansion` parent guard does not apply here.
7348            ("${cmd} arg\n", [0, 0, 2, 2], (3, 3)),
7349            // command_substitution: counts through the nested command.
7350            // Two command names here — the outer one and `which`.
7351            ("$(which ls) arg\n", [0, 0, 3, 3], (4, 5)),
7352            // process_substitution, likewise two command names.
7353            ("<(ls) arg\n", [0, 0, 2, 2], (3, 4)),
7354            // arithmetic_expansion
7355            ("$((1+1)) arg\n", [1, 1, 2, 3], (3, 4)),
7356            // brace_expression
7357            ("{1..3} arg\n", [1, 1, 3, 3], (4, 4)),
7358            // concatenation
7359            ("foo$x arg\n", [0, 0, 3, 3], (4, 4)),
7360        ];
7361        for (source, expected, before) in cases {
7362            assert!(
7363                expected[3] > 1,
7364                "row {source:?} must leave the command name at least one \
7365                 operand beside `arg`; a zero here means the deleted arm \
7366                 was load-bearing for this spelling",
7367            );
7368            assert_bash_wrapper_sheds_one_operand(source, expected, before, Bash::CommandName);
7369        }
7370    }
7371
7372    /// Regression for #1358, the sibling of #1351 in the same `match`.
7373    /// `translated_string: $ => seq('$', $.string)` carries exactly one
7374    /// required `string` child and no text of its own beyond the `$`, so
7375    /// classifying the wrapper *and* letting the walk reach the child
7376    /// counted every `$"…"` twice in `N2`.
7377    ///
7378    /// The rows assert the total, `N2`. `n2` alone cannot witness this
7379    /// double count: the wrapper and its child spell *different* text
7380    /// (`$"x"` against `"x"`), so the pre-fix tree split one operand into
7381    /// two vocabulary entries rather than repeating one — which is why the
7382    /// `n2_before` column is carried too, and checked against the wrapper
7383    /// spellings the row actually parses to.
7384    #[cfg(feature = "bash")]
7385    #[test]
7386    fn bash_translated_string_wrapper_no_double_count() {
7387        // (source, [n1, N1, n2, N2], (n2_before, N2_before))
7388        let cases: [(&str, [u64; 4], (u64, u64)); 7] = [
7389            // Assignment RHS.
7390            ("a=$\"x\"\n", [1, 1, 2, 2], (3, 3)),
7391            // Empty translated string. The grammar makes the inner `string`
7392            // required, so the wrapper is never childless — the condition
7393            // grammar-dispatch §6 warns that deletion would regress.
7394            ("a=$\"\"\n", [1, 1, 2, 2], (3, 3)),
7395            // The one spelling the deleted arm *was* load-bearing for: the
7396            // inner `string` wraps an operandless brace `expansion`, so the
7397            // whole `$"…"` now scores zero. That is #1351's residue one
7398            // level down rather than a new gap — a plain `a="${#}"` already
7399            // scored `[1, 1, 1, 1]` — and
7400            // `bash_translated_string_scores_alike_in_both_positions` pins
7401            // that both positions agree on it.
7402            ("a=$\"${#}\"\n", [1, 1, 1, 1], (2, 2)),
7403            // `${!}` is the same residue plus a `!`, which the operator arm
7404            // counts; it must not turn into an operand along the way.
7405            ("a=$\"${!}\"\n", [2, 2, 1, 1], (2, 2)),
7406            // `case` subject.
7407            ("case $\"y\" in *) ;; esac\n", [4, 4, 1, 1], (2, 2)),
7408            // Command-name position, where #1351's wrapper stacks on top of
7409            // this one; the `_before` columns here price the
7410            // `translated_string` alone.
7411            ("$\"ls\" arg\n", [0, 0, 2, 2], (3, 3)),
7412            // Interpolating form. `bash_string_has_expansion` inspects the
7413            // node's own children, and a `translated_string` has only `$`
7414            // and `string`, so it never saw the `simple_expansion` one level
7415            // down: the wrapper reintroduced exactly the double count #180
7416            // removed, for the `$"…"` spelling alone. Only `$y` and `b` may
7417            // survive.
7418            ("b=$\"$y\"\n", [1, 1, 2, 2], (3, 3)),
7419        ];
7420        for (source, expected, before) in cases {
7421            assert_bash_wrapper_sheds_one_operand(source, expected, before, Bash::TranslatedString);
7422        }
7423    }
7424
7425    /// The load-bearing half of #1358's grammar-dispatch §6 argument, which
7426    /// nothing else asserts. In ordinary argument position the grammar emits
7427    /// no `translated_string` at all — just a `$` token and a `string` — so
7428    /// the `string` is the node present for *every* spelling, and dropping
7429    /// the wrapper makes the wrapper-bearing positions agree with argument
7430    /// position rather than newly disagree. If a grammar bump starts
7431    /// emitting the wrapper here, the deletion has to be re-derived.
7432    #[cfg(feature = "bash")]
7433    #[test]
7434    fn bash_translated_string_scores_alike_in_both_positions() {
7435        // `${#}` carries the residue: it contributes no operand of its own,
7436        // so the second row pins that the deletion left *both* positions at
7437        // zero rather than only the wrapper-bearing one.
7438        //
7439        // Only the command-name half moves under the pre-fix arm; the
7440        // argument-position half is the reference the parity is measured
7441        // against, and `cmd $"…"` is asserted nowhere else. Measured — with
7442        // the argument assertions dropped this test still fails pre-fix,
7443        // with the command-name ones dropped it passes — so do not
7444        // "simplify" it down to the failing half.
7445        for (spelling, expected) in [("$\"hi\"", [0, 0, 2, 2]), ("$\"${#}\"", [0, 0, 1, 1])] {
7446            let as_command_name = format!("{spelling} arg\n");
7447            let as_argument = format!("cmd {spelling}\n");
7448            let parse = |source: &str| {
7449                BashParser::new(source.as_bytes().to_vec(), &PathBuf::from("foo.sh"), None)
7450            };
7451            assert!(
7452                !ast_has_kind_id(&parse(&as_argument), Bash::TranslatedString as u16),
7453                "argument position must still parse {spelling} as a bare `$` \
7454                 plus a `string`; a grammar bump that emits the wrapper here \
7455                 invalidates the parity this test rests on",
7456            );
7457            assert!(
7458                ast_has_kind_id(&parse(&as_command_name), Bash::TranslatedString as u16),
7459                "command-name position must still emit the wrapper, or the \
7460                 pair witnesses nothing",
7461            );
7462            assert_halstead_counts::<BashParser>(
7463                &as_command_name,
7464                "foo.sh",
7465                expected,
7466                &as_command_name,
7467            );
7468            assert_halstead_counts::<BashParser>(&as_argument, "foo.sh", expected, &as_argument);
7469        }
7470    }
7471
7472    /// Pins the one residue of #1351: a brace `expansion` with no
7473    /// `variable_name` leaf. `${#}` (positional-parameter count) and `${!}`
7474    /// (last background PID) hold only anonymous tokens, and
7475    /// `Bash::Expansion` is not an operand, so the whole expansion
7476    /// contributes nothing.
7477    ///
7478    /// That was already true in *argument* position before the fix, which
7479    /// is the reason grammar-dispatch §6's gate-don't-delete rule did not
7480    /// apply to the `command_name` arm: deleting it made command-name
7481    /// position agree with argument position rather than newly disagree.
7482    /// The parity is the load-bearing half of that argument and nothing
7483    /// else asserts it, so if a future arm starts classifying these the
7484    /// two positions have to move together.
7485    #[cfg(feature = "bash")]
7486    #[test]
7487    fn bash_operandless_expansion_scores_alike_in_both_positions() {
7488        // `${!}` carries a `!`, which the operator arm counts; `${#}`'s `#`
7489        // is not an operator. Both positions must agree per spelling.
7490        for (spelling, expected) in [("${#}", [0, 0, 1, 1]), ("${!}", [1, 1, 1, 1])] {
7491            let as_command_name = format!("{spelling} arg\n");
7492            let as_argument = format!("cmd {spelling}\n");
7493            assert_halstead_counts::<BashParser>(
7494                &as_command_name,
7495                "foo.sh",
7496                expected,
7497                &as_command_name,
7498            );
7499            assert_halstead_counts::<BashParser>(&as_argument, "foo.sh", expected, &as_argument);
7500        }
7501    }
7502
7503    /// Drift marker for #1351 (lesson 34 / grammar-dispatch §2). `_concat`
7504    /// (`Bash::Concat`) is a hidden zero-width external token the scanner
7505    /// emits between `concatenation` parts; the parser never surfaces it as
7506    /// a node, and it spells no operand, so `BashCode::get_op_type` lists
7507    /// neither it nor the visible `concatenation` wrapper. If a grammar
7508    /// bump starts emitting it, this fails and the classification must be
7509    /// re-derived rather than assumed still absent.
7510    ///
7511    /// Measured, not assumed: putting `Bash::Concat` back in the operand
7512    /// arm fails no test in the suite, because the token is unreachable.
7513    /// Unreachability is the only coverage such an arm can have, which is
7514    /// why this test asserts it directly instead of asserting a count.
7515    #[cfg(feature = "bash")]
7516    #[test]
7517    fn bash_hidden_concat_token_is_unreachable() {
7518        let source = "a=foo$x\nb=pre\"$y\"post\ncmd bar$z\n";
7519        let path = PathBuf::from("foo.sh");
7520        let parser = BashParser::new(source.as_bytes().to_vec(), &path, None);
7521        // Non-vacuity: the visible `concatenation` this source is written
7522        // to produce must actually be in the parse, so the negative below
7523        // is about `_concat` and not about a source that concatenates
7524        // nothing.
7525        assert!(
7526            ast_has_kind_id(&parser, Bash::Concatenation as u16),
7527            "expected a visible `concatenation` node in the parse",
7528        );
7529        assert!(
7530            !ast_has_kind_id(&parser, Bash::Concat as u16),
7531            "the hidden `_concat` token surfaced; re-derive its \
7532             classification in BashCode::get_op_type against the new grammar",
7533        );
7534    }
7535
7536    #[cfg(feature = "tcl")]
7537    #[test]
7538    fn tcl_operators_and_operands() {
7539        check_metrics::<TclParser>(
7540            "proc f {a b} {
7541    set x [expr {$a + $b}]
7542    if {$x > 0 && $x != 0} {
7543        return $x
7544    }
7545    return 0
7546}",
7547            "foo.tcl",
7548            |metric| {
7549                // Anchored per the snapshot policy in AGENTS.md, which
7550                // this call predates. Operators `proc`, `set`, `[]`,
7551                // `{}`, `expr`, `+`, `if`, `>`, `&&`, `!=` → n1 = 10,
7552                // N1 = 14 (`{}` × 4 for the proc parameter list, the
7553                // proc body, the two `expr`/`if` conditions and the
7554                // `if` body — the `expr` braces are an `Expr`, the
7555                // bodies a `BracedWord`). Operands `f`, `a`, `b`, `x`,
7556                // `$a`, `$b`, `$x`, `0` and `return` → n2 = 9,
7557                // N2 = 14. Before #1354 the proc body and the `if`
7558                // body were operands too → 11 / 16.
7559                assert_eq!(metric.halstead.unique_operators(), 10);
7560                assert_eq!(metric.halstead.total_operators(), 14);
7561                assert_eq!(metric.halstead.unique_operands(), 9);
7562                assert_eq!(metric.halstead.total_operands(), 14);
7563                insta::assert_json_snapshot!(metric.halstead);
7564            },
7565        );
7566    }
7567
7568    #[cfg(feature = "tcl")]
7569    #[test]
7570    fn tcl_bitwise_ternary_string_ops() {
7571        // Exercises operator families not covered by tcl_operators_and_operands:
7572        // bitwise (&, |, ^, ~, <<, >>), ternary (?), and string-comparison (eq, ne, in, ni).
7573        check_metrics::<TclParser>(
7574            "proc f {a b} {
7575    set bits [expr {$a & $b | $a ^ ~$b}]
7576    set sh [expr {$a << 1 | $b >> 1}]
7577    set t [expr {$a > 0 ? $a : $b}]
7578    if {$a eq {x} || $a ne {y}} {
7579        return $a
7580    }
7581    return $b
7582}",
7583            "foo.tcl",
7584            |metric| {
7585                // Anchored per the snapshot policy in AGENTS.md, which
7586                // this call predates. N1 fell 33 → 31 with #1314: the
7587                // `if` condition's `{x}` and `{y}` are braced *words*,
7588                // so their openers stopped fabricating a `{}` operator.
7589                // n1 is unchanged at 18 because the `{}` entry survives
7590                // on the proc body and the `expr` braces — which is
7591                // exactly why the fabrication was invisible in n1 and
7592                // is the reason to assert N1 as well (#1294).
7593                //
7594                // The operand columns fell 17 / 30 → 13 / 26 with
7595                // #1354, and the operator columns did not move: the two
7596                // script bodies (the proc's and the `if`'s) stopped
7597                // being operands, and the `x` / `y` inside the braced
7598                // words `{x}` and `{y}` are now part of the one operand
7599                // each word contributes.
7600                assert_eq!(metric.halstead.unique_operators(), 18);
7601                assert_eq!(metric.halstead.total_operators(), 31);
7602                assert_eq!(metric.halstead.unique_operands(), 13);
7603                assert_eq!(metric.halstead.total_operands(), 26);
7604                insta::assert_json_snapshot!(metric.halstead);
7605            },
7606        );
7607    }
7608
7609    #[cfg(feature = "tcl")]
7610    #[test]
7611    fn tcl_array_reference_bills_the_reference_and_the_index() {
7612        // `$arr($i)` is the reference plus the index Tcl substitutes
7613        // inside the parens, and `arr(k)` as a `set` target is the name
7614        // plus the literal index; the `array_index` wrapper is neither.
7615        // The quoted spelling is deliberate — the vendored grammar
7616        // mis-parses a bare `$arr(k)` in command-word position. Pinned
7617        // per dialect and as a parity in `tests/parity/`, because the
7618        // iRules twin listed the wrapper as a third operand.
7619        assert_ops_operands::<TclParser>(
7620            "set arr(k) 1\nset z \"$arr($i)\"\n",
7621            "foo.tcl",
7622            6,
7623            vec!["arr", "k", "1", "z", "$arr($i)", "$i"],
7624        );
7625    }
7626
7627    #[cfg(feature = "tcl")]
7628    #[test]
7629    fn tcl_bare_variable_operand() {
7630        // Bare `$varname` produces a VariableSubstitution node (already an operand).
7631        // Its anonymous Id2 child must NOT be counted separately; each reference is 1 operand.
7632        check_metrics::<TclParser>(
7633            "proc f {x} {
7634    return $x
7635}",
7636            "foo.tcl",
7637            |metric| {
7638                // Anchored per the snapshot policy in AGENTS.md, which
7639                // this call predates. Operators `proc` and `{}` × 2
7640                // (the parameter list and the body) → n1 = 2, N1 = 3.
7641                // Operands `f`, the parameter `x`, `return` and `$x` —
7642                // one occurrence each, so a re-counted `x` leaf inside
7643                // `$x` would show up in N2 even though it collides with
7644                // the parameter in n2. Before #1354 the proc body was a
7645                // fifth operand.
7646                assert_eq!(metric.halstead.unique_operators(), 2);
7647                assert_eq!(metric.halstead.total_operators(), 3);
7648                assert_eq!(metric.halstead.unique_operands(), 4);
7649                assert_eq!(metric.halstead.total_operands(), 4);
7650                insta::assert_json_snapshot!(metric.halstead);
7651            },
7652        );
7653    }
7654
7655    #[cfg(feature = "tcl")]
7656    #[test]
7657    fn tcl_inert_quoted_word_counts_as_operand() {
7658        // Regression for #277. A `"..."` literal with no `$var` / `[cmd]`
7659        // interpolation must contribute exactly one operand (the wrapping
7660        // `QuotedWord`). The string content `hello world` is exposed as a
7661        // single `_quoted_word_content` token (not itself classified by
7662        // `get_op_type`), so the only operands here are `f`, `s`, and the
7663        // quoted string. `set` is the anonymous `Set2` keyword and is
7664        // classified as an operator, not an operand.
7665        check_metrics::<TclParser>(
7666            "proc f {} {
7667    set s \"hello world\"
7668}",
7669            "foo.tcl",
7670            |metric| {
7671                // Operands: `f`, the `set` target `s`, `"hello world"` —
7672                // 3 unique, 3 total. Before #1294 this read 3/3 for a
7673                // different reason, with `s` missing and the proc-body
7674                // `braced_word` making the count coincidentally
7675                // plausible; #1354 removed that body operand, so the
7676                // three named here are now the whole list. The wrapping
7677                // `QuotedWord` must still contribute exactly one operand
7678                // when it carries no interpolation children; dropping to 2
7679                // would mean the inert case was over-guarded.
7680                assert_eq!(metric.halstead.unique_operands(), 3);
7681                assert_eq!(metric.halstead.total_operands(), 3);
7682                insta::assert_json_snapshot!(metric.halstead);
7683            },
7684        );
7685    }
7686
7687    #[cfg(feature = "tcl")]
7688    #[test]
7689    fn tcl_interpolated_quoted_word_no_double_count() {
7690        // Regression for #277. Before the fix, `"$x is $y"` produced an
7691        // extra operand for the wrapping `QuotedWord` on top of the two
7692        // inner `VariableSubstitution` operands (`$x`, `$y`), giving 7.
7693        // After the fix, the wrapper is `TokenRole::Unknown` whenever
7694        // it carries an interpolation child, so operand attribution
7695        // belongs solely to the inner substitutions.
7696        check_metrics::<TclParser>(
7697            "proc f {x y} {
7698    set s \"$x is $y\"
7699}",
7700            "foo.tcl",
7701            |metric| {
7702                // Operands: `f`, `x`, `y` (proc args), the `set` target
7703                // `s`, `$x`, `$y` — 6 unique, 6 total. The wrapping
7704                // `QuotedWord` contributes nothing, and since #1354
7705                // neither does the proc-body `braced_word`. Before #277
7706                // the wrapper double-counted.
7707                assert_eq!(metric.halstead.unique_operands(), 6);
7708                assert_eq!(metric.halstead.total_operands(), 6);
7709                insta::assert_json_snapshot!(metric.halstead);
7710            },
7711        );
7712    }
7713
7714    #[cfg(feature = "tcl")]
7715    #[test]
7716    fn tcl_command_substitution_quoted_word_no_double_count() {
7717        // Regression for #277. A `"...[cmd]..."` literal exposes the
7718        // bracketed command as a `command_substitution` child whose inner
7719        // identifiers/literals contribute their own operands. The wrapping
7720        // `QuotedWord` must not also be classified as an operand, or the
7721        // command's identifier would be counted alongside a phantom
7722        // wrapper operand.
7723        check_metrics::<TclParser>(
7724            "proc f {} {
7725    set s \"result: [foo]\"
7726}",
7727            "foo.tcl",
7728            |metric| {
7729                // Operands: `f`, the `set` target `s`, `foo` — 3 unique,
7730                // 3 total. The wrapping `QuotedWord` and the inert text
7731                // `result: ` do not contribute extra operands, and since
7732                // #1354 neither does the proc-body `braced_word`. Before
7733                // #277 the wrapper double-counted.
7734                assert_eq!(metric.halstead.unique_operands(), 3);
7735                assert_eq!(metric.halstead.total_operands(), 3);
7736                insta::assert_json_snapshot!(metric.halstead);
7737            },
7738        );
7739    }
7740
7741    /// Regression for #1294. The `set` target parses as the anonymous
7742    /// `id` token (`Tcl::Id2`) — the same kind as the leaf inside a
7743    /// `variable_substitution` — and the getter used to exclude that kind
7744    /// wholesale, so every variable a Tcl script assigned was absent from
7745    /// n2/N2. The guard is now parent-scoped: a target `id` counts, a
7746    /// var-sub leaf does not. Exact occurrence counts distinguish this
7747    /// fix from a regression in either direction: a re-blanketed
7748    /// exclusion drops `s`/`t` (total 2), while losing the guard
7749    /// double-counts the `$s` leaf as a second `s` (total 5).
7750    #[cfg(feature = "tcl")]
7751    #[test]
7752    fn tcl_set_target_is_operand() {
7753        let source = "set s 1\nset t $s\n";
7754        let path = PathBuf::from("foo.tcl");
7755        let parser = TclParser::new(source.as_bytes().to_vec(), &path, None);
7756        let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
7757        // expected operands: targets `s` and `t`, literal `1`, reference
7758        // `$s` (wrapper only) — 4 total, each exactly once.
7759        for operand in ["s", "t", "1", "$s"] {
7760            assert_eq!(
7761                ops.operands
7762                    .iter()
7763                    .filter(|o| o.as_str() == operand)
7764                    .count(),
7765                1,
7766                "`{operand}` must be exactly one operand; operands were {:?}",
7767                ops.operands
7768            );
7769        }
7770        assert_eq!(
7771            ops.operands.len(),
7772            4,
7773            "operands must be exactly s, t, 1, $s; got {:?}",
7774            ops.operands
7775        );
7776
7777        check_metrics::<TclParser>(source, "foo.tcl", |metric| {
7778            // expected: n2 = 4 (s, t, 1, $s), N2 = 4; operators are the
7779            // two `set` keywords — n1 = 1, N1 = 2.
7780            assert_eq!(metric.halstead.unique_operands(), 4);
7781            assert_eq!(metric.halstead.total_operands(), 4);
7782            assert_eq!(metric.halstead.unique_operators(), 1);
7783            assert_eq!(metric.halstead.total_operators(), 2);
7784        });
7785    }
7786
7787    /// Drift marker for #1294 (lesson 34 / grammar-dispatch §2): the
7788    /// *named* `id` rule (`Tcl::Id`, kind_id 84) never surfaces at the
7789    /// pinned tree-sitter-tcl — the parser emits the anonymous `Id2` in
7790    /// both positions the getter guards (the `set` target and the
7791    /// var-sub leaf). The `Tcl::Id` arm in `get_op_type` is therefore
7792    /// defensive; if a grammar bump starts emitting 84 this fails and
7793    /// the arm's classification must be re-derived instead of trusted.
7794    #[cfg(feature = "tcl")]
7795    #[test]
7796    fn tcl_named_id_variant_is_unreachable() {
7797        let source = "proc f {x} {\n    set s $x\n    foreach v {1 2} { puts \"$v\" }\n}\n";
7798        let path = PathBuf::from("foo.tcl");
7799        let parser = TclParser::new(source.as_bytes().to_vec(), &path, None);
7800        // Non-vacuity: the anonymous token must be present in this parse
7801        // (both the `set` target and the `$x` / `$v` leaves emit it).
7802        assert!(
7803            ast_has_kind_id(&parser, Tcl::Id2 as u16),
7804            "expected the anonymous Tcl::Id2 token to appear in the parse",
7805        );
7806        assert!(
7807            !ast_has_kind_id(&parser, Tcl::Id as u16),
7808            "the named Tcl::Id rule surfaced; re-derive the defensive \
7809             `Tcl::Id` arm in TclCode::get_op_type against the new grammar",
7810        );
7811    }
7812
7813    #[cfg(feature = "tcl")]
7814    #[test]
7815    fn tcl_braced_word_delimiter_is_not_an_operator() {
7816        // Regression: issue #1314, the Tcl sibling of Elixir #1256 and
7817        // Ruby/Perl #1312. A braced *word* — a literal value, not a
7818        // script — carries its `{` as an `LBRACE` child, the kind id a
7819        // real block uses, so `set a {braced word}` reported a `{}`
7820        // operator with no block in the source.
7821        //
7822        // expected: operator `set` × 3 → n1 = 1, N1 = 3. Operands
7823        // `a`, `b`, `c`, `$a` and `{braced word}` × 2 → n2 = 5,
7824        // N2 = 6. Before the guard the two openers added `{}` →
7825        // n1 = 2, N1 = 5; before #1354 the inner `braced` / `word`
7826        // counted alongside the word containing them → n2 = 7,
7827        // N2 = 10.
7828        check_metrics::<TclParser>(
7829            "set a {braced word}\nset b {braced word}\nset c $a\n",
7830            "foo.tcl",
7831            |metric| {
7832                assert_eq!(metric.halstead.unique_operators(), 1);
7833                assert_eq!(metric.halstead.total_operators(), 3);
7834                assert_eq!(metric.halstead.unique_operands(), 5);
7835                assert_eq!(metric.halstead.total_operands(), 6);
7836            },
7837        );
7838    }
7839
7840    #[cfg(feature = "tcl")]
7841    #[test]
7842    fn tcl_script_bodies_keep_their_braces() {
7843        // Control for #1314, and the reason a kind-scoped guard is safe
7844        // in Tcl where it would not be elsewhere: the grammar gives the
7845        // literal and the block *different* kinds. A `proc` body, an
7846        // `if` body and an `if` condition are `BracedWord` (88) and
7847        // `Expr` (97); only the value form is `BracedWordSimple` (89).
7848        // This fixture nests a braced word inside a real script body,
7849        // so a guard that keyed on the brace alone would drop the
7850        // block's `{}` and fail here.
7851        //
7852        // expected: operators `proc`, `set` × 2, `if`, `>`, and `{}`
7853        // × 4 (the proc parameter list, the proc body, the `if`
7854        // condition, the `if` body) → n1 = 5, N1 = 9. Operands, all
7855        // distinct → n2 = N2 = 8: `p`, `x`, `a`, `$x`, `1`, `b` and
7856        // the two braced *words* `{v w}` and `{y}`, each one operand
7857        // rather than one per inner word.
7858        //
7859        // Before #1354 this read 13 / 13. The five extra entries were
7860        // the inner words `v`, `w`, `y` and the two *script* bodies,
7861        // which were `BracedWord` operands in their own right — so a
7862        // block counted twice over, once as the operand and once as
7863        // the `{}` operator this test is about. Both halves are gone;
7864        // the operator columns are what this test guards and they did
7865        // not move.
7866        check_metrics::<TclParser>(
7867            "proc p {x} {\n  set a {v w}\n  if {$x > 1} { set b {y} }\n}\n",
7868            "foo.tcl",
7869            |metric| {
7870                assert_eq!(metric.halstead.unique_operators(), 5);
7871                assert_eq!(metric.halstead.total_operators(), 9);
7872                assert_eq!(metric.halstead.unique_operands(), 8);
7873                assert_eq!(metric.halstead.total_operands(), 8);
7874            },
7875        );
7876    }
7877
7878    #[cfg(feature = "tcl")]
7879    #[test]
7880    fn tcl_braced_word_guard_is_parent_scoped_not_ancestor_scoped() {
7881        // The input that separates the parent-scoped guard from the
7882        // ancestor-scanning mutant. I first recorded this distinction
7883        // as *unobservable* in Tcl, reasoning that a braced word holds
7884        // only simple words and nested braced words. `bca dump` says
7885        // otherwise: the grammar parses a `[…]` command substitution
7886        // inside a braced word, and the `if` inside it brings an `Expr`
7887        // condition and a `BracedWord` body, each with its own `{`,
7888        // both of them non-immediate descendants of the
7889        // `BracedWordSimple`. An ancestor scan swallows both.
7890        //
7891        // (Real Tcl does not substitute inside braces — this is the
7892        // grammar modelling structure it will not evaluate. What the
7893        // classifier sees is what the metric reports, so it is the
7894        // right fixture regardless.)
7895        //
7896        // expected: operators `set`, `[]`, `if`, and `{}` × 2 (the
7897        // `if` condition's `Expr` and its `BracedWord` body; the outer
7898        // value word's own `{` is suppressed) → n1 = 4, N1 = 5.
7899        // Operands `z`, the whole braced word, and `$q` / `puts` / `w`
7900        // from inside the command substitution → n2 = N2 = 5. Under
7901        // the ancestor-scoped mutant both surviving braces vanish:
7902        // n1 = 3, N1 = 3.
7903        //
7904        // #1354 widened the guard from the `{` alone to every direct
7905        // child of the braced word, which is why `x` and `v` are no
7906        // longer operands and the nested script body no longer is
7907        // either. It did not change the *scope*: the three operands
7908        // from inside the command substitution are grandchildren, and
7909        // an ancestor-scoped guard would drop them too.
7910        check_metrics::<TclParser>("set z {x [if {$q} {puts w}] v}\n", "foo.tcl", |metric| {
7911            assert_eq!(metric.halstead.unique_operators(), 4);
7912            assert_eq!(metric.halstead.total_operators(), 5);
7913            assert_eq!(metric.halstead.unique_operands(), 5);
7914            assert_eq!(metric.halstead.total_operands(), 5);
7915        });
7916    }
7917
7918    #[cfg(feature = "irules")]
7919    #[test]
7920    fn irules_braced_word_guard_is_parent_scoped_not_ancestor_scoped() {
7921        // The iRules twin of the test above — the two getters are
7922        // clones, so the mutant must fail in both.
7923        //
7924        // expected: operators `when`, `set`, `[]`, `if`, `{}` × 3 (the
7925        // handler body, the `if` condition and the `if` body) → n1 = 5,
7926        // N1 = 7. Operands `HTTP_REQUEST`, `z`, the whole braced word,
7927        // and `$q` / `log` / `w` from inside the command substitution →
7928        // n2 = N2 = 6 (10 before #1354, which also took the direct
7929        // children `x` / `v` and the two script bodies out of the
7930        // operand set without moving the operator columns this test
7931        // guards).
7932        check_metrics::<IrulesParser>(
7933            "when HTTP_REQUEST {\n  set z {x [if {$q} {log w}] v}\n}\n",
7934            "foo.irule",
7935            |metric| {
7936                assert_eq!(metric.halstead.unique_operators(), 5);
7937                assert_eq!(metric.halstead.total_operators(), 7);
7938                assert_eq!(metric.halstead.unique_operands(), 6);
7939                assert_eq!(metric.halstead.total_operands(), 6);
7940            },
7941        );
7942    }
7943
7944    /// How one dialect of the Tcl family spells the braced-word
7945    /// construct. The two grammars are deliberate clones with different
7946    /// id blocks, so #1354's guard is derived once and instantiated
7947    /// twice — and a fix that landed in only one dialect fails the
7948    /// second instantiation rather than going unnoticed.
7949    struct BracedWordKinds {
7950        /// `braced_word_simple`, the literal *value* form the guard
7951        /// keys on.
7952        wrapper: u16,
7953        /// `braced_word`, the *script* form #1354 gated on holding a
7954        /// command: an operand only when it holds none.
7955        script_body: u16,
7956        /// `comment`, the one named child of a script that is not a
7957        /// command and that the gate must not mistake for one.
7958        comment: u16,
7959        /// Every named kind node-types.json admits directly inside
7960        /// `wrapper`. A child outside this set means the grammar moved
7961        /// and the parent-keyed arm has to be re-derived
7962        /// (grammar-dispatch §1).
7963        children: [u16; 6],
7964        /// The `{` / `}` the wrapper also holds. Suppressing the opener
7965        /// was the whole of #1314's narrower guard; the closer has
7966        /// never been classified, since `get_operator_id_as_str` folds
7967        /// the pair to one `{}` glyph.
7968        delimiters: [u16; 2],
7969        /// Of `children`, the three the operand arm classified
7970        /// unconditionally before the guard.
7971        operand_children: [u16; 3],
7972        /// `quoted_word`, which was an operand only when *inert* —
7973        /// `string_operand_type`'s own rule, replicated here so the
7974        /// shed count below is what `N2` actually shed.
7975        quoted_word: u16,
7976        /// The interpolation kinds that decide that.
7977        interpolation: [u16; 2],
7978    }
7979
7980    #[cfg(feature = "tcl")]
7981    const TCL_BRACED_WORD_KINDS: BracedWordKinds = BracedWordKinds {
7982        wrapper: Tcl::BracedWordSimple as u16,
7983        script_body: Tcl::BracedWord as u16,
7984        comment: Tcl::Comment as u16,
7985        children: [
7986            Tcl::SimpleWord as u16,
7987            Tcl::EscapedCharacter as u16,
7988            Tcl::QuotedWord as u16,
7989            Tcl::VariableSubstitution as u16,
7990            Tcl::CommandSubstitution as u16,
7991            Tcl::BracedWordSimple as u16,
7992        ],
7993        delimiters: [Tcl::LBRACE as u16, Tcl::RBRACE as u16],
7994        operand_children: [
7995            Tcl::SimpleWord as u16,
7996            Tcl::VariableSubstitution as u16,
7997            Tcl::BracedWordSimple as u16,
7998        ],
7999        quoted_word: Tcl::QuotedWord as u16,
8000        interpolation: [
8001            Tcl::VariableSubstitution as u16,
8002            Tcl::CommandSubstitution as u16,
8003        ],
8004    };
8005
8006    #[cfg(feature = "irules")]
8007    const IRULES_BRACED_WORD_KINDS: BracedWordKinds = BracedWordKinds {
8008        wrapper: Irules::BracedWordSimple as u16,
8009        script_body: Irules::BracedWord as u16,
8010        comment: Irules::Comment as u16,
8011        children: [
8012            Irules::SimpleWord as u16,
8013            Irules::EscapedCharacter as u16,
8014            Irules::QuotedWord as u16,
8015            Irules::VariableSubstitution as u16,
8016            Irules::CommandSubstitution as u16,
8017            Irules::BracedWordSimple as u16,
8018        ],
8019        delimiters: [Irules::LBRACE as u16, Irules::RBRACE as u16],
8020        operand_children: [
8021            Irules::SimpleWord as u16,
8022            Irules::VariableSubstitution as u16,
8023            Irules::BracedWordSimple as u16,
8024        ],
8025        quoted_word: Irules::QuotedWord as u16,
8026        interpolation: [
8027            Irules::VariableSubstitution as u16,
8028            Irules::CommandSubstitution as u16,
8029        ],
8030    };
8031
8032    /// The operand occurrences #1354 removed from `source`, as their
8033    /// source texts, plus the set of `braced_word_simple` child kinds
8034    /// the fixture witnessed.
8035    ///
8036    /// Walks with `for_each_node_with_chain`, which maintains the
8037    /// ancestor chain exactly as `spaces::compute` does, so "parent"
8038    /// here means what `Ancestors::parent` means inside the guard.
8039    /// Doubles as the grammar-dispatch §1 / §2 drift marker: a child of
8040    /// the wrapper outside `children` ∪ `delimiters` fails on the spot,
8041    /// which is what makes keying the arm on the parent alone — rather
8042    /// than on an enumerated child list — safe to rely on.
8043    #[cfg(any(feature = "irules", feature = "tcl"))]
8044    fn braced_word_shed<L: crate::LanguageInfo>(
8045        source: &str,
8046        kinds: &BracedWordKinds,
8047    ) -> (Vec<String>, HashSet<u16>) {
8048        let code = source.as_bytes();
8049        let mut shed = Vec::new();
8050        let mut witnessed = HashSet::new();
8051        for_each_node_with_chain::<L>(code, |node, chain| {
8052            let text = || {
8053                node.utf8_text(code)
8054                    .expect("fixture is valid UTF-8")
8055                    .to_owned()
8056            };
8057            // The script form was an operand of its own wherever it
8058            // appeared until #1354, which now gates it on holding a
8059            // command — a named child that is not a comment. This is not
8060            // a child of the wrapper, so it is counted before the parent
8061            // test below.
8062            if node.kind_id() == kinds.script_body
8063                && node
8064                    .children()
8065                    .any(|child| child.is_named() && child.kind_id() != kinds.comment)
8066            {
8067                shed.push(text());
8068            }
8069            if chain.last().is_none_or(|p| p.kind_id() != kinds.wrapper) {
8070                return;
8071            }
8072            assert!(
8073                kinds.children.contains(&node.kind_id())
8074                    || kinds.delimiters.contains(&node.kind_id()),
8075                "`{source}`: a `{}` inside a braced word is a child this guard \
8076                 was not derived against; re-read node-types.json before \
8077                 trusting it",
8078                node.kind(),
8079            );
8080            if kinds.children.contains(&node.kind_id()) {
8081                witnessed.insert(node.kind_id());
8082            }
8083            let was_operand = kinds.operand_children.contains(&node.kind_id())
8084                || (node.kind_id() == kinds.quoted_word && !node.wraps_any(&kinds.interpolation));
8085            if was_operand {
8086                shed.push(text());
8087            }
8088        });
8089        (shed, witnessed)
8090    }
8091
8092    /// One row of the #1354 tables: a fixture, what it measures now,
8093    /// what it measured before, and the operand texts behind the counts.
8094    struct BracedWordCase {
8095        source: &'static str,
8096        /// `[n1, N1, n2, N2]` with the guard in place.
8097        counts: [u64; 4],
8098        /// `[n2, N2]` without it. Re-derived by the loop rather than
8099        /// trusted, so a stale row fails instead of misinforming.
8100        before: [u64; 2],
8101        operands: &'static [&'static str],
8102    }
8103
8104    /// Every row is measured in *both* dialects, so a fix applied to
8105    /// one getter and not its clone fails here. `braced_word_shed`'s
8106    /// drift assertion likewise runs against both grammars.
8107    #[cfg(any(feature = "irules", feature = "tcl"))]
8108    fn check_braced_word_cases<T: crate::MetricSuite, L: crate::LanguageInfo>(
8109        cases: &[BracedWordCase],
8110        file: &str,
8111        kinds: &BracedWordKinds,
8112    ) -> HashSet<u16> {
8113        let mut witnessed = HashSet::new();
8114        for case in cases {
8115            let (shed, seen) = braced_word_shed::<L>(case.source, kinds);
8116            witnessed.extend(seen);
8117
8118            // Phrased as an addition rather than a subtraction so a
8119            // future edit that inverts the two underflows nothing.
8120            assert_eq!(
8121                case.before[1],
8122                case.counts[3] + shed.len() as u64,
8123                "{file} `{}`: N2 must shed exactly one occurrence per \
8124                 previously-billed part; recorded {}, parts {shed:?}",
8125                case.source,
8126                case.before[1],
8127            );
8128            let mut vocabulary: HashSet<&str> = case.operands.iter().copied().collect();
8129            vocabulary.extend(shed.iter().map(String::as_str));
8130            assert_eq!(
8131                vocabulary.len() as u64,
8132                case.before[0],
8133                "{file} `{}`: n2 before the fix is the post-fix vocabulary \
8134                 plus those parts; got {vocabulary:?}",
8135                case.source,
8136            );
8137
8138            assert_halstead_counts::<T>(case.source, file, case.counts, case.source);
8139            assert_ops_operands::<T>(
8140                case.source,
8141                file,
8142                case.operands.len(),
8143                case.operands.to_vec(),
8144            );
8145        }
8146        witnessed
8147    }
8148
8149    /// The rows shared by both dialects: one per named child kind
8150    /// `braced_word_simple` admits, the childless spelling, the
8151    /// repeated-value row that separates `n2` from `N2` (#1294), and
8152    /// the braced/quoted parity pair #1317 asks for.
8153    #[cfg(any(feature = "irules", feature = "tcl"))]
8154    const BRACED_WORD_CASES: [BracedWordCase; 11] = [
8155        // simple_word, the reported fixture. Two words inside one
8156        // value scored two operands beside the value itself.
8157        BracedWordCase {
8158            source: "set x {literal here}\n",
8159            counts: [1, 1, 2, 2],
8160            before: [4, 4],
8161            operands: &["x", "{literal here}"],
8162        },
8163        // The childless spelling, and the reason the wrapper is kept
8164        // rather than dropped in favour of its contents
8165        // (grammar-dispatch §6): with nothing inside, the wrapper is
8166        // the empty string's only carrier. Nothing is shed here, so
8167        // this row asserts the two columns agree.
8168        BracedWordCase {
8169            source: "set y {}\n",
8170            counts: [1, 1, 2, 2],
8171            before: [2, 2],
8172            operands: &["y", "{}"],
8173        },
8174        // braced_word_simple inside braced_word_simple: the nesting
8175        // that makes the over-count unbounded in depth. One value
8176        // spelled six vocabulary entries.
8177        BracedWordCase {
8178            source: "set a {x {y z}}\n",
8179            counts: [1, 1, 2, 2],
8180            before: [6, 6],
8181            operands: &["a", "{x {y z}}"],
8182        },
8183        // quoted_word, inert — an operand in its own right elsewhere,
8184        // and shed here.
8185        BracedWordCase {
8186            source: "set a {x \"q w\" v}\n",
8187            counts: [1, 1, 2, 2],
8188            before: [5, 5],
8189            operands: &["a", "{x \"q w\" v}"],
8190        },
8191        // quoted_word carrying an interpolation, which was *not* an
8192        // operand before the guard either (`string_operand_type` had
8193        // already suppressed it) — so only `x` and `v` are shed. Its
8194        // `$q` is a grandchild of the braced word and still counts,
8195        // which is the parent-scoping this arm inherits from #1314.
8196        BracedWordCase {
8197            source: "set a {x \"$q\" v}\n",
8198            counts: [1, 1, 3, 3],
8199            before: [5, 5],
8200            operands: &["a", "$q", "{x \"$q\" v}"],
8201        },
8202        // escaped_character, never classified — so it sheds nothing
8203        // and the row measures the two `simple_word`s around it.
8204        BracedWordCase {
8205            source: "set a {x \\n y}\n",
8206            counts: [1, 1, 2, 2],
8207            before: [4, 4],
8208            operands: &["a", "{x \\n y}"],
8209        },
8210        // variable_substitution. Tcl substitutes nothing between
8211        // braces, so `{$x}` is the two-character string `$x` — the row
8212        // that makes "the wrapper is the value" more than a tie-break.
8213        BracedWordCase {
8214            source: "set a {$x}\n",
8215            counts: [1, 1, 2, 2],
8216            before: [3, 3],
8217            operands: &["a", "{$x}"],
8218        },
8219        // command_substitution, likewise never an operand itself. Its
8220        // interior is, and stays so: `$q`, `puts` and `w` are
8221        // grandchildren. The nested `{puts w}` is a *script* body and
8222        // sheds under the other half of #1354.
8223        BracedWordCase {
8224            source: "set z {x [if {$q} {puts w}] v}\n",
8225            counts: [4, 5, 5, 5],
8226            before: [8, 8],
8227            operands: &["z", "$q", "puts", "w", "{x [if {$q} {puts w}] v}"],
8228        },
8229        // The same value twice: n2 3 against N2 4, so a row that
8230        // asserted only the vocabulary could not tell the two axes
8231        // apart (#1294).
8232        BracedWordCase {
8233            source: "set a {b c}\nset d {b c}\n",
8234            counts: [1, 2, 3, 4],
8235            before: [5, 8],
8236            operands: &["a", "d", "{b c}"],
8237        },
8238        // The parity pair #1317 named: two spellings of one literal
8239        // value must score alike. They did not before — braced 4 / 4
8240        // against quoted 2 / 2 — which is the spelling sensitivity
8241        // #695, #1312 and #1314 each removed elsewhere.
8242        BracedWordCase {
8243            source: "set a {one two}\n",
8244            counts: [1, 1, 2, 2],
8245            before: [4, 4],
8246            operands: &["a", "{one two}"],
8247        },
8248        BracedWordCase {
8249            source: "set a \"one two\"\n",
8250            counts: [1, 1, 2, 2],
8251            before: [2, 2],
8252            operands: &["a", "\"one two\""],
8253        },
8254    ];
8255
8256    /// The script half of #1354, shared by both dialects: a
8257    /// `braced_word` holding commands is a block whose contents the
8258    /// walk already counts, so it is no longer also an operand
8259    /// spanning the whole block.
8260    ///
8261    /// The three childless rows are the gate, and the reason this is a
8262    /// gate and not a deletion (grammar-dispatch §6). `braced_word` is
8263    /// not only the script kind: it is the value slot of every command
8264    /// the grammar does not special-case, where `lappend l {}` is an
8265    /// empty list whose brace pair is its only carrier. Deleting the
8266    /// kind scored that zero while its `lappend l ""` synonym — the
8267    /// last row, the control — scored one. An empty `proc` body is
8268    /// spelled identically and so also keeps an operand; #1318 tells
8269    /// the two roles apart by the enclosing command, but it bills them
8270    /// alike, so this gate is still what keeps the operand.
8271    ///
8272    /// The rows' *operator* columns moved with #1318 wherever the
8273    /// enclosing command takes a value, which is why the `lappend`
8274    /// rows here read `0` where the `proc` and `if` rows read the
8275    /// block's `{}`. The operand columns are untouched by it — #1318
8276    /// revises the brace and nothing else — so every `before` here
8277    /// still describes #1354 alone.
8278    #[cfg(any(feature = "irules", feature = "tcl"))]
8279    const SCRIPT_BODY_CASES: [BracedWordCase; 9] = [
8280        BracedWordCase {
8281            source: "proc p {} { set b 1 }\n",
8282            counts: [3, 4, 3, 3],
8283            before: [4, 4],
8284            operands: &["p", "b", "1"],
8285        },
8286        BracedWordCase {
8287            source: "proc p {} {}\n",
8288            counts: [2, 3, 2, 2],
8289            before: [2, 2],
8290            operands: &["p", "{}"],
8291        },
8292        // A comment is a named child of the script but not a command,
8293        // and no arm bills it, so a comment-only body scores like the
8294        // empty one above rather than like nothing: the block's whole
8295        // text is its one operand. Gating on "any named child" billed
8296        // it zero — adding a comment to an empty block lowered N2.
8297        BracedWordCase {
8298            source: "proc p {} {\n    # only a comment\n}\n",
8299            counts: [2, 3, 2, 2],
8300            before: [2, 2],
8301            operands: &["p", "{\n    # only a comment\n}"],
8302        },
8303        BracedWordCase {
8304            source: "if {$q} {\n    # noop\n}\n",
8305            counts: [2, 3, 2, 2],
8306            before: [2, 2],
8307            operands: &["$q", "{\n    # noop\n}"],
8308        },
8309        // The control: a comment *beside* a command changes nothing, so a
8310        // gate keyed on "contains a comment" would fail this row.
8311        BracedWordCase {
8312            source: "proc p {} {\n    # c\n    set b 1\n}\n",
8313            counts: [3, 4, 3, 3],
8314            before: [4, 4],
8315            operands: &["p", "b", "1"],
8316        },
8317        // The value-role twin: inside a literal string Tcl performs no
8318        // substitution, so `# x` is not a comment at all, and the word
8319        // is its one operand exactly as `lappend l {}` below is. Its
8320        // operator columns are zero since #1318 — `lappend` takes a
8321        // value, so the brace pair is a quote and not a block.
8322        BracedWordCase {
8323            source: "lappend l {\n    # x\n}\n",
8324            counts: [0, 0, 3, 3],
8325            before: [3, 3],
8326            operands: &["lappend", "l", "{\n    # x\n}"],
8327        },
8328        // A `braced_word` in *value* position — the #1318 role, where
8329        // the same kind carries a literal list. Its interior words
8330        // counted before #1354 and still do; what #1354 removed is the
8331        // whole-block operand that was double-billing them, and what
8332        // #1318 removed is the `{}` operator (`lappend` takes a value,
8333        // so the brace pair quotes rather than opens).
8334        BracedWordCase {
8335            source: "lappend l {a b}\n",
8336            counts: [0, 0, 4, 4],
8337            before: [5, 5],
8338            operands: &["lappend", "l", "a", "b"],
8339        },
8340        // …and the childless spelling of that value, which sheds
8341        // nothing: the brace pair is the empty list's only carrier.
8342        BracedWordCase {
8343            source: "lappend l {}\nreturn {}\n",
8344            counts: [0, 0, 4, 5],
8345            before: [4, 5],
8346            operands: &["lappend", "l", "return", "{}"],
8347        },
8348        // The control the row above is measured against: the quoted
8349        // spelling of the same empty value, which never depended on
8350        // the arm and must keep scoring one operand. Since #1318 the
8351        // two rows agree on all four columns rather than only on the
8352        // operand pair — an empty list is an empty list however it is
8353        // spelled.
8354        BracedWordCase {
8355            source: "lappend l \"\"\nreturn \"\"\n",
8356            counts: [0, 0, 4, 5],
8357            before: [4, 5],
8358            operands: &["lappend", "l", "return", "\"\""],
8359        },
8360    ];
8361
8362    /// The table must exercise every named child kind the grammar
8363    /// admits inside a braced word, and no other — the other half of
8364    /// the drift marker in `braced_word_shed`, which can only police
8365    /// kinds a fixture actually produces.
8366    #[cfg(any(feature = "irules", feature = "tcl"))]
8367    fn assert_braced_word_children_witnessed(
8368        witnessed: &HashSet<u16>,
8369        kinds: &BracedWordKinds,
8370        dialect: &str,
8371    ) {
8372        let mut got: Vec<u16> = witnessed.iter().copied().collect();
8373        got.sort_unstable();
8374        let mut expected = kinds.children;
8375        expected.sort_unstable();
8376        assert_eq!(
8377            got.as_slice(),
8378            expected.as_slice(),
8379            "{dialect}: the fixtures must witness every child kind \
8380             node-types.json admits inside a braced word",
8381        );
8382    }
8383
8384    /// Regression for #1354 and #1317. `braced_word_simple` is a
8385    /// literal value and `braced_word` a script, and both were
8386    /// operands *beside* the content the walk counts anyway: a braced
8387    /// word was billed once per inner word plus once for itself, and a
8388    /// block once for every command in it plus once for its whole
8389    /// text. `set x {literal here}` scored n2 4 / N2 4 for one value.
8390    /// The name is the invariant either way — the value's content is
8391    /// the literal, the script's is its commands, and each is billed
8392    /// once.
8393    ///
8394    /// Each row's `before` column is re-derived by the loop from the
8395    /// current parse rather than trusted. That derivation is a replica
8396    /// of the pre-#1354 arms and could in principle drift from what
8397    /// they did, so every column was also measured directly against a
8398    /// build of the old getters — `lappend l {a b}` at n2 5 / N2 5,
8399    /// `lappend l {}` at 4 / 5, `proc p {} {}` at 2 / 2 — rather than
8400    /// derived only from the model here.
8401    ///
8402    /// The same walk doubles as the drift marker for the parent-keyed
8403    /// arm: it fails if the grammar ever puts a seventh kind directly
8404    /// inside a braced word, and the union assertion below fails if a
8405    /// bump stops emitting one of the six.
8406    #[cfg(feature = "tcl")]
8407    #[test]
8408    fn tcl_braced_word_bills_its_content_once_1354() {
8409        let mut witnessed = check_braced_word_cases::<TclParser, TclCode>(
8410            &BRACED_WORD_CASES,
8411            "foo.tcl",
8412            &TCL_BRACED_WORD_KINDS,
8413        );
8414        witnessed.extend(check_braced_word_cases::<TclParser, TclCode>(
8415            &SCRIPT_BODY_CASES,
8416            "foo.tcl",
8417            &TCL_BRACED_WORD_KINDS,
8418        ));
8419        assert_braced_word_children_witnessed(&witnessed, &TCL_BRACED_WORD_KINDS, "tcl");
8420    }
8421
8422    /// The iRules twin. The tables are shared, so a fix that reached
8423    /// only `big-code-analysis-ast/src/getter/tcl.rs` fails every row
8424    /// here — the two getters are deliberate clones and #1354 names
8425    /// both.
8426    #[cfg(feature = "irules")]
8427    #[test]
8428    fn irules_braced_word_bills_its_content_once_1354() {
8429        let mut witnessed = check_braced_word_cases::<IrulesParser, IrulesCode>(
8430            &BRACED_WORD_CASES,
8431            "foo.irule",
8432            &IRULES_BRACED_WORD_KINDS,
8433        );
8434        witnessed.extend(check_braced_word_cases::<IrulesParser, IrulesCode>(
8435            &SCRIPT_BODY_CASES,
8436            "foo.irule",
8437            &IRULES_BRACED_WORD_KINDS,
8438        ));
8439        // The `when` handler body is the iRules-only spelling of a
8440        // script body, and the largest instance of the defect: its
8441        // operand text was the entire event handler.
8442        let handlers: [BracedWordCase; 3] = [
8443            BracedWordCase {
8444                source: "when HTTP_REQUEST { set x 1 }\n",
8445                counts: [3, 3, 3, 3],
8446                before: [4, 4],
8447                operands: &["HTTP_REQUEST", "x", "1"],
8448            },
8449            BracedWordCase {
8450                source: "when HTTP_REQUEST {}\n",
8451                counts: [2, 2, 2, 2],
8452                before: [2, 2],
8453                operands: &["HTTP_REQUEST", "{}"],
8454            },
8455            // The comment-only twin of the empty handler above; see the
8456            // shared table for why it scores like it.
8457            BracedWordCase {
8458                source: "when HTTP_REQUEST {\n    # only a comment\n}\n",
8459                counts: [2, 2, 2, 2],
8460                before: [2, 2],
8461                operands: &["HTTP_REQUEST", "{\n    # only a comment\n}"],
8462            },
8463        ];
8464        witnessed.extend(check_braced_word_cases::<IrulesParser, IrulesCode>(
8465            &handlers,
8466            "foo.irule",
8467            &IRULES_BRACED_WORD_KINDS,
8468        ));
8469        assert_braced_word_children_witnessed(&witnessed, &IRULES_BRACED_WORD_KINDS, "irules");
8470    }
8471
8472    /// One row of the #1318 tables: a fixture and the four Halstead
8473    /// columns plus the operand vocabulary behind them.
8474    ///
8475    /// No `before` column, unlike [`BracedWordCase`]: #1318 is not a
8476    /// guard whose absence a walk can model, it is a *classification*.
8477    /// The pre-fix counts are recorded per row in prose instead, and
8478    /// each was measured against a build without the
8479    /// `get_op_type_with_code` overrides.
8480    struct BracedWordValueCase {
8481        source: &'static str,
8482        /// `[n1, N1, n2, N2]`.
8483        counts: [u64; 4],
8484        operands: &'static [&'static str],
8485    }
8486
8487    /// Runs one #1318 table against one dialect. Both dialects run
8488    /// every shared row, so a fix that reached one getter and not its
8489    /// clone fails here.
8490    #[cfg(any(feature = "irules", feature = "tcl"))]
8491    fn check_braced_word_value_cases<T: crate::MetricSuite>(
8492        cases: &[BracedWordValueCase],
8493        file: &str,
8494    ) {
8495        for case in cases {
8496            assert_halstead_counts::<T>(case.source, file, case.counts, case.source);
8497            assert_ops_operands::<T>(
8498                case.source,
8499                file,
8500                case.operands.len(),
8501                case.operands.to_vec(),
8502            );
8503        }
8504    }
8505
8506    /// The #1318 rows both dialects share. Every independent path
8507    /// through the role test gets a row of its own
8508    /// (`.claude/rules/testing.md`, "give one input to each independent
8509    /// path"): the value default, the script-command list, a modelled
8510    /// construct reached through a `word_list`, the two modelled slots
8511    /// that hold values, an unresolvable command name, and both
8512    /// nesting directions.
8513    ///
8514    /// **Every row asserts the operand columns too, and they are the
8515    /// half that must not move.** #1318 revises one operator and
8516    /// nothing else, so a mutant that suppressed a value word's
8517    /// *contents* — the tidier-looking rule, which collapses an
8518    /// `oo::class create C {…}` body into a single operand — fails
8519    /// here rather than passing as an improvement. #1382 keeps to the
8520    /// same discipline: it withdraws further `{}` operators and moves
8521    /// no operand column in this table.
8522    ///
8523    /// The trailing rows are #1382's. The first four are two *pairs*:
8524    /// a braced value slot beside the bare spelling of the same
8525    /// argument, which must score alike. Before #1382 they did not —
8526    /// the braced spelling billed one extra `{}`. The four after them
8527    /// carry no bare partner: each braces a value with a space in it
8528    /// (`proc {my proc}`, `{a b}`, `{code msg}`), which is the whole
8529    /// reason the braces are there. Their pre-#1382 counts are
8530    /// recorded per row instead.
8531    #[cfg(any(feature = "irules", feature = "tcl"))]
8532    const BRACED_WORD_VALUE_CASES: [BracedWordValueCase; 24] = [
8533        // The rule reaches the opener and *only* the opener. A `;`
8534        // separating two commands is a direct child of the
8535        // `braced_word` — `_terminator` is a hidden rule, so it is
8536        // inlined rather than wrapped — and both dialects classify
8537        // `SEMI` as an operator, so a revision keyed on the parent
8538        // kind alone swallowed it too: this row read n1 0 / N1 0 and
8539        // `halstead.effort` 0.0, against a line that is two commands.
8540        // The pairing with the `eval` row below is the point — same
8541        // brace, same separator, one command name apart.
8542        BracedWordValueCase {
8543            source: "lappend l {puts a ; puts b}\n",
8544            counts: [1, 1, 5, 6],
8545            operands: &["lappend", "l", "puts", "a", "b"],
8546        },
8547        BracedWordValueCase {
8548            source: "eval {puts a ; puts b}\n",
8549            counts: [2, 2, 4, 5],
8550            operands: &["eval", "puts", "a", "b"],
8551        },
8552        // The reported fixture. `lappend` takes a list, so `{a b}`
8553        // quotes rather than opens: no operator. Before #1318 this
8554        // read n1 1 / N1 1 — a `{}` block the line does not contain.
8555        BracedWordValueCase {
8556            source: "lappend l {a b}\n",
8557            counts: [0, 0, 4, 4],
8558            operands: &["lappend", "l", "a", "b"],
8559        },
8560        // The issue's second line, and the one showing the defect
8561        // needs no second argument to appear.
8562        BracedWordValueCase {
8563            source: "puts {c d}\n",
8564            counts: [0, 0, 3, 3],
8565            operands: &["puts", "c", "d"],
8566        },
8567        // The headline: one value, two commands, and until #1318 two
8568        // different operator counts — `set` reported none, `lappend` a
8569        // `{}`. `set` is an operator and `lappend` an operand because
8570        // the grammar models one command and not the other; that
8571        // asymmetry is the grammar's, not this rule's.
8572        BracedWordValueCase {
8573            source: "set x {a b}\nlappend y {a b}\n",
8574            counts: [1, 1, 6, 6],
8575            operands: &["lappend", "x", "y", "{a b}", "a", "b"],
8576        },
8577        // The script side of the same shape, and the reason `LBRACE`
8578        // is gated rather than deleted (grammar-dispatch §6): `eval`
8579        // evaluates its argument, so the block is real and keeps its
8580        // `{}`.
8581        BracedWordValueCase {
8582            source: "eval {puts hi}\n",
8583            counts: [1, 1, 3, 3],
8584            operands: &["eval", "puts", "hi"],
8585        },
8586        // The same command named through the global namespace, which
8587        // is the spelling a `namespace eval` body uses to guarantee the
8588        // core command over a local proc shadowing it. `::eval` *is*
8589        // `eval`, so the two rows must agree on the operator column;
8590        // before the leading `::` was stripped this one fell to the
8591        // value default and lost its block.
8592        BracedWordValueCase {
8593            source: "::eval {puts hi}\n",
8594            counts: [1, 1, 3, 3],
8595            operands: &["::eval", "puts", "hi"],
8596        },
8597        // The control for that strip: only the *leading* qualifier is
8598        // a namespace path to the core command. `ns::eval` is a
8599        // different proc living in `ns`, so it takes the value default
8600        // like any other unrecognised name — a strip of every `::`
8601        // segment would have promoted it to a script.
8602        BracedWordValueCase {
8603            source: "ns::eval {a b}\n",
8604            counts: [0, 0, 3, 3],
8605            operands: &["ns::eval", "a", "b"],
8606        },
8607        // The default for an unrecognised command — a user proc here,
8608        // but equally any package command. Nothing distinguishes it
8609        // from `lappend` structurally, so the row exists to pin which
8610        // way the coin lands. Note the words inside are still
8611        // operands: the rule withdraws the block, never the code.
8612        BracedWordValueCase {
8613            source: "myproc {g h}\n",
8614            counts: [0, 0, 3, 3],
8615            operands: &["myproc", "g", "h"],
8616        },
8617        // `namespace` is the one construct either grammar models that
8618        // still reaches its body through a `word_list`, so it is the
8619        // row that fails if the rule stops at the parent instead of
8620        // reading the grandparent. Its body must stay a script.
8621        BracedWordValueCase {
8622            source: "namespace eval ns {\n    set a 1\n}\n",
8623            counts: [3, 3, 4, 4],
8624            operands: &["eval", "ns", "a", "1"],
8625        },
8626        // A literal nested in a literal: neither brace opens a block,
8627        // and both words survive.
8628        BracedWordValueCase {
8629            source: "puts {a {b c}}\n",
8630            counts: [0, 0, 4, 4],
8631            operands: &["puts", "a", "b", "c"],
8632        },
8633        // A `set` inside a literal. The `set` keyword and the `[]`
8634        // substitution are real operators of a command the walk
8635        // descends into, and they stay — the rule reaches exactly the
8636        // one brace whose parent it can classify, never a subtree.
8637        // `{x [foo] v}` is a `braced_word_simple`, so #1354 bills it
8638        // whole and its `[foo]` interior separately, unchanged.
8639        BracedWordValueCase {
8640            source: "lappend l {set a {x [foo] v}}\n",
8641            counts: [2, 2, 5, 5],
8642            operands: &["lappend", "l", "a", "{x [foo] v}", "foo"],
8643        },
8644        // A braced word in the command *name* position. Legal Tcl —
8645        // `{puts} hi` invokes `puts` — and the one generic position
8646        // that is not an argument, so it is answered before the
8647        // command lookup. It must also never reach the `switch`-arm
8648        // rescue, which is what the braced-pattern row in the Tcl
8649        // table checks.
8650        BracedWordValueCase {
8651            source: "{puts} hi\n",
8652            counts: [0, 0, 2, 2],
8653            operands: &["puts", "hi"],
8654        },
8655        // A defaulted `proc` parameter — the other modelled slot that
8656        // holds a value, decided by the parent kind rather than by a
8657        // command name. `{x y}` is data the interpreter assigns, never
8658        // a script it runs, and it billed a fourth `{}` before #1318.
8659        // The three that remain are the parameter list, the parameter
8660        // spec and the body: those two list delimiters group a
8661        // declaration rather than quote a value, the line #1354 drew
8662        // for `Arguments`.
8663        BracedWordValueCase {
8664            source: "proc p {a {b {x y}}} { puts $a }\n",
8665            counts: [2, 4, 7, 7],
8666            operands: &["p", "a", "b", "x", "y", "puts", "$a"],
8667        },
8668        // A literal nested in a script, the direction that must keep
8669        // working: the `proc` body is a block (`{}` twice with the
8670        // parameter list) and the list it appends is not.
8671        BracedWordValueCase {
8672            source: "proc p {} { lappend l {a b} }\n",
8673            counts: [2, 3, 5, 5],
8674            operands: &["p", "lappend", "l", "a", "b"],
8675        },
8676        // A computed command name resolves to nothing, so the value
8677        // default decides. `$c` is the reference's own operand.
8678        BracedWordValueCase {
8679            source: "set c puts\n$c {a b}\n",
8680            counts: [1, 1, 5, 5],
8681            operands: &["c", "puts", "$c", "a", "b"],
8682        },
8683        // #1382's headline, and the pair that names the defect: `after`
8684        // is a script-taking command, but only from its *second*
8685        // argument — the first is a millisecond count. Both spellings
8686        // of that count are one operand and no operator; before #1382
8687        // the braced one billed a `{}` for a block the line does not
8688        // contain, reading N1 2 against the bare form's 1. The `{}`
8689        // both rows do report is the `{puts hi}` script, which is real.
8690        BracedWordValueCase {
8691            source: "after {100} {puts hi}\n",
8692            counts: [1, 1, 4, 4],
8693            operands: &["after", "100", "puts", "hi"],
8694        },
8695        BracedWordValueCase {
8696            source: "after 100 {puts hi}\n",
8697            counts: [1, 1, 4, 4],
8698            operands: &["after", "100", "puts", "hi"],
8699        },
8700        // The same pair for `time script ?count?`, whose value slot is
8701        // the *last* rather than the first — so a rule that hard-coded
8702        // "argument 0 is the value" passes the `after` pair and fails
8703        // this one. The script argument keeps its `{}` in both rows,
8704        // which is what makes the operator column a 1 and not a 0.
8705        //
8706        // The *bare* row is the half that catches it, which is not the
8707        // half it looks like. Measured by giving `time` `after`'s slot
8708        // rule: `time {puts hi} 3` reads n1 0 / N1 0, its one block
8709        // demoted to a value — while the braced row below is unmoved
8710        // at [1, 1, 4, 4], because `{puts hi}` loses the `{}` that
8711        // `{3}` gains and the operand texts are the same either way.
8712        // Do not trim the bare spelling as redundant with the braced
8713        // one; here it is the discriminating row of the two.
8714        BracedWordValueCase {
8715            source: "time {puts hi} {3}\n",
8716            counts: [1, 1, 4, 4],
8717            operands: &["time", "puts", "hi", "3"],
8718        },
8719        BracedWordValueCase {
8720            source: "time {puts hi} 3\n",
8721            counts: [1, 1, 4, 4],
8722            operands: &["time", "puts", "hi", "3"],
8723        },
8724        // A braced `proc` *name*, the one value slot the grammar names
8725        // as a field rather than by position. `{my proc}` is an
8726        // identifier containing a space, not a body, and billed a
8727        // fourth `{}` before #1382. The three operators left are the
8728        // `proc` keyword and the two braces of the parameter list and
8729        // the body — and note the interior words survive, `proc` being
8730        // an operand here and an operator one column over.
8731        BracedWordValueCase {
8732            source: "proc {my proc} {} {}\n",
8733            counts: [2, 3, 3, 3],
8734            operands: &["my", "proc", "{}"],
8735        },
8736        // The two `namespace` subcommands that take values throughout,
8737        // both of them Tcl 8.6 standard-library spellings. Their braced
8738        // argument is a pattern list and a dictionary; neither is
8739        // evaluated, and each billed a `{}` before #1382 — n1 2 / N1 2
8740        // against the 1 / 1 here. The `namespace` keyword is the
8741        // operator that remains, which is what keeps these two rows
8742        // from reading `effort` 0.0.
8743        BracedWordValueCase {
8744            source: "namespace export {a b}\n",
8745            counts: [1, 1, 3, 3],
8746            operands: &["export", "a", "b"],
8747        },
8748        BracedWordValueCase {
8749            source: "namespace ensemble create -map {a b}\n",
8750            counts: [1, 1, 5, 5],
8751            operands: &["ensemble", "create", "-map", "a", "b"],
8752        },
8753        // A `trap` clause written outside a `try`, which is how both
8754        // grammars reach the generic-command path for it. This is the
8755        // `ScriptSlots::LastOf` row — the pattern and the variable list
8756        // are values and the handler is the script — and the only one
8757        // whose slot is counted from the *end* of the list, so a rule
8758        // that read `Only(2)` from the front would need the arity to
8759        // pass it. Before #1382 all three braces billed: N1 3.
8760        BracedWordValueCase {
8761            source: "trap {POSIX} {code msg} {puts hi}\n",
8762            counts: [1, 1, 6, 6],
8763            operands: &["trap", "POSIX", "code", "msg", "puts", "hi"],
8764        },
8765    ];
8766
8767    /// Regression for #1318. `braced_word` carries both a block and a
8768    /// plain literal, and the grammars spell the two identically, so
8769    /// #1314's kind-scoped guard reached only the value slots they
8770    /// special-case: `set x {a b}` reported no operator while
8771    /// `lappend x {a b}` reported a `{}` for a block the line does not
8772    /// contain. The role is recognised out-of-band, by the enclosing
8773    /// command's leading word (grammar-dispatch §9).
8774    ///
8775    /// The Tcl-only rows are the constructs this grammar models with
8776    /// no node of its own (#467, #1264) — `switch` and `for` — plus the
8777    /// braced `switch` *pattern* that must not be mistaken for an arm
8778    /// body, in both the one-arm-per-line and the one-line layouts.
8779    /// Their iRules counterparts are dedicated nodes and are covered by
8780    /// the sibling test.
8781    #[cfg(feature = "tcl")]
8782    #[test]
8783    fn tcl_braced_word_role_follows_the_enclosing_command_1318() {
8784        check_braced_word_value_cases::<TclParser>(&BRACED_WORD_VALUE_CASES, "foo.tcl");
8785        let tcl_only: [BracedWordValueCase; 8] = [
8786            // `switch` arm bodies. The grammar flattens `pat body pat
8787            // body` into a `command` named after the first pattern, so
8788            // the bodies read as arguments of a command called `a` —
8789            // the shape the value default would call a literal. The
8790            // arm list's own `switch` name is what rescues them, and
8791            // `puts` occurring in both bodies is what separates n2
8792            // from N2.
8793            BracedWordValueCase {
8794                source: "switch $v { a { puts A } b { puts B } }\n",
8795                counts: [1, 3, 7, 8],
8796                operands: &["switch", "$v", "a", "b", "puts", "A", "B"],
8797            },
8798            // …and the limit of that rescue. A braced *pattern* is the
8799            // arm command's `name`, not an argument, so it is a
8800            // literal — `switch -regexp $v { {^a.*b$} {…} }` is
8801            // idiomatic and its regex must not be wrapped in a block.
8802            // Rescuing the whole arm command would read N1 4 here.
8803            // The leading `-exact` / `--` options are `simple_word`
8804            // operands of the *`switch`* command and shift nothing:
8805            // the arm list is found as the sole `braced_word` argument,
8806            // not by index (grammar-dispatch §9).
8807            BracedWordValueCase {
8808                source: "switch -exact -- $v {\n    {a b}   { puts X }\n    default { puts Y }\n}\n",
8809                counts: [1, 3, 10, 11],
8810                operands: &[
8811                    "switch", "-exact", "--", "$v", "a", "b", "puts", "X", "default", "Y",
8812                ],
8813            },
8814            // The same arms on one line. Only the *first* pattern is
8815            // the arm command's name; `{^b}` is its third argument, and
8816            // the name test alone rescued it as a body — N1 4 here,
8817            // against N1 3 for the identical arms written one per line.
8818            // The arm list is `pattern body pattern body …` by Tcl's
8819            // own definition, so the arguments alternate and the
8820            // even-indexed ones are the bodies.
8821            BracedWordValueCase {
8822                source: "switch -regexp $v { {^a} { puts A } {^b} { puts B } }\n",
8823                counts: [1, 3, 8, 9],
8824                operands: &["switch", "-regexp", "$v", "^a", "puts", "A", "^b", "B"],
8825            },
8826            // The words that can interpose between a pattern and its
8827            // body take one slot each and leave the parity intact: a
8828            // `-` fall-through body and a `default` pattern are both
8829            // `simple_word`s. The two braced bodies are at indexes 2
8830            // and 4 of the run and both keep their `{}`.
8831            BracedWordValueCase {
8832                source: "switch $v { a - b { puts AB } default { puts D } }\n",
8833                counts: [1, 3, 9, 10],
8834                operands: &["switch", "$v", "a", "-", "b", "puts", "AB", "default", "D"],
8835            },
8836            // `for` has four braced arguments and all four are
8837            // evaluated, so all four keep their braces. `i` appears as
8838            // the `set` target and again inside `incr`, and `$i` in
8839            // the condition and the body, which is what separates
8840            // n2 = 8 from N2 = 10. `<` is an *operand* here rather
8841            // than a comparison operator: with no `for` rule there is
8842            // no `expr` slot, so the condition parses as a command and
8843            // its comparison never surfaces as a token — the same
8844            // grammar limitation #1264 recorded for ABC.
8845            BracedWordValueCase {
8846                source: "for {set i 0} {$i < 3} {incr i} { puts $i }\n",
8847                counts: [2, 5, 8, 10],
8848                operands: &["for", "i", "0", "$i", "<", "3", "incr", "puts"],
8849            },
8850            // #1382's guard rows: an arm *pattern* spelled like a
8851            // command whose value slots the rule now withdraws. An arm
8852            // list parses as commands, so the pattern becomes a real
8853            // command name and the arm *body* lands in one of that
8854            // command's value slots — where only `Getter::is_switch_arm`
8855            // keeps it a block. Both rows are `switch` bodies that must
8856            // keep their `{}`, and both were measured with the guard
8857            // short-circuited to `false`.
8858            //
8859            // This one is the positional half: `after`'s slot 0 is the
8860            // millisecond count, so without the guard `{ puts B }` reads
8861            // as that value and the row drops to N1 1.
8862            BracedWordValueCase {
8863                source: "switch $v { after { puts B } }\n",
8864                counts: [1, 2, 5, 5],
8865                operands: &["switch", "$v", "after", "puts", "B"],
8866            },
8867            // …and this is the field-keyed half. `proc` needs three
8868            // words, so the arm patterns must be written one per line
8869            // for the grammar to build a `procedure` at all — on one
8870            // line it recovers into an `ERROR` and no rule fires. Here
8871            // it swallows two arms, taking `{ puts A }` as its `name`
8872            // field and `{ puts B }` as its body; without the guard the
8873            // name's brace goes and the row reads N1 5. The `namespace`
8874            // arm is the third spelling of the same hazard and is not
8875            // discriminating: its subcommand slot holds the braced body
8876            // itself rather than a `simple_word`, so no layout resolves
8877            // and the construct-wide script answer stands either way.
8878            // `proc` and `namespace` are operators here and `after` an
8879            // operand, which is the three patterns' whole difference.
8880            BracedWordValueCase {
8881                source: "switch $v {\n    proc { puts A }\n    after { puts B }\n    namespace { puts C }\n}\n",
8882                counts: [3, 6, 7, 9],
8883                operands: &["switch", "$v", "after", "puts", "A", "B", "C"],
8884            },
8885            // The arity guard on `ScriptSlots::LastOf`, Tcl-only
8886            // because the iRules grammar models `trap_handler` and so
8887            // never reaches the generic-command path (#1382 review).
8888            // Tcl models neither `on` nor `trap`, so the whole tail
8889            // after the `try` body parses as *one* `trap` command with
8890            // five arguments — `{p} {v} {puts b} finally {puts c}`.
8891            // Reading "the script is the last argument" off that names
8892            // the `finally` body and calls `{puts b}` a value, which
8893            // dropped the `{}` of a block the line does contain and
8894            // read N1 3. All five braces are blocks here, which is the
8895            // answer a list the signature cannot explain has to get.
8896            // The `trap` row in the shared table is the same clause at
8897            // its documented arity, where two of the three braces
8898            // really are values — so a guard stuck on "always script"
8899            // fails there and one stuck on "always last" fails here.
8900            BracedWordValueCase {
8901                source: "try {puts a} trap {p} {v} {puts b} finally {puts c}\n",
8902                counts: [2, 6, 8, 10],
8903                operands: &["a", "b", "c", "finally", "p", "puts", "trap", "v"],
8904            },
8905        ];
8906        check_braced_word_value_cases::<TclParser>(&tcl_only, "foo.tcl");
8907    }
8908
8909    /// The iRules twin of the test above. The shared table runs
8910    /// unchanged — the two getters are deliberate clones and #1318
8911    /// names both — and the dialect rows cover the handler bodies,
8912    /// which have no Tcl spelling.
8913    #[cfg(feature = "irules")]
8914    #[test]
8915    fn irules_braced_word_role_follows_the_enclosing_command_1318() {
8916        check_braced_word_value_cases::<IrulesParser>(&BRACED_WORD_VALUE_CASES, "foo.irule");
8917        let irules_only: [BracedWordValueCase; 4] = [
8918            // A `when` handler body is a modelled slot, so the literal
8919            // inside it is classified independently of the block that
8920            // holds it.
8921            BracedWordValueCase {
8922                source: "when HTTP_REQUEST {\n    lappend b {x y}\n}\n",
8923                counts: [2, 2, 5, 5],
8924                operands: &["HTTP_REQUEST", "lappend", "b", "x", "y"],
8925            },
8926            // This grammar models `switch` with `switch_arm` children,
8927            // so the arm body fills a modelled slot and never reaches
8928            // the command-name list the Tcl rows need.
8929            BracedWordValueCase {
8930                source: "when HTTP_REQUEST {\n    switch $v { a { pool p1 } }\n}\n",
8931                counts: [3, 5, 5, 5],
8932                operands: &["HTTP_REQUEST", "$v", "a", "pool", "p1"],
8933            },
8934            // `on` and `trap` are why the command list carries two
8935            // words no *Tcl* command spells. The grammar emits
8936            // `on_handler` / `trap_handler` only under `try` — pinned
8937            // by `irules_try_handler_kinds_appear_only_under_try` — so
8938            // a statement-level handler is a generic command, and
8939            // without the list entry its body would lose the `{}` that
8940            // the second occurrence here counts.
8941            BracedWordValueCase {
8942                source: "when RULE_INIT { set x 1 }\non error { log local0. oops }\n",
8943                counts: [3, 4, 8, 8],
8944                operands: &[
8945                    "RULE_INIT",
8946                    "x",
8947                    "1",
8948                    "on",
8949                    "error",
8950                    "log",
8951                    "local0.",
8952                    "oops",
8953                ],
8954            },
8955            // #1382 inside a modelled handler slot: the `when` body is
8956            // a block and keeps its `{}`, and the `after` delay nested
8957            // in it is a value and does not. Before #1382 this read
8958            // N1 4. The shared table's `after` pair already runs for
8959            // this dialect; what this row adds is that the rule reaches
8960            // a command nested inside a handler, where the `{}` billed
8961            // for the handler could otherwise mask a withdrawal.
8962            BracedWordValueCase {
8963                source: "when HTTP_REQUEST {\n    after {100} {puts hi}\n}\n",
8964                counts: [2, 3, 5, 5],
8965                operands: &["HTTP_REQUEST", "after", "100", "puts", "hi"],
8966            },
8967        ];
8968        check_braced_word_value_cases::<IrulesParser>(&irules_only, "foo.irule");
8969    }
8970
8971    #[cfg(feature = "php")]
8972    #[test]
8973    fn php_operators_and_operands() {
8974        check_metrics::<PhpParser>(
8975            "<?php
8976            function avg(int $a, int $b, int $c): int {
8977                return ($a + $b + $c) / 3;
8978            }",
8979            "foo.php",
8980            |metric| {
8981                // After #695 only the opening delimiters count: `()` and
8982                // `{}` fold to one operator each per balanced pair, so the
8983                // former `)`/`}` closers no longer inflate n1/N1 (was
8984                // 11 unique / 15 total).
8985                //
8986                // Operands after #1293, tallied by `get_id` (source bytes):
8987                //   `avg` × 1, `int` × 4 (the `primitive_type` wrapper at
8988                //   all four type positions — its `int` keyword child is
8989                //   suppressed under it), `$a` / `$b` / `$c` × 2 each,
8990                //   `3` × 1 ⇒ n2 = 6, N2 = 12. Between #1259 and #1293 the
8991                //   keyword leaf doubled the type count ⇒ 6 / 16; before
8992                //   #1259 each `$v` also contributed its sigil-less `name`
8993                //   leaf ⇒ 9 / 22.
8994                assert_eq!(metric.halstead.unique_operators(), 9);
8995                assert_eq!(metric.halstead.total_operators(), 12);
8996                assert_eq!(metric.halstead.unique_operands(), 6);
8997                assert_eq!(metric.halstead.total_operands(), 12);
8998                insta::assert_json_snapshot!(metric.halstead);
8999            },
9000        );
9001    }
9002
9003    #[cfg(feature = "php")]
9004    #[test]
9005    fn php_simple_function() {
9006        check_metrics::<PhpParser>(
9007            "<?php
9008            function inc(int $x): int { return $x + 1; }",
9009            "foo.php",
9010            |metric| {
9011                // After #695 only opening delimiters count: the `)`/`}`
9012                // closers no longer add operators (was 9 unique / 9 total).
9013                //
9014                // Operands after #1293: `inc` × 1, `int` × 2 (the
9015                // `primitive_type` wrapper at both type positions, its
9016                // `int` keyword child suppressed under it), `$x` × 2,
9017                // `1` × 1 ⇒ n2 = 4, N2 = 6. Between #1259 and #1293 the
9018                // keyword leaf doubled the type count ⇒ 4 / 8; before
9019                // #1259 `$x` also contributed its `x` leaf twice ⇒ 5 / 10.
9020                assert_eq!(metric.halstead.unique_operators(), 7);
9021                assert_eq!(metric.halstead.total_operators(), 7);
9022                assert_eq!(metric.halstead.unique_operands(), 4);
9023                assert_eq!(metric.halstead.total_operands(), 6);
9024                insta::assert_json_snapshot!(metric.halstead);
9025            },
9026        );
9027    }
9028
9029    #[cfg(feature = "php")]
9030    #[test]
9031    fn php_variable_reference_counts_once() {
9032        // Regression: issue #1259. `$x` parses as a `variable_name`
9033        // wrapping a `name` leaf, and both kinds were in the operand
9034        // arm — so every variable reference contributed twice to N2 and
9035        // planted a sigil-less twin (`x` beside `$x`) in the n2
9036        // vocabulary. Since `$var` is the most common token class in
9037        // PHP, that roughly doubled N2 for real files.
9038        //
9039        // Source: the issue's reproducer plus a re-reference of `$a` and
9040        // `$b`, so N2 exceeds n2 and the assertions can tell "counted
9041        // once per occurrence" from "deduplicated into the vocabulary".
9042        //   <?php $a = null; $b = true; $c = NULL; $a = $b;
9043        //
9044        // Operands by text key: `$a` × 2, `$b` × 2, `$c`, `null`, `true`,
9045        // `NULL` ⇒ n2 = 6, N2 = 8. Before the fix the `a` / `b` / `c`
9046        // leaves added 3 unique and 5 occurrences ⇒ 9 / 13.
9047        check_metrics::<PhpParser>(
9048            "<?php\n$a = null;\n$b = true;\n$c = NULL;\n$a = $b;\n",
9049            "foo.php",
9050            |metric| {
9051                assert_eq!(metric.halstead.unique_operands(), 6);
9052                assert_eq!(metric.halstead.total_operands(), 8);
9053            },
9054        );
9055    }
9056
9057    #[cfg(feature = "php")]
9058    #[test]
9059    fn php_dynamic_variable_name_counts_once_at_any_depth() {
9060        // Regression: issue #1259. Variable-variable syntax nests the
9061        // wrappers, so the double count compounds: `$$a` is a
9062        // `dynamic_variable_name` → `variable_name` → `name` chain that
9063        // scored 3 for one reference, and `$$$b` scored 4. Only the
9064        // outermost wrapper may count.
9065        //
9066        // Source: <?php $$a = 1; $$$b = 2; ${$c} = 3; $$a = 4;
9067        // The trailing re-assignment repeats `$$a` so N2 exceeds n2 and
9068        // the assertions can tell "counted once per occurrence" from
9069        // "deduplicated into the vocabulary".
9070        //
9071        // Operands: `$$a` × 2, `$$$b`, `${$c}`, `1`, `2`, `3`, `4`
9072        // ⇒ n2 = 7, N2 = 8. Before the fix: 14 / 17 (measured), each
9073        // target contributing its whole nesting chain — `$$a` → `$a` →
9074        // `a` is 3 (twice over), `$$$b` → `$$b` → `$b` → `b` is 4, and
9075        // `${$c}` → `$c` → `c` is 3, plus the four integers.
9076        check_metrics::<PhpParser>(
9077            "<?php $$a = 1; $$$b = 2; ${$c} = 3; $$a = 4;",
9078            "foo.php",
9079            |metric| {
9080                assert_eq!(metric.halstead.unique_operands(), 7);
9081                assert_eq!(metric.halstead.total_operands(), 8);
9082            },
9083        );
9084    }
9085
9086    #[cfg(feature = "php")]
9087    #[test]
9088    fn php_dynamic_variable_name_guard_is_parent_scoped() {
9089        // Companion to the two tests above (#1259): the guards fire on
9090        // the *parent* kind, never on the kind alone, so the two
9091        // positions where a nested node is a reference in its own right
9092        // keep counting.
9093        //
9094        // Source: <?php $y = "brace ${z} end"; $s = ${$a . 'b'};
9095        //
9096        // `"${z}"` is a `dynamic_variable_name` whose `name` child is
9097        // suppressed (the wrapper `${z}` carries the reference), while
9098        // `${$a . 'b'}` reaches its `$a` through a `binary_expression`,
9099        // so that `variable_name`'s parent is not a
9100        // `dynamic_variable_name` and it counts normally.
9101        //
9102        // Operands: `$y`, `${z}`, `$s`, `${$a . 'b'}`, `$a`, `'b'` — one
9103        // each ⇒ n2 = 6, N2 = 6. A guard written as a blanket kind
9104        // exclusion instead of a parent check would drop `$a` ⇒ 5 / 5.
9105        check_metrics::<PhpParser>(
9106            "<?php $y = \"brace ${z} end\"; $s = ${$a . 'b'};",
9107            "foo.php",
9108            |metric| {
9109                assert_eq!(metric.halstead.unique_operands(), 6);
9110                assert_eq!(metric.halstead.total_operands(), 6);
9111            },
9112        );
9113    }
9114
9115    #[cfg(feature = "php")]
9116    #[test]
9117    fn php_type_wrappers_count_the_type_once() {
9118        // Regression: issue #1293. A parameter type nests wrapper nodes
9119        // whose text spans the node below them — `primitive_type` around
9120        // the `int` keyword token, `named_type` around a `name`,
9121        // `optional_type` around either — and every level was in the
9122        // operand arm, so `int` scored 2 and `?int` scored 3.
9123        //
9124        // Source: the issue's first reproducer.
9125        //   <?php
9126        //   function f(int $a, bool $b, float $c, string $d, array $e,
9127        //              Foo $g): ?int { return 0; }
9128        //
9129        // Operands by text key: `f`, the five `primitive_type` parameter
9130        // types, `Foo` (the `name` under its `named_type`), `$a`..`$g`,
9131        // the return `int`, and `0`. `int` occurs twice (parameter and
9132        // return) ⇒ n2 = 14, N2 = 15. Before the fix: 15 / 23 — the
9133        // extra vocabulary entry being `?int`, which the `?` operator
9134        // already accounts for.
9135        check_metrics::<PhpParser>(
9136            "<?php\nfunction f(int $a, bool $b, float $c, string $d, \
9137             array $e, Foo $g): ?int { return 0; }\n",
9138            "foo.php",
9139            |metric| {
9140                assert_eq!(metric.halstead.unique_operands(), 14);
9141                assert_eq!(metric.halstead.total_operands(), 15);
9142            },
9143        );
9144    }
9145
9146    #[cfg(feature = "php")]
9147    #[test]
9148    fn php_qualified_name_counts_its_components_once() {
9149        // Regression: issue #1293. `Foo\Bar\Baz` parses as
9150        // `qualified_name` → `namespace_name` → `name` × N, and all
9151        // three kinds were operands, so one three-part path scored 5 and
9152        // planted `Foo`, `Foo\Bar` and `Foo\Bar\Baz` in the vocabulary.
9153        // The components carry the operand and `\` stays an operator,
9154        // matching how PHP's own `::` and `->` already read here.
9155        //
9156        // Source: the issue's second reproducer.
9157        //   <?php
9158        //   namespace App\Sub;
9159        //   use Foo\Bar\Baz;
9160        //   $o = new \Vendor\Pkg\Thing();
9161        //
9162        // Operands: `App`, `Sub`, `Foo`, `Bar`, `Baz`, `$o`, `Vendor`,
9163        // `Pkg`, `Thing` — one each ⇒ n2 = 9, N2 = 9. Before the fix:
9164        // 14 / 14.
9165        check_metrics::<PhpParser>(
9166            "<?php\nnamespace App\\Sub;\nuse Foo\\Bar\\Baz;\n\
9167             $o = new \\Vendor\\Pkg\\Thing();\n",
9168            "foo.php",
9169            |metric| {
9170                assert_eq!(metric.halstead.unique_operands(), 9);
9171                assert_eq!(metric.halstead.total_operands(), 9);
9172            },
9173        );
9174    }
9175
9176    #[cfg(feature = "php")]
9177    #[test]
9178    fn php_nested_type_wrappers_count_once_at_any_depth() {
9179        // Companion to the two tests above (#1293): the type and
9180        // qualified-name wrappers compose, so a single annotation can
9181        // stack five levels — `?A\B` is `optional_type` → `named_type` →
9182        // `qualified_name` → `namespace_name` → `name`, which scored 6
9183        // operands for two identifiers. `union_type` and
9184        // `intersection_type` stack the same way over their members;
9185        // their `|` and `&` are already operators.
9186        //
9187        // Source:
9188        //   <?php function k(?A\B $p, int|string $q, C&D $r): ?A\B
9189        //   { return 0; }
9190        // The return type repeats `?A\B` so N2 exceeds n2 and the
9191        // assertions can tell "counted once per occurrence" from
9192        // "deduplicated into the vocabulary".
9193        //
9194        // Operands: `k`, `A` × 2, `B` × 2, `$p`, `int`, `string`, `$q`,
9195        // `C`, `D`, `$r`, `0` ⇒ n2 = 11, N2 = 13.
9196        check_metrics::<PhpParser>(
9197            "<?php function k(?A\\B $p, int|string $q, C&D $r): ?A\\B { return 0; }",
9198            "foo.php",
9199            |metric| {
9200                assert_eq!(metric.halstead.unique_operands(), 11);
9201                assert_eq!(metric.halstead.total_operands(), 13);
9202            },
9203        );
9204    }
9205
9206    #[cfg(feature = "php")]
9207    #[test]
9208    fn php_childless_primitive_types_still_count() {
9209        // Guards the direction of the #1293 fix for `primitive_type`,
9210        // where — unlike the qualified-name wrappers — the *wrapper*
9211        // carries the operand and the keyword leaf is suppressed. The
9212        // grammar emits no token node under `primitive_type` for
9213        // `callable`, `iterable`, `mixed`, `void`, `false` or `true`
9214        // (verified with `bca dump`), so the other direction would score
9215        // those six types zero — grammar-dispatch §6.
9216        //
9217        // Source:
9218        //   <?php function q(callable $a, iterable $b, mixed $c,
9219        //                    false $d, true $e): void { }
9220        //
9221        // Operands: `q`, `callable`, `iterable`, `mixed`, `false`,
9222        // `true`, `void`, `$a`..`$e` ⇒ n2 = 12, N2 = 12. Dropping
9223        // `PrimitiveType` from the operand arm instead of gating its
9224        // leaf gives 6 / 6.
9225        check_metrics::<PhpParser>(
9226            "<?php function q(callable $a, iterable $b, mixed $c, \
9227             false $d, true $e): void { }",
9228            "foo.php",
9229            |metric| {
9230                assert_eq!(metric.halstead.unique_operands(), 12);
9231                assert_eq!(metric.halstead.total_operands(), 12);
9232            },
9233        );
9234    }
9235
9236    #[cfg(feature = "php")]
9237    #[test]
9238    fn php_primitive_type_keyword_guard_is_parent_scoped() {
9239        // Companion to the test above (#1293): the keyword suppression
9240        // fires on the *parent* kind, never on the kind alone.
9241        // `array` is also the head token of an `array(…)` literal, where
9242        // it is the construct's only operand and must keep counting; a
9243        // `(int)` / `(string)` cast is a childless `cast_type` that
9244        // never reaches the guard at all.
9245        //
9246        // Source: <?php $x = array(1, 2); $y = (int) $x; $z = (string) $x;
9247        //
9248        // Operands: `$x` × 3, `array`, `1`, `2`, `$y`, `int`, `$z`,
9249        // `string` ⇒ n2 = 8, N2 = 10. A blanket kind exclusion instead
9250        // of a parent check would drop the `array` head ⇒ 7 / 9.
9251        check_metrics::<PhpParser>(
9252            "<?php $x = array(1, 2); $y = (int) $x; $z = (string) $x;",
9253            "foo.php",
9254            |metric| {
9255                assert_eq!(metric.halstead.unique_operands(), 8);
9256                assert_eq!(metric.halstead.total_operands(), 10);
9257            },
9258        );
9259    }
9260
9261    #[cfg(feature = "php")]
9262    #[test]
9263    fn php_encapsed_string_interpolation_no_double_count() {
9264        // Regression: issue #184. A PHP `"Hello $name!"` used to be
9265        // classified as a Halstead operand (the wrapping
9266        // `encapsed_string`) AND have its inner `variable_name`
9267        // (`$name`) plus the inner `name` token classified as
9268        // operands too. With the fix, the wrapping literal drops to
9269        // `Unknown` when it carries any `$var` / `${name}` / `{$expr}`
9270        // child, so `$name` is counted exactly once at each text
9271        // occurrence.
9272        //
9273        // Source:
9274        //   <?php $name = "world"; echo "Hello $name!";
9275        //
9276        // Inert operand: `"world"` (no interpolation, still operand).
9277        // Operands by text key (`get_id` keys by source bytes):
9278        //   `$name` × 2 (assignment LHS and `$name` inside the
9279        //   interpolated string), `"world"` × 1.
9280        // u_operands = 2, N2 = 3.
9281        // Without the #184 fix the wrapping `"Hello $name!"` would also
9282        // count → 3 / 4. This test additionally pinned the *inner* `name`
9283        // leaf of each `variable_name` (a further 2 occurrences, 1 unique
9284        // ⇒ the historical 3 / 5) until #1259 recognised that as the same
9285        // double count one level down.
9286        check_metrics::<PhpParser>(
9287            "<?php $name = \"world\"; echo \"Hello $name!\";",
9288            "foo.php",
9289            |metric| {
9290                assert_eq!(metric.halstead.unique_operands(), 2);
9291                assert_eq!(metric.halstead.total_operands(), 3);
9292            },
9293        );
9294    }
9295
9296    #[cfg(feature = "php")]
9297    #[test]
9298    fn php_encapsed_string_no_interpolation_still_operand() {
9299        // The fix for #184 only drops `EncapsedString`/`Heredoc` from
9300        // the operand arm when interpolation is present. An inert
9301        // double-quoted string must still count as exactly one
9302        // operand, identical to the single-quoted equivalent.
9303        //
9304        // Source: `<?php echo "Hello world!";`
9305        // Operands: `"Hello world!"` × 1 → u_operands = 1, N2 = 1.
9306        check_metrics::<PhpParser>("<?php echo \"Hello world!\";", "foo.php", |metric| {
9307            assert_eq!(metric.halstead.unique_operands(), 1);
9308            assert_eq!(metric.halstead.total_operands(), 1);
9309        });
9310    }
9311
9312    #[cfg(feature = "php")]
9313    #[test]
9314    fn php_heredoc_interpolation_no_double_count() {
9315        // Regression: issue #184. A PHP heredoc whose body
9316        // interpolates `$name` previously counted both the wrapping
9317        // `heredoc` node and the inner `$name` as operands; the fix
9318        // drops the wrapper when its `heredoc_body` carries any
9319        // interpolation child.
9320        //
9321        // Source:
9322        //   <?php $name = "x"; echo <<<EOT
9323        //   hi $name
9324        //   EOT;
9325        //
9326        // Operands by text key: `$name` × 2, `"x"` × 1 (inert encapsed
9327        // string, still an operand). With the fix u_operands = 2,
9328        // N2 = 3. Without it the wrapping heredoc text would add one
9329        // more unique operand. The sigil-less `name` leaf inside each
9330        // `variable_name` was counted too until #1259.
9331        check_metrics::<PhpParser>(
9332            "<?php $name = \"x\"; echo <<<EOT\nhi $name\nEOT;\n",
9333            "foo.php",
9334            |metric| {
9335                assert_eq!(metric.halstead.unique_operands(), 2);
9336                assert_eq!(metric.halstead.total_operands(), 3);
9337            },
9338        );
9339    }
9340
9341    #[cfg(feature = "php")]
9342    #[test]
9343    fn php_nowdoc_unaffected() {
9344        // `Nowdoc` (single-quoted heredoc) never interpolates and is
9345        // never matched by `php_string_has_interpolation`. It must
9346        // continue counting as exactly one operand regardless of the
9347        // text inside, mirroring single-quoted `String`.
9348        //
9349        // Source:
9350        //   <?php echo <<<'EOT'
9351        //   plain $name not interpolated
9352        //   EOT;
9353        //
9354        // Operands: the nowdoc literal × 1 → u_operands = 1, N2 = 1.
9355        check_metrics::<PhpParser>(
9356            "<?php echo <<<'EOT'\nplain $name not interpolated\nEOT;\n",
9357            "foo.php",
9358            |metric| {
9359                assert_eq!(metric.halstead.unique_operands(), 1);
9360                assert_eq!(metric.halstead.total_operands(), 1);
9361            },
9362        );
9363    }
9364
9365    #[cfg(feature = "php")]
9366    #[test]
9367    fn php_encapsed_string_bare_member_access_no_double_count() {
9368        // Regression: issue #184 follow-up. The PHP grammar allows
9369        // bare `$obj->prop` interpolation inside `"…"` without
9370        // surrounding `{ … }`; tree-sitter-php emits this as a
9371        // direct `member_access_expression` child of
9372        // `encapsed_string` (kind_id 329 in the current grammar).
9373        // The wrapper must drop to `Unknown` for that form too —
9374        // otherwise the inner `$obj` and `prop` `name` tokens are
9375        // walked as operands while the wrapper also counts,
9376        // double-counting `N2`.
9377        //
9378        // Source:
9379        //   <?php $obj = new stdClass; $obj->prop = "x"; echo "Hi $obj->prop!";
9380        //
9381        // Operands tallied by `get_id` (keyed on source bytes):
9382        //   `$obj`        × 3 (LHS assignment, member-access target,
9383        //                      inside the interpolated string)
9384        //   `prop` (name) × 2 (member-access RHS twice — a bare `name`
9385        //                      outside any `variable_name`, so #1259's
9386        //                      guard leaves it an operand)
9387        //   `stdClass`    × 1
9388        //   `"x"`         × 1
9389        // ⇒ u_operands = 4, N2 = 7.
9390        // With the bug the wrapping `"Hi $obj->prop!"` text adds one
9391        // more unique operand and one more occurrence ⇒ 5 / 8.
9392        check_metrics::<PhpParser>(
9393            "<?php $obj = new stdClass; $obj->prop = \"x\"; echo \"Hi $obj->prop!\";",
9394            "foo.php",
9395            |metric| {
9396                assert_eq!(metric.halstead.unique_operands(), 4);
9397                assert_eq!(metric.halstead.total_operands(), 7);
9398            },
9399        );
9400    }
9401
9402    #[cfg(feature = "php")]
9403    #[test]
9404    fn php_encapsed_string_bare_subscript_no_double_count() {
9405        // Regression: issue #184 follow-up. Bare `$arr[0]` inside
9406        // `"…"` produces a `subscript_expression` child of
9407        // `encapsed_string` (kind_id 351). The wrapper must drop to
9408        // `Unknown` for that form.
9409        //
9410        // Source:
9411        //   <?php $arr = [1]; echo "Hi $arr[0]!";
9412        //
9413        // Operands tallied by `get_id`:
9414        //   `$arr` × 2, `1` × 1, `0` × 1.
9415        // ⇒ u_operands = 3, N2 = 4.
9416        // With the bug the wrapping `"Hi $arr[0]!"` text adds 1 / 1.
9417        // The inner `arr` leaf of each `variable_name` added a further
9418        // 1 / 2 until #1259.
9419        check_metrics::<PhpParser>(
9420            "<?php $arr = [1]; echo \"Hi $arr[0]!\";",
9421            "foo.php",
9422            |metric| {
9423                assert_eq!(metric.halstead.unique_operands(), 3);
9424                assert_eq!(metric.halstead.total_operands(), 4);
9425            },
9426        );
9427    }
9428
9429    #[cfg(feature = "php")]
9430    #[test]
9431    fn php_shell_command_expression_inert_is_operand() {
9432        // Regression: issue #288. Backtick command literals (PHP's
9433        // `shell_command_expression`) were filtered as strings by
9434        // `Checker::is_string` and `Alterator::alterate`, but never
9435        // classified as Halstead operands — so they contributed
9436        // nothing to N2 / eta2. An inert backtick literal must now
9437        // count as exactly one operand, matching `EncapsedString`
9438        // and `Heredoc`.
9439        //
9440        // Source: `<?php $out = ` + backtick `ls` + backtick + `;`
9441        // Operands tallied by `get_id`:
9442        //   `$out` × 1, backtick literal × 1.
9443        // ⇒ u_operands = 2, N2 = 2.
9444        // Before the fix the backtick literal vanished from the count
9445        // ⇒ u_operands = 1, N2 = 1. (The inner `out` leaf of the
9446        // `variable_name` added another 1 / 1 until #1259.)
9447        check_metrics::<PhpParser>("<?php $out = `ls`;", "foo.php", |metric| {
9448            assert_eq!(metric.halstead.unique_operands(), 2);
9449            assert_eq!(metric.halstead.total_operands(), 2);
9450        });
9451    }
9452
9453    #[cfg(feature = "php")]
9454    #[test]
9455    fn php_shell_command_expression_interpolation_no_double_count() {
9456        // Regression: issue #288. PHP backtick literals DO support
9457        // `$var` interpolation (see tree-sitter-php node-types.json:
9458        // `shell_command_expression` children include `variable_name`,
9459        // `dynamic_variable_name`, `member_access_expression`,
9460        // `subscript_expression`). With the fix the wrapper drops to
9461        // `Unknown` when it carries any interpolation child, exactly
9462        // as `EncapsedString` does.
9463        //
9464        // Source: `<?php $dir = "/tmp"; $out = ` + backtick `ls $dir` +
9465        //   backtick + `;`
9466        //
9467        // Operands tallied by `get_id`:
9468        //   `$dir` × 2 (assignment LHS, inside backticks),
9469        //   `$out` × 1, `"/tmp"` × 1.
9470        // ⇒ u_operands = 3, N2 = 4.
9471        // Without the interpolation guard the wrapping backtick literal
9472        // would also count ⇒ u_operands = 4, N2 = 5. The sigil-less
9473        // `dir` / `out` leaves added a further 2 / 3 until #1259.
9474        check_metrics::<PhpParser>(
9475            "<?php $dir = \"/tmp\"; $out = `ls $dir`;",
9476            "foo.php",
9477            |metric| {
9478                assert_eq!(metric.halstead.unique_operands(), 3);
9479                assert_eq!(metric.halstead.total_operands(), 4);
9480            },
9481        );
9482    }
9483
9484    #[cfg(feature = "php")]
9485    #[test]
9486    fn php_interpolation_opener_is_not_an_operator() {
9487        // Regression: issue #1314. `Php::LBRACE` is *both* the
9488        // compound-statement brace and the complex-interpolation
9489        // opener, so `"dq {$y} end"` reported a `{}` operator — and
9490        // reported it against the same vocabulary entry a real block
9491        // uses, which no other language does.
9492        //
9493        // expected: operators `=` × 2, `;` × 2 → n1 = 2, N1 = 4.
9494        // Operands `$s`, `$t`, `$y` × 2 → n2 = 3, N2 = 4. Before the
9495        // guard the two openers added `{}` → n1 = 3, N1 = 6.
9496        check_metrics::<PhpParser>(
9497            "<?php\n$s = \"dq {$y} end\";\n$t = \"dq {$y} end\";\n",
9498            "foo.php",
9499            |metric| {
9500                assert_eq!(metric.halstead.unique_operators(), 2);
9501                assert_eq!(metric.halstead.total_operators(), 4);
9502                assert_eq!(metric.halstead.unique_operands(), 3);
9503                assert_eq!(metric.halstead.total_operands(), 4);
9504            },
9505        );
9506    }
9507
9508    #[cfg(feature = "php")]
9509    #[test]
9510    fn php_interpolation_opener_guard_covers_every_wrapper() {
9511        // The opener is a direct child of four distinct parents, and
9512        // each is an independent leg of the guard (grammar-dispatch
9513        // section 11) — a fixture covering only `encapsed_string`
9514        // leaves the other three dead.
9515        //
9516        // One row per parent, so a failure names the leg that broke.
9517        // The heredoc's brace hangs off `heredoc_body` rather than
9518        // `heredoc`; the backtick form is `shell_command_expression`;
9519        // and the bare `${$y}` variable-variable is a
9520        // `dynamic_variable_name`, the one position that is not inside
9521        // a string at all.
9522        //
9523        // expected per row: operators `=` × 2, `;` × 2 → n1 = 2,
9524        // N1 = 4; three distinct operands with one repeated → n2 = 3,
9525        // N2 = 4.
9526        for (label, source) in [
9527            (
9528                "encapsed_string",
9529                "<?php\n$s = \"dq {$y} end\";\n$t = \"dq {$y} end\";\n",
9530            ),
9531            (
9532                "heredoc_body",
9533                "<?php\n$h = <<<EOT\na {$y} b\nEOT;\n$i = <<<EOT\na {$y} b\nEOT;\n",
9534            ),
9535            (
9536                "shell_command_expression",
9537                "<?php\n$b = `ls {$y}`;\n$c = `ls {$y}`;\n",
9538            ),
9539            ("dynamic_variable_name", "<?php\n$q = ${$y};\n$r = ${$y};\n"),
9540        ] {
9541            assert_halstead_counts::<PhpParser>(source, "foo.php", [2, 4, 3, 4], label);
9542        }
9543    }
9544
9545    #[cfg(feature = "php")]
9546    #[test]
9547    fn php_every_interpolation_spelling_scores_alike() {
9548        // The policy stated as a test (#1314). PHP writes one
9549        // interpolation three ways; the choice is spelling, so all
9550        // three must score identically. Before the guard the two
9551        // braced forms reported a `{}` the bare `$y` form did not.
9552        //
9553        // `"${y}"` is deprecated as of PHP 8.2 and removed in 9.0, but
9554        // the pinned grammar still parses it and it is still in the
9555        // wild, so it stays a row here.
9556        //
9557        // The fixture deliberately omits a `$y = …` declaration: with
9558        // one, the bare and `{$y}` forms key their operand as `$y` and
9559        // collapse into the declaration's entry while `${y}` keys as
9560        // `${y}` and does not, so n2 would differ for a reason that has
9561        // nothing to do with this guard.
9562        //
9563        // expected per spelling: operators `=` × 2, `;` × 2 → n1 = 2,
9564        // N1 = 4; operands `$s`, `$t`, the interpolated reference × 2
9565        // → n2 = 3, N2 = 4.
9566        for literal in ["\"a $y b\"", "\"a {$y} b\"", "\"a ${y} b\""] {
9567            assert_halstead_counts::<PhpParser>(
9568                &format!("<?php\n$s = {literal};\n$t = {literal};\n"),
9569                "foo.php",
9570                [2, 4, 3, 4],
9571                &format!("interpolation {literal}"),
9572            );
9573        }
9574    }
9575
9576    #[cfg(feature = "php")]
9577    #[test]
9578    fn php_compound_statement_brace_still_counts() {
9579        // Control for #1314: the guard is scoped to the four
9580        // interpolating wrappers, so a real block keeps its `{}`. A
9581        // guard widened to every `LBRACE` would take `{}` out of the
9582        // operator set entirely and fail here.
9583        //
9584        // expected: operators `function`, `()`, `{}` × 2, `if`,
9585        // `return`, `;` → n1 = 6, N1 = 8. Operands `f`, `1`, `2` →
9586        // n2 = N2 = 3.
9587        check_metrics::<PhpParser>(
9588            "<?php\nfunction f() { if (1) { return 2; } }\n",
9589            "foo.php",
9590            |metric| {
9591                assert_eq!(metric.halstead.unique_operators(), 6);
9592                assert_eq!(metric.halstead.total_operators(), 8);
9593                assert_eq!(metric.halstead.unique_operands(), 3);
9594                assert_eq!(metric.halstead.total_operands(), 3);
9595            },
9596        );
9597    }
9598
9599    #[cfg(feature = "php")]
9600    #[test]
9601    fn php_interpolation_guard_is_parent_scoped_not_ancestor_scoped() {
9602        // The input that separates the parent-scoped guard from the
9603        // ancestor-scanning mutant — the mutant #1256's post-mortem
9604        // says survives every ordinary fixture. PHP is one of only two
9605        // languages in #1314 where such an input exists: a closure
9606        // inside a complex interpolation puts a *compound-statement*
9607        // brace under an `encapsed_string` ancestor while its parent is
9608        // the `compound_statement`. An ancestor scan swallows it.
9609        //
9610        // expected: operators `=`, `;` × 2, `->`, `()` × 2, `function`,
9611        // `{}`, `return` → n1 = 7, N1 = 9. Operands `$s`, `$o`, `m`,
9612        // `1` → n2 = N2 = 4. Under the ancestor-scoped mutant the
9613        // closure's brace vanishes: n1 = 6, N1 = 8.
9614        check_metrics::<PhpParser>(
9615            "<?php\n$s = \"{$o->m(function() { return 1; })}\";\n",
9616            "foo.php",
9617            |metric| {
9618                assert_eq!(metric.halstead.unique_operators(), 7);
9619                assert_eq!(metric.halstead.total_operators(), 9);
9620                assert_eq!(metric.halstead.unique_operands(), 4);
9621                assert_eq!(metric.halstead.total_operands(), 4);
9622            },
9623        );
9624    }
9625
9626    #[cfg(feature = "elixir")]
9627    #[test]
9628    fn elixir_operators_and_operands() {
9629        // Exercises every Halstead family classified in Elixir's
9630        // `get_op_type`: control-flow keywords (`do`, `end`, `fn`),
9631        // structural punctuation — only the *opening* delimiters `(`,
9632        // `[` count after #695 (the `)`/`]` closers were dropped), plus
9633        // `,`, `.`, `@`,
9634        // arithmetic (`+`, `-`, `*`, `/`), comparison (`==`, `>`),
9635        // logical (`&&`, `||`, `and`, `or`, `!`), pipe (`|>`), capture
9636        // (`&`), assignment/match (`=`), and the stab arrow (`->`).
9637        // The body mixes identifiers, integers, atoms, and a string.
9638        check_metrics::<ElixirParser>(
9639            "defmodule Foo do\n  @doc \"add\"\n  def calc(a, b) do\n    result = a + b * 2\n    flag = result > 0 && a == b\n    out = if flag, do: result, else: -result\n    [out, a, b]\n  end\nend\n",
9640            "foo.ex",
9641            |metric| {
9642                // Positive headline assertions on integer counts. After
9643                // #695 only opening delimiters count: the `)`/`]` closers
9644                // no longer add operators (was 15 unique / 23 total).
9645                assert_eq!(metric.halstead.unique_operators(), 13);
9646                assert_eq!(metric.halstead.total_operators(), 21);
9647                assert_eq!(metric.halstead.unique_operands(), 16);
9648                assert_eq!(metric.halstead.total_operands(), 27);
9649                insta::assert_json_snapshot!(
9650                    metric.halstead,
9651                    @r#"
9652                {
9653                  "unique_operators": 13,
9654                  "total_operators": 21,
9655                  "unique_operands": 16,
9656                  "total_operands": 27,
9657                  "length": 48,
9658                  "estimated_program_length": 112.10571633583419,
9659                  "purity_ratio": 2.3355357569965456,
9660                  "vocabulary": 29,
9661                  "volume": 233.18308776612344,
9662                  "difficulty": 10.96875,
9663                  "level": 0.09116809116809117,
9664                  "effort": 2557.7269939346666,
9665                  "time": 142.09594410748147,
9666                  "bugs": 0.062342115670886794
9667                }
9668                "#
9669                );
9670            },
9671        );
9672    }
9673
9674    #[cfg(feature = "ruby")]
9675    #[test]
9676    fn ruby_operators_and_operands() {
9677        // A small Ruby method exercising operators (def/if/end keyword
9678        // tokens, `+`, `==`, `<=`, structural punctuation) and operands
9679        // (`n`, `1`, `factorial`). Anchors the unique/total counts on
9680        // both sides and snapshots the full Halstead derivation.
9681        //
9682        // Lesson 4 invariants: u_operators / u_operands here equal the
9683        // dedupe lengths the `--ops` accessor would emit on the same
9684        // source. Any future grammar bump that adds an aliased kind_id
9685        // to either side will trip this without snapshot drift.
9686        check_metrics::<RubyParser>(
9687            "def factorial(n)\n  return 1 if n <= 1\n  n * factorial(n - 1)\nend\n",
9688            "foo.rb",
9689            |metric| {
9690                // After #695 only the `(` opener counts (folded `()`); the
9691                // `)` closer — which appeared twice across the two calls —
9692                // no longer adds an operator (was 9 unique / 11 total).
9693                assert_eq!(metric.halstead.unique_operators(), 8);
9694                assert_eq!(metric.halstead.total_operators(), 9);
9695                assert_eq!(metric.halstead.unique_operands(), 3);
9696                assert_eq!(metric.halstead.total_operands(), 9);
9697                insta::assert_json_snapshot!(metric.halstead);
9698            },
9699        );
9700    }
9701
9702    #[cfg(feature = "ruby")]
9703    #[test]
9704    fn ruby_halstead_plain_string_operand() {
9705        // A bare string literal contributes exactly one operand. The
9706        // counterpart to `ruby_halstead_interpolated_string_no_double_count`
9707        // — verifies the "no interpolation" branch of the same arm
9708        // (see `get_op_type`'s `R::String | …` case in
9709        // `big-code-analysis-ast/src/getter.rs`).
9710        // expected: operators = {def, end} = 2; operands = {f, "hello"} = 2.
9711        check_metrics::<RubyParser>("def f\n  \"hello\"\nend\n", "foo.rb", |metric| {
9712            assert_eq!(metric.halstead.unique_operators(), 2);
9713            assert_eq!(metric.halstead.total_operators(), 2);
9714            assert_eq!(metric.halstead.unique_operands(), 2);
9715            assert_eq!(metric.halstead.total_operands(), 2);
9716        });
9717    }
9718
9719    #[cfg(feature = "ruby")]
9720    #[test]
9721    fn ruby_halstead_interpolated_string_no_double_count() {
9722        // Regression mirror for #180 (Bash) / #183 (C#): when a Ruby
9723        // string literal carries an `Interpolation` child, the
9724        // wrapping `String` node is intentionally classified as
9725        // `Unknown` so the inner expression's identifiers are not
9726        // double-counted as operands.
9727        //
9728        // expected: for `def f(name)\n  "Hi #{name}"\nend\n` —
9729        //   operators: def, (, ), #{, }, end → u_operators = 6.
9730        //   operands: f, name (param), name (inside `#{name}`). The
9731        //   wrapping `"…#{name}"` literal is skipped by the
9732        //   `is_child(R::Interpolation)` guard; the operand store
9733        //   keys by token text so the two `name` occurrences dedupe
9734        //   into one distinct entry → u_operands = 2, operands = 3
9735        //   (`f` once, `name` twice).
9736        // Without the guard, the wrapping literal would also count,
9737        // inflating u_operands to 3 and operands to 4.
9738        check_metrics::<RubyParser>("def f(name)\n  \"Hi #{name}\"\nend\n", "foo.rb", |metric| {
9739            assert_eq!(metric.halstead.unique_operands(), 2);
9740            assert_eq!(metric.halstead.total_operands(), 3);
9741        });
9742    }
9743
9744    #[cfg(feature = "ruby")]
9745    #[test]
9746    fn ruby_halstead_symbol_literal_operand() {
9747        // `:foo` is a `SimpleSymbol` leaf — counts as a single
9748        // operand, no interpolation guard needed (only
9749        // `DelimitedSymbol` (`:"…#{x}…"`) can interpolate).
9750        // expected: operators = {def, end} = 2; operands = {f, :ok} = 2.
9751        check_metrics::<RubyParser>("def f\n  :ok\nend\n", "foo.rb", |metric| {
9752            assert_eq!(metric.halstead.unique_operators(), 2);
9753            assert_eq!(metric.halstead.unique_operands(), 2);
9754        });
9755    }
9756
9757    #[cfg(feature = "ruby")]
9758    #[test]
9759    fn ruby_halstead_regex_operand() {
9760        // `/foo/` parses as a `Regex` node — one operand. Its two
9761        // `SLASH` delimiters used to fall through to the shared
9762        // arithmetic arm and add a `/` operator that is nowhere in the
9763        // source; #1312 parent-guards them to `Unknown`.
9764        // expected: u_operators = {def, (, =~, end} = 4, N1 = 4 (only
9765        // the `(` opener counts after #695 — the `)` closer was
9766        // dropped; was 5 with the fabricated `/`); u_operands =
9767        // {f, s, /foo/} = 3, N2 = 4 (`s` twice: parameter and use).
9768        check_metrics::<RubyParser>("def f(s)\n  s =~ /foo/\nend\n", "foo.rb", |metric| {
9769            assert_eq!(metric.halstead.unique_operators(), 4);
9770            assert_eq!(metric.halstead.total_operators(), 4);
9771            assert_eq!(metric.halstead.unique_operands(), 3);
9772            assert_eq!(metric.halstead.total_operands(), 4);
9773        });
9774    }
9775
9776    #[cfg(feature = "ruby")]
9777    #[test]
9778    fn ruby_regex_delimiters_are_not_operators() {
9779        // Regression: issue #1312, the Ruby sibling of Elixir #1256.
9780        // Both of a `Regex` literal's delimiter tokens are `SLASH` —
9781        // the same kind id real division uses — so `x = /abc/`
9782        // reported a `/` operator with no division in the source.
9783        //
9784        // expected: operators `=` → n1 = N1 = 1. Operands `x` and the
9785        // `/abc/` literal → n2 = N2 = 2. Before the guard the two
9786        // delimiters added `/` → n1 = 2, N1 = 3.
9787        check_metrics::<RubyParser>("x = /abc/\n", "foo.rb", |metric| {
9788            assert_eq!(metric.halstead.unique_operators(), 1);
9789            assert_eq!(metric.halstead.total_operators(), 1);
9790            assert_eq!(metric.halstead.unique_operands(), 2);
9791            assert_eq!(metric.halstead.total_operands(), 2);
9792        });
9793    }
9794
9795    #[cfg(feature = "ruby")]
9796    #[test]
9797    fn ruby_regex_delimiter_choice_is_invariant() {
9798        // Companion to the test above (#1312): `%r`-form regexes are
9799        // the same literal spelled differently, so every delimiter
9800        // choice must produce identical counts. tree-sitter-ruby
9801        // aliases all of them to `SLASH` — verified with `bca dump`,
9802        // which shows `%r{`/`}`, `%r(`/`)`, `%r[`/`]`, `%r<`/`>`,
9803        // `%r|`/`|` and `%r!`/`!` every one emitting kind `SLASH` —
9804        // so each row here genuinely exercises the guard rather than
9805        // reaching a different, already-clean path.
9806        //
9807        // expected per variant: operator `=` → n1 = N1 = 1; operands
9808        // `x` and the literal → n2 = N2 = 2.
9809        for literal in [
9810            "/abc/", "%r{abc}", "%r(abc)", "%r[abc]", "%r<abc>", "%r|abc|", "%r!abc!",
9811        ] {
9812            assert_halstead_counts::<RubyParser>(
9813                &format!("x = {literal}\n"),
9814                "foo.rb",
9815                [1, 1, 2, 2],
9816                &format!("regex literal {literal}"),
9817            );
9818        }
9819    }
9820
9821    #[cfg(feature = "ruby")]
9822    #[test]
9823    fn ruby_division_survives_the_regex_guard() {
9824        // Control for #1312: the guard is scoped to a `Regex` parent,
9825        // so real division still counts. Two divisions, so a mutant
9826        // that collapsed repeated hits would move `N1` even though
9827        // `n1` held (the #1294 count-only-anchor lesson).
9828        //
9829        // expected: operators `=` and `/` × 2 → n1 = 2, N1 = 3.
9830        // Operands `z`, `a`, `b`, `c` → n2 = N2 = 4.
9831        check_metrics::<RubyParser>("z = a / b / c\n", "foo.rb", |metric| {
9832            assert_eq!(metric.halstead.unique_operators(), 2);
9833            assert_eq!(metric.halstead.total_operators(), 3);
9834            assert_eq!(metric.halstead.unique_operands(), 4);
9835            assert_eq!(metric.halstead.total_operands(), 4);
9836        });
9837    }
9838
9839    #[cfg(feature = "ruby")]
9840    #[test]
9841    fn ruby_regex_guard_is_parent_scoped_not_ancestor_scoped() {
9842        // The one input that separates the correct parent-scoped guard
9843        // from the ancestor-scoped mutant of it (#1312, mirroring
9844        // #1256's Elixir case): a division *inside* a regex's `#{…}`
9845        // interpolation. Its `/` has `Binary` as its parent but the
9846        // `Regex` as a further ancestor, so an ancestor scan would
9847        // swallow it. Every other fixture in this file passes under
9848        // both spellings. Two interpolations, so the mutant moves both
9849        // n1 (2 → 1) and N1 (3 → 1).
9850        //
9851        // expected: operators `=`, `/` × 2 → n1 = 2, N1 = 3. Operands
9852        // `w`, `p`, `q`, `r`, `t` → n2 = N2 = 5; the wrapping `Regex`
9853        // is skipped because it carries an `Interpolation` child (the
9854        // #180 double-count guard).
9855        //
9856        // Was n1 = 3, N1 = 5 until #1314 dropped `#{` from the operator
9857        // arm. The mutant still moves both axes, so this fixture is as
9858        // discriminating as it was — it just no longer counts the two
9859        // interpolation openers alongside the two divisions.
9860        check_metrics::<RubyParser>("w = /a#{p / q}c#{r / t}b/\n", "foo.rb", |metric| {
9861            assert_eq!(metric.halstead.unique_operators(), 2);
9862            assert_eq!(metric.halstead.total_operators(), 3);
9863            assert_eq!(metric.halstead.unique_operands(), 5);
9864            assert_eq!(metric.halstead.total_operands(), 5);
9865        });
9866    }
9867
9868    #[cfg(feature = "ruby")]
9869    #[test]
9870    fn ruby_regex_start_alias_never_reaches_kind_id() {
9871        // Drift marker for the `R::SLASH2` half of #1312's guard.
9872        // `SLASH2` is the aliased regex-start token: it sits in the
9873        // enum beside the other literal-start aliases (`DQUOTE`,
9874        // `COLONDQUOTE`, `BQUOTE2`, `PERCENTwLPAREN`) and the runtime
9875        // `public_symbol_map` collapses it to `SLASH` before
9876        // `kind_id()`, exactly like `LPAREN2` in #768. It is listed in
9877        // the guard rather than the arithmetic arm because a regex
9878        // delimiter is the only thing it could ever be; this pins that
9879        // it is currently unreachable, so a grammar bump that starts
9880        // emitting it fails here instead of silently changing a metric.
9881        let path = PathBuf::from("foo.rb");
9882        for source in ["x = /abc/\n", "x = %r{abc}\n"] {
9883            let parser = RubyParser::new(source.as_bytes().to_vec(), &path, None);
9884            assert!(
9885                !ast_has_kind_id(&parser, Ruby::SLASH2 as u16),
9886                "Ruby::SLASH2 must stay collapsed to Ruby::SLASH for `{source}`"
9887            );
9888            // Positive control: the id the guard actually fires on is
9889            // present, so the assertion above cannot pass merely
9890            // because no delimiter was parsed at all.
9891            assert!(
9892                ast_has_kind_id(&parser, Ruby::SLASH as u16),
9893                "Ruby::SLASH must be the delimiter kind for `{source}`"
9894            );
9895        }
9896    }
9897
9898    #[cfg(feature = "ruby")]
9899    #[test]
9900    fn ruby_subshell_delimiters_are_not_operators() {
9901        // Regression: issue #1360, the second delimiter family of the
9902        // fabrication #1312 removed for regexes. tree-sitter-ruby
9903        // aliases both ends of a subshell literal to `BQUOTE` — the
9904        // same kind id the `` def ` `` method-name marker uses — so
9905        // `` x = `echo hi` `` reported a `` ` `` operator with no
9906        // backtick method call in the source.
9907        //
9908        // expected: operator `=` → n1 = N1 = 1. Operands `x` and the
9909        // `` `echo hi` `` literal → n2 = N2 = 2. Before the guard the
9910        // two delimiters added `` ` `` → n1 = 2, N1 = 3.
9911        assert_halstead_counts::<RubyParser>(
9912            "x = `echo hi`\n",
9913            "foo.rb",
9914            [1, 1, 2, 2],
9915            "backtick subshell",
9916        );
9917    }
9918
9919    #[cfg(feature = "ruby")]
9920    #[test]
9921    fn ruby_subshell_delimiter_choice_is_invariant() {
9922        // Companion to the test above (#1360): `%x`-form subshells are
9923        // the same literal spelled differently, so every delimiter
9924        // choice must produce identical counts. tree-sitter-ruby
9925        // aliases all of them to `BQUOTE` — verified with `bca dump`,
9926        // which shows `` ` ``/`` ` ``, `%x{`/`}`, `%x(`/`)`, `%x[`/`]`,
9927        // `%x<`/`>`, `%x|`/`|` and `%x!`/`!` every one emitting kind
9928        // `BQUOTE` — so each row here genuinely exercises the guard
9929        // rather than reaching a different, already-clean path. Note
9930        // the `%x!…!` and `%x|…|` closers in particular: they are
9931        // `BQUOTE`, not the `BANG` / `PIPE` operators they look like.
9932        //
9933        // The empty row records that `` `` `` parses to a `Subshell`
9934        // holding nothing but its two delimiters, and still bills that
9935        // wrapper as one operand once both are suppressed. It is
9936        // documentation, not a discriminator: the guard deletes nothing
9937        // from the operand arm, so grammar-dispatch §6's
9938        // childless-variant-regresses-to-zero hazard cannot arise here
9939        // and this row moves with the first row under every mutant.
9940        //
9941        // expected per variant: operator `=` → n1 = N1 = 1; operands
9942        // `x` and the literal → n2 = N2 = 2.
9943        for literal in [
9944            "`echo`", "%x{echo}", "%x(echo)", "%x[echo]", "%x<echo>", "%x|echo|", "%x!echo!", "``",
9945        ] {
9946            assert_halstead_counts::<RubyParser>(
9947                &format!("x = {literal}\n"),
9948                "foo.rb",
9949                [1, 1, 2, 2],
9950                &format!("subshell literal {literal}"),
9951            );
9952        }
9953    }
9954
9955    #[cfg(feature = "ruby")]
9956    #[test]
9957    fn ruby_backtick_method_name_survives_the_subshell_guard() {
9958        // Control for #1360, and the reason the kind is gated rather
9959        // than dropped from the operator arm (grammar-dispatch §6):
9960        // `` ` `` is a legal Ruby method name. `bca dump` at the pinned
9961        // grammar shows `` def `(cmd) `` emitting a `BQUOTE` wrapped in
9962        // a named `operator` node, so its parent is `Operator` and the
9963        // parent-scoped guard leaves it alone. Deleting `BQUOTE` from
9964        // the operator arm would have scored this construct zero.
9965        //
9966        // expected: operators `def`, `` ` ``, `(` and `end` →
9967        // n1 = N1 = 4 (only the `(` opener counts after #695).
9968        // Operand `cmd` twice, parameter and use → n2 = 1, N2 = 2.
9969        assert_halstead_counts::<RubyParser>(
9970            "def `(cmd)\n  cmd\nend\n",
9971            "foo.rb",
9972            [4, 4, 1, 2],
9973            "backtick method name",
9974        );
9975    }
9976
9977    #[cfg(feature = "ruby")]
9978    #[test]
9979    fn ruby_subshell_guard_is_parent_scoped_not_ancestor_scoped() {
9980        // The one input that separates the correct parent-scoped guard
9981        // from the ancestor-scoped mutant of it (#1360, mirroring
9982        // #1312's regex case): backtick *methods* used inside a
9983        // subshell's `#{…}` interpolation. Their `BQUOTE` has the named
9984        // `operator` node as its parent but the `Subshell` as a further
9985        // ancestor, so an ancestor scan would swallow both. Every other
9986        // fixture in this file passes under either spelling. Two
9987        // markers — a definition and a call — so the mutant moves both
9988        // n1 (7 → 6) and N1 (10 → 8), per the #1294 count-only-anchor
9989        // lesson.
9990        //
9991        // expected: operators `=` (1), `def` (1), `` ` `` (2), `(` (2),
9992        // `;` (2), `end` (1), `.` (1) → n1 = 7, N1 = 10. Operands `n`,
9993        // `c` × 2, `s`, `"x"` → n2 = 4, N2 = 5; the wrapping `Subshell`
9994        // is skipped because it carries `Interpolation` children (the
9995        // #180 double-count guard), and its `a` / `b` string content is
9996        // classified by no arm.
9997        check_metrics::<RubyParser>(
9998            "n = `a#{def `(c); c; end}b#{s.`(\"x\")}`\n",
9999            "foo.rb",
10000            |metric| {
10001                assert_eq!(metric.halstead.unique_operators(), 7);
10002                assert_eq!(metric.halstead.total_operators(), 10);
10003                assert_eq!(metric.halstead.unique_operands(), 4);
10004                assert_eq!(metric.halstead.total_operands(), 5);
10005            },
10006        );
10007    }
10008
10009    #[cfg(feature = "ruby")]
10010    #[test]
10011    fn ruby_subshell_start_alias_never_reaches_kind_id() {
10012        // Drift marker for #1360's guard, the `BQUOTE2` sibling of
10013        // `ruby_regex_start_alias_never_reaches_kind_id`. `BQUOTE2`
10014        // sits in the enum's literal-start alias block beside `DQUOTE`,
10015        // `COLONDQUOTE` and `SLASH2`, and the runtime
10016        // `public_symbol_map` collapses it to `BQUOTE` before
10017        // `kind_id()`, exactly like `LPAREN2` in #768.
10018        //
10019        // `BQUOTE2` is therefore in no dispatch arm at all — not the
10020        // guard and not the operator arm — which is the asymmetry with
10021        // `SLASH2`, a kind #1312 had to *move* out of the arithmetic
10022        // arm. Adding it to the guard would change nothing, since the
10023        // wildcard already answers `Unknown` for it. What that leaves
10024        // unguarded is the reverse direction: a bump that started
10025        // emitting `BQUOTE2` would drop a subshell opener silently
10026        // (already the wanted answer) but would also stop counting a
10027        // `` def ` `` marker spelled that way. This pins the
10028        // reachability so such a bump fails here rather than moving a
10029        // metric unobserved.
10030        let path = PathBuf::from("foo.rb");
10031        for source in ["x = `echo`\n", "x = %x{echo}\n"] {
10032            let parser = RubyParser::new(source.as_bytes().to_vec(), &path, None);
10033            assert!(
10034                !ast_has_kind_id(&parser, Ruby::BQUOTE2 as u16),
10035                "Ruby::BQUOTE2 must stay collapsed to Ruby::BQUOTE for `{source}`"
10036            );
10037            // Positive control: the id the guard actually fires on is
10038            // present, so the assertion above cannot pass merely
10039            // because no delimiter was parsed at all.
10040            assert!(
10041                ast_has_kind_id(&parser, Ruby::BQUOTE as u16),
10042                "Ruby::BQUOTE must be the delimiter kind for `{source}`"
10043            );
10044        }
10045    }
10046
10047    #[cfg(feature = "ruby")]
10048    #[test]
10049    fn ruby_interpolation_opener_is_not_an_operator() {
10050        // Behaviour change, not a fabrication fix: #1314 drops
10051        // `HASHLBRACE` from Ruby's operator arm. `#{` is a token of its
10052        // own here — unlike PHP's `{`, which aliases the
10053        // compound-statement brace — so nothing was being miscounted;
10054        // the question was whether an interpolation opener is an
10055        // operation at all, and across the five interpolating languages
10056        // three already said no. Ruby Halstead operator counts drop for
10057        // interpolated literals as a result.
10058        //
10059        // Asserted as an invariance: the interpolated and plain
10060        // spellings of one string must now score identically, which is
10061        // the policy rather than a magic number. Before the change the
10062        // interpolated row was n1 = 2, N1 = 3.
10063        //
10064        // expected per row: operator `=` × 2 → n1 = 1, N1 = 2; operands
10065        // `s`, `t`, and the literal's content contribution × 2 →
10066        // n2 = 3, N2 = 4.
10067        for literal in ["\"a #{y} b\"", "\"a b\""] {
10068            assert_halstead_counts::<RubyParser>(
10069                &format!("s = {literal}\nt = {literal}\n"),
10070                "foo.rb",
10071                [1, 2, 3, 4],
10072                &format!("literal {literal}"),
10073            );
10074        }
10075    }
10076
10077    #[cfg(feature = "ruby")]
10078    #[test]
10079    fn ruby_interpolation_opener_drop_covers_every_literal() {
10080        // `HASHLBRACE` is one arm, but it fires under every Ruby
10081        // literal that interpolates, so the drop is not specific to
10082        // double-quoted strings. A symbol and a regex — two literals
10083        // whose `#{…}` reaches the same token — must contribute no
10084        // operator for the opener either.
10085        //
10086        // expected: operator `=` × 3 → n1 = 1, N1 = 3. Operands `y`,
10087        // `1`, `a`, `b`, plus the two interpolated `y` references →
10088        // n2 = 4, N2 = 6. Before the change each `#{` added one →
10089        // n1 = 2, N1 = 5.
10090        check_metrics::<RubyParser>("y = 1\na = :\"s#{y}\"\nb = /r#{y}/\n", "foo.rb", |metric| {
10091            assert_eq!(metric.halstead.unique_operators(), 1);
10092            assert_eq!(metric.halstead.total_operators(), 3);
10093            assert_eq!(metric.halstead.unique_operands(), 4);
10094            assert_eq!(metric.halstead.total_operands(), 6);
10095        });
10096    }
10097
10098    #[cfg(feature = "ruby")]
10099    #[test]
10100    fn ruby_element_containers_count_elements_not_the_composite_1353() {
10101        // #1353. `chained_string`, `string_array` and `symbol_array`
10102        // hold *classified operand* children instead of the raw
10103        // `string_content` every other string-like literal wraps, so
10104        // the arm's shared `Interpolation` guard never fired and the
10105        // wrapper was billed alongside each element. Every row below
10106        // scored n2 4 / N2 4 for three operands, the extra entry being
10107        // the wrapper's whole-span text — which made the vocabulary
10108        // depend on how the author grouped the literals.
10109        //
10110        // `assert_ops_operands` pins the operand *text*, not just the
10111        // count: a wrapper coming back would show up as `"one" "two"` /
10112        // `%w[x y]` / `%i[p q]` rather than as an off-by-one number.
10113        //
10114        // The kind assertions are the grammar-dispatch §1 / §2 drift
10115        // marker — a bump that renumbers a wrapper or an element would
10116        // otherwise leave these counts passing while measuring a
10117        // construct the arm no longer names.
10118        //
10119        // Two of the three rows also pin the arm's *membership*:
10120        // dropping `StringArray` or `SymbolArray` from it fails
10121        // `ruby_empty_word_and_symbol_arrays_still_bill_one_operand_1353`.
10122        // `ChainedString`'s membership is unobservable and no test can
10123        // pin it — `repeat1($.string)` guarantees the guard always
10124        // fires, so `Unknown` and "not in the arm at all" are the same
10125        // answer for every input. It is listed for symmetry with its
10126        // siblings, and correct-by-construction is the only defence
10127        // available there.
10128        for (source, wrapper, element, operands) in [
10129            (
10130                "a = \"one\" \"two\"\n",
10131                Ruby::ChainedString,
10132                Ruby::String,
10133                vec!["a", "\"one\"", "\"two\""],
10134            ),
10135            (
10136                "b = %w[x y]\n",
10137                Ruby::StringArray,
10138                Ruby::BareString,
10139                vec!["b", "x", "y"],
10140            ),
10141            (
10142                "c = %i[p q]\n",
10143                Ruby::SymbolArray,
10144                Ruby::BareSymbol,
10145                vec!["c", "p", "q"],
10146            ),
10147        ] {
10148            let parser =
10149                RubyParser::new(source.as_bytes().to_vec(), &PathBuf::from("foo.rb"), None);
10150            assert!(
10151                ast_has_kind_id(&parser, wrapper as u16),
10152                "the container kind this arm gates on is unreachable for `{source}`"
10153            );
10154            assert!(
10155                ast_has_kind_id(&parser, element as u16),
10156                "the element kind this arm gates on is unreachable for `{source}`"
10157            );
10158
10159            // expected: operator `=` → n1 = N1 = 1; operands are the
10160            // assignment target and the two elements → n2 = N2 = 3.
10161            assert_halstead_counts::<RubyParser>(source, "foo.rb", [1, 1, 3, 3], source);
10162            assert_ops_operands::<RubyParser>(source, "foo.rb", 3, operands);
10163        }
10164    }
10165
10166    #[cfg(feature = "ruby")]
10167    #[test]
10168    fn ruby_empty_word_and_symbol_arrays_still_bill_one_operand_1353() {
10169        // The childless spelling, and the whole reason #1353 gates the
10170        // three container kinds instead of dropping them from the arm
10171        // (grammar-dispatch §6). `%w[]` / `%i[]` parse to a wrapper
10172        // holding nothing but its two delimiter tokens, so the tempting
10173        // "just delete the wrapper" fix — the one #1351 was right to
10174        // take for Bash's `command_name` — would score an empty literal
10175        // zero operands where the source plainly has a literal. This is
10176        // the assertion to watch fail against that alternative.
10177        //
10178        // expected: operator `=` → n1 = N1 = 1; operands the assignment
10179        // target and the empty literal itself → n2 = N2 = 2.
10180        for (source, operands) in [
10181            ("d = %w[]\n", vec!["d", "%w[]"]),
10182            ("e = %i[]\n", vec!["e", "%i[]"]),
10183        ] {
10184            assert_halstead_counts::<RubyParser>(source, "foo.rb", [1, 1, 2, 2], source);
10185            assert_ops_operands::<RubyParser>(source, "foo.rb", 2, operands);
10186        }
10187    }
10188
10189    #[cfg(feature = "ruby")]
10190    #[test]
10191    fn ruby_interpolated_array_elements_are_not_double_counted_1353() {
10192        // `bare_string` and `bare_symbol` are two aliases of a single
10193        // grammar production (`_literal_contents`), one per array form,
10194        // so `%W[…]` and `%I[…]` must score identically. Until #1353
10195        // only `bare_string` carried the interpolation guard and
10196        // `bare_symbol` sat in the plain operand arm, so `%I[a#{n}b c]`
10197        // billed the element `a#{n}b` *and* the `n` inside it (n2 4)
10198        // where `%W[a#{n}b c]` billed 3.
10199        //
10200        // Asserted as an invariance over the two spellings, the way
10201        // `ruby_interpolation_opener_is_not_an_operator` is — the
10202        // policy is the claim, not the magic number. The third row is
10203        // the composed case: a `chained_string` whose own guard defers
10204        // to an element that is itself interpolated.
10205        //
10206        // expected per row: operator `=` → n1 = N1 = 1; operands the
10207        // assignment target, the interpolated expression `n`, and the
10208        // one inert element → n2 = N2 = 3. The operand *text* is pinned
10209        // too, because 3 is also the count an implementation that
10210        // suppressed the inert element instead of the wrapper would
10211        // report.
10212        for (source, operands) in [
10213            ("w = %W[a#{n}b c]\n", vec!["w", "n", "c"]),
10214            ("w = %I[a#{n}b c]\n", vec!["w", "n", "c"]),
10215            ("a = \"x#{n}\" \"y\"\n", vec!["a", "n", "\"y\""]),
10216        ] {
10217            assert_halstead_counts::<RubyParser>(source, "foo.rb", [1, 1, 3, 3], source);
10218            assert_ops_operands::<RubyParser>(source, "foo.rb", 3, operands);
10219        }
10220    }
10221
10222    #[cfg(feature = "ruby")]
10223    #[test]
10224    fn ruby_suffixed_numeric_literals_bill_one_operand_1359() {
10225        // #1359, the wrapper/leaf double count one arm below #1353's.
10226        // `rational` and `complex` wrap the numeral they suffix, and
10227        // `get_op_type` billed every level: `a = 1r` scored n2 3 / N2 3
10228        // for two operands, and `c = 3ri` — a `complex` over a
10229        // `rational` over an `integer` — scored n2 3 / N2 4 for the
10230        // same two.
10231        //
10232        // The rows are ordered by nesting depth on purpose, because the
10233        // half-fix is a plausible one to write. Guard the two leaf
10234        // kinds and not the intermediate `rational` — `R::Integer |
10235        // R::Float` where the arm says `R::Integer | R::Float |
10236        // R::Rational` — and the `1r` and `2i` rows still pass; the
10237        // `ri` rows, whose middle `rational` must be suppressed as well
10238        // as the leaf beneath it, are the two that fail (measured:
10239        // `c = 3ri` reports n2 3 / N2 3).
10240        //
10241        // The trailing rows are the ones that must *not* move. `g = 4`
10242        // has no wrapper at all and pins that the guard is scoped to
10243        // the numerals inside one (grammar-dispatch §6 — nothing
10244        // regresses to zero). `"#{4r}"` and `-3r` put the wrapper
10245        // somewhere other than an assignment right-hand side, under a
10246        // node that guards its own children (`Interpolation`, whose
10247        // enclosing `String` is suppressed by the arm above) and under
10248        // one that does not (`unary`); both must still bill the
10249        // wrapper once. They do *not* pin parent-versus-ancestor
10250        // scoping and nothing can: `complex` and `rational` admit
10251        // numeral children only, so an ancestor-scoped guard would
10252        // classify every Ruby input identically. And `1r + 1r` is the
10253        // row where N2 and n2 come apart, so the `N2` column is
10254        // load-bearing on its own rather than tracking `n2`.
10255        //
10256        // The kind assertions are the grammar-dispatch §1 / §2 drift
10257        // marker: a bump that renumbered `complex` or `rational`, or
10258        // stopped nesting them, would leave the counts passing while
10259        // measuring a construct the guard no longer names. `IntOrFloat`
10260        // is the §2 half — the hidden `_int_or_float` the two wrappers
10261        // are written over, which a bump could promote into the
10262        // numeral's parent and silently stop the guard firing. Not
10263        // every kind a row mentions is one the guard names
10264        // (`Interpolation` is context), and `unary` and `binary` are
10265        // deliberately absent: they carry five and three aliased
10266        // variants here, so naming one would pin the alias rather than
10267        // the shape.
10268        for (source, kinds, counts, operands) in [
10269            (
10270                "a = 1r\n",
10271                &[Ruby::Rational as u16, Ruby::Integer as u16][..],
10272                [1, 1, 2, 2],
10273                vec!["a", "1r"],
10274            ),
10275            (
10276                "b = 2i\n",
10277                &[Ruby::Complex as u16, Ruby::Integer as u16][..],
10278                [1, 1, 2, 2],
10279                vec!["b", "2i"],
10280            ),
10281            (
10282                "c = 3ri\n",
10283                &[
10284                    Ruby::Complex as u16,
10285                    Ruby::Rational as u16,
10286                    Ruby::Integer as u16,
10287                ][..],
10288                [1, 1, 2, 2],
10289                vec!["c", "3ri"],
10290            ),
10291            (
10292                "d = 1.5r\n",
10293                &[Ruby::Rational as u16, Ruby::Float as u16][..],
10294                [1, 1, 2, 2],
10295                vec!["d", "1.5r"],
10296            ),
10297            (
10298                "e = 2.5i\n",
10299                &[Ruby::Complex as u16, Ruby::Float as u16][..],
10300                [1, 1, 2, 2],
10301                vec!["e", "2.5i"],
10302            ),
10303            (
10304                "f = 1.5ri\n",
10305                &[
10306                    Ruby::Complex as u16,
10307                    Ruby::Rational as u16,
10308                    Ruby::Float as u16,
10309                ][..],
10310                [1, 1, 2, 2],
10311                vec!["f", "1.5ri"],
10312            ),
10313            (
10314                "g = 4\n",
10315                &[Ruby::Integer as u16][..],
10316                [1, 1, 2, 2],
10317                vec!["g", "4"],
10318            ),
10319            (
10320                "h = \"#{4r}\"\n",
10321                &[Ruby::Interpolation as u16, Ruby::Rational as u16][..],
10322                [1, 1, 2, 2],
10323                vec!["h", "4r"],
10324            ),
10325            (
10326                "i = -3r\n",
10327                &[Ruby::Rational as u16, Ruby::Integer as u16][..],
10328                [2, 2, 2, 2],
10329                vec!["i", "3r"],
10330            ),
10331            (
10332                "j = 1r + 1r\n",
10333                &[Ruby::Rational as u16, Ruby::Integer as u16][..],
10334                [2, 2, 2, 3],
10335                vec!["j", "1r"],
10336            ),
10337        ] {
10338            let parser =
10339                RubyParser::new(source.as_bytes().to_vec(), &PathBuf::from("foo.rb"), None);
10340            for kind in kinds {
10341                assert!(
10342                    ast_has_kind_id(&parser, *kind),
10343                    "the kind this row measures is unreachable for `{source}`"
10344                );
10345            }
10346            assert!(
10347                !ast_has_kind_id(&parser, Ruby::IntOrFloat as u16),
10348                "`_int_or_float` is hidden; a grammar that emits it would \
10349                 sit between the wrapper and the numeral for `{source}`"
10350            );
10351
10352            assert_halstead_counts::<RubyParser>(source, "foo.rb", counts, source);
10353            assert_ops_operands::<RubyParser>(source, "foo.rb", operands.len(), operands);
10354        }
10355    }
10356
10357    #[cfg(feature = "ruby")]
10358    #[test]
10359    fn ruby_numeric_suffixes_stay_distinct_operands_1359() {
10360        // The reason #1359 keeps the *wrapper* and gates the leaf
10361        // rather than dropping the two wrapper kinds from the arm, as
10362        // #1351 and #1358 did for their Bash wrappers. `1`, `1r`, `1i`
10363        // and `1ri` are four distinct Ruby constants — Integer,
10364        // Rational, Complex and Complex-of-Rational — and only the
10365        // wrapper's span carries the suffix that says which.
10366        //
10367        // This is the assertion to watch fail against the leaf-keeping
10368        // alternative: billing the `integer` would file all four under
10369        // the operand text `1`, reporting n2 5 / N2 8 where the source
10370        // plainly has eight distinct operands. The count alone does not
10371        // say that, so the vocabulary is pinned by text.
10372        //
10373        // expected: operator `=` → n1 1, N1 4; four targets and four
10374        // constants, none repeated → n2 = N2 = 8.
10375        let source = "w = 1\nx = 1r\ny = 1i\nz = 1ri\n";
10376        assert_halstead_counts::<RubyParser>(source, "foo.rb", [1, 4, 8, 8], source);
10377        assert_ops_operands::<RubyParser>(
10378            source,
10379            "foo.rb",
10380            8,
10381            vec!["w", "x", "y", "z", "1", "1r", "1i", "1ri"],
10382        );
10383    }
10384
10385    /// Comprehensive iRules Halstead test exercising every operator family
10386    /// classified in `get_op_type`: declaration/control keywords (`proc`,
10387    /// `set`, `if`, `return`), structural punctuation (`{}` `[]` `()`),
10388    /// arithmetic (`+`), comparison (`>`), the word-form string comparator
10389    /// (`eq`), and short-circuit logical (`&&`). Anchored on the integer
10390    /// `n1`/`N1`/`n2`/`N2` headline values; the float fields are derived and
10391    /// bit-brittle, so they are not pinned.
10392    ///
10393    /// The second half pins the lesson-4 invariant: the independent
10394    /// text-keyed `operands_and_operators` store must dedupe to the same
10395    /// `n1`/`n2`. A classification change that moved one store without the
10396    /// other (e.g. a kind landing in both the operator and operand arms)
10397    /// would break this even though the snapshot stayed green.
10398    #[cfg(feature = "irules")]
10399    #[test]
10400    fn irules_operators_and_operands() {
10401        let source = "proc f { a b } {
10402    set x [expr { $a + $b }]
10403    if { $x > 0 && $a eq \"go\" } {
10404        return $x
10405    }
10406    return 0
10407}
10408";
10409        check_metrics::<IrulesParser>(source, "foo.irule", |metric| {
10410            // After #695 only opening delimiters count: the `}`/`]`
10411            // closers no longer add operators (was 12 unique / 20 total).
10412            assert_eq!(metric.halstead.unique_operators(), 10);
10413            assert_eq!(metric.halstead.total_operators(), 14);
10414            // Operands fell 12 / 16 → 10 / 14 with #1354: the proc body
10415            // and the `if` body are `BracedWord` script kinds and are no
10416            // longer operands beside the commands they contain.
10417            assert_eq!(metric.halstead.unique_operands(), 10);
10418            assert_eq!(metric.halstead.total_operands(), 14);
10419        });
10420
10421        let path = PathBuf::from("foo.irule");
10422        let parser = IrulesParser::new(source.as_bytes().to_vec(), &path, None);
10423        let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
10424        let unique_operators: HashSet<&str> = ops.operators.iter().map(String::as_str).collect();
10425        let unique_operands: HashSet<&str> = ops.operands.iter().map(String::as_str).collect();
10426        assert_eq!(
10427            unique_operators.len(),
10428            10,
10429            "dedupe(ops.operators) must equal n1; operators were {:?}",
10430            ops.operators
10431        );
10432        assert_eq!(
10433            unique_operands.len(),
10434            10,
10435            "dedupe(ops.operands) must equal n2; operands were {:?}",
10436            ops.operands
10437        );
10438    }
10439
10440    /// An inert `"hello world"` double-quoted string (no `$var` / `[cmd]`
10441    /// interpolation child) contributes exactly **one** operand — the
10442    /// wrapping `QuotedWord`. Operands are `f`, `s` and `"hello world"` —
10443    /// n2 = 3, the same as Tcl since #1294 restored its `set` target to
10444    /// the operand count and #1354 dropped the proc-body `braced_word`
10445    /// from both. Mirrors `tcl_inert_quoted_word_counts_as_operand`
10446    /// (#277).
10447    #[cfg(feature = "irules")]
10448    #[test]
10449    fn irules_inert_quoted_word_counts_as_operand() {
10450        let source = "proc f {} {\n    set s \"hello world\"\n}\n";
10451        check_metrics::<IrulesParser>(source, "foo.irule", |metric| {
10452            // After #695 only the `{` opener counts; the `}` closer no
10453            // longer adds an operator (was 4 unique / 6 total).
10454            assert_eq!(metric.halstead.unique_operators(), 3);
10455            assert_eq!(metric.halstead.total_operators(), 4);
10456            assert_eq!(metric.halstead.unique_operands(), 3);
10457            assert_eq!(metric.halstead.total_operands(), 3);
10458        });
10459
10460        let path = PathBuf::from("foo.irule");
10461        let parser = IrulesParser::new(source.as_bytes().to_vec(), &path, None);
10462        let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
10463        // The inert quoted word is present as exactly one operand (not
10464        // dropped, not split): dropping it would mean the inert branch was
10465        // over-guarded.
10466        let quoted = ops
10467            .operands
10468            .iter()
10469            .filter(|o| o.as_str() == "\"hello world\"")
10470            .count();
10471        assert_eq!(quoted, 1, "inert quoted word must be one operand");
10472        let unique_operands: HashSet<&str> = ops.operands.iter().map(String::as_str).collect();
10473        assert_eq!(unique_operands.len(), 3, "operands were {:?}", ops.operands);
10474    }
10475
10476    /// Regression for the `QuotedWord` interpolation guard (the #277 /
10477    /// Bash-#180 / C#-#183 / PHP-#184 pattern). An interpolated
10478    /// `"$x is $y"` must contribute **zero** operands for the wrapping
10479    /// `QuotedWord`; the inner `$x` / `$y` `variable_substitution` nodes are
10480    /// walked separately and count on their own. Operands are `f`, `x`, `y`,
10481    /// `s`, `$x`, `$y` = 6 (7 before #1354 dropped the proc-body
10482    /// `braced_word`). If the guard regressed (wrapper classified
10483    /// `Operand`), the wrapper string would add a 7th operand. This is the
10484    /// branch that had no test before.
10485    #[cfg(feature = "irules")]
10486    #[test]
10487    fn irules_interpolated_quoted_word_no_double_count() {
10488        let source = "proc f {x y} {\n    set s \"$x is $y\"\n}\n";
10489        check_metrics::<IrulesParser>(source, "foo.irule", |metric| {
10490            // After #695 only the `{` opener counts; the `}` closer no
10491            // longer adds an operator (was 4 unique / 6 total).
10492            assert_eq!(metric.halstead.unique_operators(), 3);
10493            assert_eq!(metric.halstead.total_operators(), 4);
10494            assert_eq!(metric.halstead.unique_operands(), 6);
10495            assert_eq!(metric.halstead.total_operands(), 6);
10496        });
10497
10498        let path = PathBuf::from("foo.irule");
10499        let parser = IrulesParser::new(source.as_bytes().to_vec(), &path, None);
10500        let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
10501        // The wrapping interpolated string must NOT appear as an operand;
10502        // its inner substitutions must. The wrapper, if wrongly counted,
10503        // would surface as the quoted literal `"$x is $y"` (with quotes,
10504        // like the inert `"hello world"` operand). Match that exact token —
10505        // a substring check would false-match the proc-body `braced_word`
10506        // operand, which legitimately contains the source text.
10507        assert!(
10508            !ops.operands.iter().any(|o| o.as_str() == "\"$x is $y\""),
10509            "interpolated wrapper must not be an operand; operands were {:?}",
10510            ops.operands
10511        );
10512        assert!(
10513            ops.operands.iter().any(|o| o.as_str() == "$x")
10514                && ops.operands.iter().any(|o| o.as_str() == "$y"),
10515            "inner $x / $y substitutions must each be operands; operands were {:?}",
10516            ops.operands
10517        );
10518        let unique_operands: HashSet<&str> = ops.operands.iter().map(String::as_str).collect();
10519        assert_eq!(unique_operands.len(), 6, "operands were {:?}", ops.operands);
10520    }
10521
10522    /// Exercises the operator families not covered by
10523    /// `irules_operators_and_operands`: bitwise (`& | ^ ~ << >>`), ternary
10524    /// (`? :`), the keyword string comparators (`starts_with`, `ends_with`,
10525    /// `contains`, `matches`, `eq`, `ne`), and the keyword logical operator
10526    /// (`and`). Pins every operator-family arm in `get_op_type` plus the
10527    /// lesson-4 dedupe invariant.
10528    #[cfg(feature = "irules")]
10529    #[test]
10530    fn irules_bitwise_ternary_string_ops() {
10531        let source = "proc f { a b } {
10532    set bits [expr { $a & $b | $a ^ ~$b }]
10533    set sh [expr { $a << 2 | $b >> 1 }]
10534    set t [expr { $a > 0 ? $a : $b }]
10535    if { $a starts_with \"x\" && $b ends_with \"y\" } { return 1 }
10536    if { $a contains \"z\" || $b matches \"q\" } { return 2 }
10537    if { $a eq \"m\" and $b ne \"n\" } { return 3 }
10538    return $b
10539}
10540";
10541        check_metrics::<IrulesParser>(source, "foo.irule", |metric| {
10542            // After #695 only opening delimiters count: the `}`/`]`
10543            // closers no longer add operators (was 26 unique / 57 total).
10544            assert_eq!(metric.halstead.unique_operators(), 24);
10545            assert_eq!(metric.halstead.total_operators(), 43);
10546            // Operands fell 23 / 42 → 19 / 38 with #1354: the proc body
10547            // and the three single-statement `if` bodies are
10548            // `BracedWord` script kinds and no longer count as operands.
10549            assert_eq!(metric.halstead.unique_operands(), 19);
10550            assert_eq!(metric.halstead.total_operands(), 38);
10551        });
10552
10553        let path = PathBuf::from("foo.irule");
10554        let parser = IrulesParser::new(source.as_bytes().to_vec(), &path, None);
10555        let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
10556        let unique_operators: HashSet<&str> = ops.operators.iter().map(String::as_str).collect();
10557        let unique_operands: HashSet<&str> = ops.operands.iter().map(String::as_str).collect();
10558        assert_eq!(
10559            unique_operators.len(),
10560            24,
10561            "dedupe(ops.operators) must equal n1; operators were {:?}",
10562            ops.operators
10563        );
10564        assert_eq!(
10565            unique_operands.len(),
10566            19,
10567            "dedupe(ops.operands) must equal n2; operands were {:?}",
10568            ops.operands
10569        );
10570    }
10571
10572    /// A bare `$x` produces one `variable_substitution` operand. Its inner
10573    /// `id` leaf (the *named* `Id` node — not the anonymous `Id2` token Tcl
10574    /// has there) must NOT be counted separately, or every variable
10575    /// reference double-counts. `get_op_type` excludes `Id` whose parent is
10576    /// a `VariableSubstitution`. Operands: `f`, the proc arg `x`, `return`
10577    /// and `$x` — four, with no duplicate (`total_operands()` == 4; the
10578    /// proc-body `braced_word` was a fifth before #1354). If the guard
10579    /// regressed, the inner `id` "x" would add a fifth operand occurrence
10580    /// (it text-collides with the proc arg `x`, so `u_operands` would stay
10581    /// 4 but `total_operands()` would rise to 5 — hence the total, not just
10582    /// the unique count, is asserted).
10583    #[cfg(feature = "irules")]
10584    #[test]
10585    fn irules_array_reference_bills_the_reference_and_the_index() {
10586        // The iRules twin of
10587        // `tcl_array_reference_bills_the_reference_and_the_index`. Until
10588        // `ArrayIndex` left the operand arm this fixture billed `(k)` and
10589        // `($i)` beside the six operands below — the grammar-dispatch §5
10590        // wrapper-plus-leaf count, and a divergence from Tcl.
10591        assert_ops_operands::<IrulesParser>(
10592            "set arr(k) 1\nset z \"$arr($i)\"\n",
10593            "foo.irule",
10594            6,
10595            vec!["arr", "k", "1", "z", "$arr($i)", "$i"],
10596        );
10597    }
10598
10599    #[cfg(feature = "irules")]
10600    #[test]
10601    fn irules_bare_variable_operand() {
10602        let source = "proc f {x} {\n    return $x\n}\n";
10603        check_metrics::<IrulesParser>(source, "foo.irule", |metric| {
10604            // After #695 only the `{` opener counts (folded `{}`); the
10605            // `}` closer no longer adds an operator (was 3 unique / 5 total).
10606            assert_eq!(metric.halstead.unique_operators(), 2);
10607            assert_eq!(metric.halstead.total_operators(), 3);
10608            assert_eq!(metric.halstead.unique_operands(), 4);
10609            assert_eq!(metric.halstead.total_operands(), 4);
10610        });
10611
10612        let path = PathBuf::from("foo.irule");
10613        let parser = IrulesParser::new(source.as_bytes().to_vec(), &path, None);
10614        let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
10615        let bare_var = ops.operands.iter().filter(|o| o.as_str() == "$x").count();
10616        assert_eq!(
10617            bare_var, 1,
10618            "bare $x must be exactly one operand (inner id leaf not double-counted); operands were {:?}",
10619            ops.operands
10620        );
10621    }
10622
10623    #[cfg(feature = "irules")]
10624    #[test]
10625    fn irules_braced_word_delimiter_is_not_an_operator() {
10626        // Regression: issue #1314. The iRules twin of
10627        // `tcl_braced_word_delimiter_is_not_an_operator` — the two
10628        // getters are deliberate clones, so the guard lands in both and
10629        // is asserted in both.
10630        //
10631        // One fixture covers the guard and its control: `{braced word}`
10632        // and `{y}` are values whose openers must not count, while the
10633        // handler body, the `if` condition and the `if` body are
10634        // `BracedWord` / `Expr` and must keep theirs. A guard keyed on
10635        // the brace alone would take `{}` out of the operator set
10636        // entirely and fail here.
10637        //
10638        // expected: operators `when`, `set` × 2, `if`, `contains`,
10639        // `[]`, `{}` × 3 → n1 = 6, N1 = 9. Every operand is distinct →
10640        // n2 = N2 = 7: `HTTP_REQUEST`, `a`, `b`, `HTTP::uri`, `"x"` and
10641        // the two braced words `{braced word}` and `{y}`. Before the
10642        // guard the two value openers added two more `{}` occurrences →
10643        // N1 = 11; before #1354 the handler and `if` bodies and the
10644        // inner `braced` / `word` / `y` were operands too → n2 = 12.
10645        check_metrics::<IrulesParser>(
10646            "when HTTP_REQUEST {\n  set a {braced word}\n  if {[HTTP::uri] contains \"x\"} { set b {y} }\n}\n",
10647            "foo.irule",
10648            |metric| {
10649                assert_eq!(metric.halstead.unique_operators(), 6);
10650                assert_eq!(metric.halstead.total_operators(), 9);
10651                assert_eq!(metric.halstead.unique_operands(), 7);
10652                assert_eq!(metric.halstead.total_operands(), 7);
10653            },
10654        );
10655    }
10656
10657    /// Regression for #563: the two Halstead `Display` labels must use the
10658    /// underscore key that matches the JSON/CSV field name, so a user can grep
10659    /// the same token across `Display` and JSON. The space-separated forms
10660    /// (`estimated program length` / `purity ratio`) were the only outliers,
10661    /// mirroring the `dump` fix in #562.
10662    #[cfg(feature = "cpp")]
10663    #[test]
10664    fn display_halstead_labels_use_underscore_keys() {
10665        check_metrics::<CppParser>("int a = 42;", "foo.cpp", |metric| {
10666            let out = metric.halstead.to_string();
10667            assert!(
10668                out.contains("estimated_program_length: "),
10669                "Display must use the underscore key `estimated_program_length`:\n{out}"
10670            );
10671            assert!(
10672                out.contains("purity_ratio: "),
10673                "Display must use the underscore key `purity_ratio`:\n{out}"
10674            );
10675            assert!(
10676                !out.contains("estimated program length"),
10677                "Display must not emit the space-separated `estimated program length`:\n{out}"
10678            );
10679            assert!(
10680                !out.contains("purity ratio"),
10681                "Display must not emit the space-separated `purity ratio`:\n{out}"
10682            );
10683        });
10684    }
10685
10686    /// `@"…"` is one `string_literal` holding its `@` as a child, so the
10687    /// literal is the operand, keyed by its whole text, and the marker is
10688    /// not an operator on top — it was, which planted a phantom `@` in
10689    /// n1 for a file whose only `@` was in NSString literals and billed
10690    /// the same byte in both streams. Boxing (`@42`) keeps its `@`: there
10691    /// the token is a child of the `at_expression`, not of the literal.
10692    #[cfg(feature = "objc")]
10693    #[test]
10694    fn objc_nsstring_literal_is_one_operand() {
10695        // expected: [n1, N1, n2, N2], each operator count including the
10696        // wrapper's `@implementation` (#1552). Before the guard the first
10697        // two rows read [9, 9, 5, 5] and [9, 10, 6, 6] — one `@` operator
10698        // per literal; the boxing control is unchanged.
10699        let cases = [
10700            ("NSString *s = @\"str\";", [8, 8, 5, 5]),
10701            ("NSString *t = @\"x\" @\"y\";", [8, 8, 6, 6]),
10702            ("NSNumber *n = @42;", [9, 9, 5, 5]),
10703        ];
10704        for (body, counts) in cases {
10705            let source = format!("@implementation Foo\n- (void)m {{\n    {body}\n}}\n@end\n");
10706            assert_halstead_counts::<ObjcParser>(&source, "foo.m", counts, body);
10707        }
10708        assert_ops_operands::<ObjcParser>(
10709            "@implementation Foo\n- (void)m {\n    NSString *s = @\"str\";\n}\n@end\n",
10710            "foo.m",
10711            5,
10712            vec!["Foo", "m", "NSString", "s", "@\"str\""],
10713        );
10714    }
10715
10716    /// Comprehensive Objective-C Halstead fixture exercising a message
10717    /// send (`[self log:@"hi"]`), an ObjC string literal (`@"hi"`), an
10718    /// `if`, a short-circuit `&&`, arithmetic (`+`), comparisons, and
10719    /// assignment. Pins every field and enforces the lesson-4 invariants
10720    /// `unique_operators == n1` / `unique_operands == n2` via the
10721    /// independent `--ops` store.
10722    #[cfg(feature = "objc")]
10723    #[test]
10724    fn objc_operators_and_operands() {
10725        let source = "@implementation Foo
10726- (int)bar:(int)x {
10727    int y = x + 1;
10728    if (x > 0 && y < 10) {
10729        [self log:@\"hi\"];
10730    }
10731    return y;
10732}
10733@end
10734";
10735        check_metrics::<ObjcParser>(source, "foo.m", |metric| {
10736            // n1 = 15 unique operators:
10737            //   `&&`, `()`, `+`, `-`, `:`, `;`, `<`, `=`, `>`,
10738            //   `[]` (message send), `if`, `int`, `return`, `{}`,
10739            //   `@implementation` (#1552).
10740            //   The `@` of `@"hi"` is part of the literal's operand
10741            //   key, not an operator (see `objc_nsstring_literal_is_one_operand`).
10742            // n2 = 10 unique operands:
10743            //   `Foo`, `bar`, `log`, `self`, `x`, `y`, `0`, `1`, `10`,
10744            //   `@"hi"` (the ObjC string literal).
10745            assert_eq!(metric.halstead.unique_operators(), 15);
10746            assert_eq!(metric.halstead.unique_operands(), 10);
10747            insta::assert_json_snapshot!(metric.halstead, @r#"
10748            {
10749              "unique_operators": 15,
10750              "total_operators": 23,
10751              "unique_operands": 10,
10752              "total_operands": 14,
10753              "length": 37,
10754              "estimated_program_length": 91.82263988300141,
10755              "purity_ratio": 2.481692969810849,
10756              "vocabulary": 25,
10757              "volume": 171.8226790216648,
10758              "difficulty": 10.5,
10759              "level": 0.09523809523809523,
10760              "effort": 1804.1381297274804,
10761              "time": 100.22989609597113,
10762              "bugs": 0.049399808887691035
10763            }
10764            "#);
10765        });
10766        // Lesson-4 invariant: dedupe(ops.operands) == n2 (10), via the
10767        // independent text-keyed `--ops` store.
10768        assert_ops_operands::<ObjcParser>(
10769            source,
10770            "foo.m",
10771            10,
10772            vec![
10773                "Foo", "bar", "log", "self", "x", "y", "0", "1", "10", "@\"hi\"",
10774            ],
10775        );
10776    }
10777
10778    /// #1316 fixture separating the vocabulary and occurrence axes
10779    /// (#1294): `'x'` appears twice, so a correct fix adds four `n2`
10780    /// entries and five `N2` hits. The `'ab'` multi-character constant
10781    /// is also the grammar-dispatch section 5 pin — that literal carries
10782    /// *two* `character` children and must still bill one operand.
10783    ///
10784    /// Both #1316 fixtures are plain C, which every C-family grammar
10785    /// parses to the same shape, so one source proves the same thing
10786    /// about each of the four clones.
10787    #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
10788    const C_FAMILY_CHAR_REPEATS: &str =
10789        "char a = 'x';\nchar b = 'x';\nchar c = 'y';\nchar d = '\\n';\nint e = 'ab';\n";
10790
10791    /// #1316 fixture holding all five spellings the grammars admit. Each
10792    /// opens on a distinct delimiter kind (`'`, `L'`, `u'`, `U'`, `u8'`),
10793    /// and operands key on source text, so the five are five vocabulary
10794    /// entries rather than one.
10795    #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
10796    const C_FAMILY_CHAR_PREFIXES: &str =
10797        "char a = 'x';\nchar b = L'x';\nchar c = u'x';\nchar d = U'x';\nchar e = u8'x';\n";
10798
10799    /// Asserts both #1316 fixtures for one C-family language, through
10800    /// the metrics store *and* the text-keyed `--ops` store (the
10801    /// lesson-4 invariant `n2 == len(dedupe(ops.operands))`).
10802    ///
10803    /// The `--ops` half pins *which text* each literal is billed under,
10804    /// which the counts cannot see: billing a literal's `character`
10805    /// payload instead of the whole literal keeps `n2` at 9 while the
10806    /// vocabulary silently becomes `x` rather than `'x'`. (Billing
10807    /// *both* is caught earlier, by the counts.) It is a second *walk*
10808    /// rather than a second classification — `ops_inner` reads the keys
10809    /// of the same `HalsteadMaps` — which is worth knowing before
10810    /// reading it as independent corroboration of the count.
10811    #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
10812    #[track_caller]
10813    fn assert_char_literal_operands<T: crate::MetricSuite>(file: &str, label: &str) {
10814        // `char` x4 and `int` are text-keyed primitive operators, so
10815        // n1 = 4 (`;`, `=`, `char`, `int`) and N1 = 5 + 5 + 4 + 1.
10816        // Operands: `a`..`e`, plus `'x'` (twice), `'y'`, `'\n'`, `'ab'`.
10817        let repeats = format!("{label}: repeated / escaped / multi-char literals");
10818        assert_halstead_counts::<T>(C_FAMILY_CHAR_REPEATS, file, [4, 15, 9, 10], &repeats);
10819        assert_ops_operands::<T>(
10820            C_FAMILY_CHAR_REPEATS,
10821            file,
10822            9,
10823            vec!["a", "b", "c", "d", "e", "'x'", "'y'", "'\\n'", "'ab'"],
10824        );
10825
10826        // Every declaration is `char` here, so the `int` primitive
10827        // operator of the other fixture is gone and n1 drops to 3. Ten
10828        // distinct operands, each seen once.
10829        let prefixes = format!("{label}: L / u / U / u8 prefixed literals");
10830        assert_halstead_counts::<T>(C_FAMILY_CHAR_PREFIXES, file, [3, 15, 10, 10], &prefixes);
10831        assert_ops_operands::<T>(
10832            C_FAMILY_CHAR_PREFIXES,
10833            file,
10834            10,
10835            vec![
10836                "a", "b", "c", "d", "e", "'x'", "L'x'", "u'x'", "U'x'", "u8'x'",
10837            ],
10838        );
10839    }
10840
10841    /// Regression for #1316: a C-family character literal is a Halstead
10842    /// operand.
10843    ///
10844    /// `char_literal` was in no arm of `CCode` / `CppCode` /
10845    /// `MozcppCode` / `ObjcCode`'s `get_op_type`, so a character literal
10846    /// contributed *nothing* — not an operator (correct) and not an
10847    /// operand (wrong) — while Rust, Java, Kotlin, C#, Go and Elixir all
10848    /// counted theirs. Both fixtures measured `n2` 5, `N2` 5 before the
10849    /// fix: the five declared identifiers and not one literal.
10850    ///
10851    /// Each language asserts separately over the same source rather than
10852    /// sharing one call, so reverting one clone's arm fails that row
10853    /// alone (grammar-dispatch section 11) — verified by perturbing each
10854    /// of the four arms in turn. A single shared assertion would be
10855    /// satisfied by whichever clone still had the arm.
10856    ///
10857    /// Mozcpp owns no file extension, so no integration snapshot ever
10858    /// reaches its clone; its row is the whole coverage that arm has.
10859    #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
10860    #[test]
10861    fn c_family_char_literals_are_operands() {
10862        assert_char_literal_operands::<CParser>("chars.c", "c");
10863        assert_char_literal_operands::<CppParser>("chars.cpp", "cpp");
10864        assert_char_literal_operands::<MozcppParser>("chars.cpp", "mozcpp");
10865        assert_char_literal_operands::<ObjcParser>("chars.m", "objc");
10866    }
10867
10868    /// ObjC boxes a character literal as `@'y'` — an `at_expression`
10869    /// wrapping the same `char_literal`, with the `@` counted as its own
10870    /// operator. The wrapper is in no operand arm, so the boxed form
10871    /// bills exactly the literal it wraps and stays distinct from a bare
10872    /// one (#1316).
10873    #[cfg(feature = "objc")]
10874    #[test]
10875    fn objc_boxed_char_literal_is_one_operand() {
10876        let source = "char a = 'x';\nid b = @'y';\n";
10877        // n1: `char`, `=`, `;`, `@`. N1: 1 + 2 + 2 + 1.
10878        // n2 / N2: `a`, `b`, `'x'`, `'y'` — `id` is a `typedefed_specifier`
10879        // and is classified by neither arm. Before #1316 this was n2 2,
10880        // N2 2.
10881        assert_halstead_counts::<ObjcParser>(source, "boxed.m", [4, 6, 4, 4], "objc @'y'");
10882        assert_ops_operands::<ObjcParser>(source, "boxed.m", 4, vec!["a", "b", "'x'", "'y'"]);
10883    }
10884
10885    /// Walks both #1316 fixtures under all four C-family `Getter`s and
10886    /// pins the two grammar facts the new operand arm rests on.
10887    ///
10888    /// * **Grammar-dispatch section 1.** In the positions these
10889    ///   fixtures exercise, every node the grammar spells `char_literal`
10890    ///   carries the one `kind_id` the arm lists. That is the weaker
10891    ///   half of the alias evidence — an alias arises in a *different*
10892    ///   syntactic position, which no fixture can enumerate. The strong
10893    ///   half is that these generated enums do carry numeric-suffix
10894    ///   aliases in quantity (`language_c.rs` alone has ninety) and none
10895    ///   of the four spells a `CharLiteral2`, so its absence is a
10896    ///   measurement rather than a silence. This loop is what notices if
10897    ///   a grammar bump changes that under an existing fixture.
10898    /// * **Grammar-dispatch section 5.** No child of a `char_literal` is
10899    ///   classified. That is what makes listing the wrapper safe rather
10900    ///   than a wrapper/leaf double count: the opening delimiter, the
10901    ///   closing `'`, and the `character` / `escape_sequence` payload
10902    ///   must all stay `Unknown`, or every literal would bill two
10903    ///   operands and a prefixed one three.
10904    ///
10905    /// Both loops are non-vacuous by assertion, since a fixture that
10906    /// stopped containing a character literal would otherwise make this
10907    /// test pass having checked nothing.
10908    #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
10909    #[test]
10910    fn c_family_char_literal_internals_stay_unclassified() {
10911        #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
10912        fn check<L: LanguageInfo + Getter>(char_literal: u16, label: &str) {
10913            let mut literals = 0_usize;
10914            let mut children = 0_usize;
10915            for source in [C_FAMILY_CHAR_REPEATS, C_FAMILY_CHAR_PREFIXES] {
10916                for_each_node_with_chain::<L>(source.as_bytes(), |node, chain| {
10917                    if node.kind() == "char_literal" {
10918                        assert_eq!(
10919                            node.kind_id(),
10920                            char_literal,
10921                            "{label}: a `char_literal` carries kind_id {} rather than the \
10922                             {char_literal} the operand arm lists — an alias the arm cannot see",
10923                            node.kind_id()
10924                        );
10925                        literals += 1;
10926                    }
10927                    if chain
10928                        .last()
10929                        .is_none_or(|parent| parent.kind_id() != char_literal)
10930                    {
10931                        return;
10932                    }
10933                    children += 1;
10934                    // `_with_code` is the spelling `compute_halstead`
10935                    // calls. The default forwards to the byte-less form,
10936                    // so today the two agree for every C-family
10937                    // language — which is exactly why asking the wrong
10938                    // one would read as correct right up until one of
10939                    // these four grew an override (grammar-dispatch
10940                    // section 7).
10941                    assert!(
10942                        matches!(
10943                            L::get_op_type_with_code(
10944                                node,
10945                                source.as_bytes(),
10946                                Ancestors::known(chain)
10947                            ),
10948                            TokenRole::Unknown
10949                        ),
10950                        "{label}: `{}` inside a character literal is classified, so the \
10951                         literal now double-counts against its wrapper",
10952                        node.kind()
10953                    );
10954                });
10955            }
10956            // Ten literals across the two fixtures; each holds two
10957            // delimiters plus at least one payload leaf, and `'ab'` two.
10958            assert_eq!(literals, 10, "{label}: fixtures lost a character literal");
10959            assert_eq!(children, 31, "{label}: fixtures lost a literal's internals");
10960        }
10961
10962        check::<CCode>(C::CharLiteral as u16, "c");
10963        check::<CppCode>(Cpp::CharLiteral as u16, "cpp");
10964        check::<MozcppCode>(Mozcpp::CharLiteral as u16, "mozcpp");
10965        check::<ObjcCode>(Objc::CharLiteral as u16, "objc");
10966    }
10967
10968    /// #1361 fixture pairing the two spellings of a member access whose
10969    /// receiver is the enclosing object: `this->x` and `p->x`.
10970    ///
10971    /// The pairing is the point, not decoration. `field_expression` is
10972    /// `<receiver> -> <field>`, so the two lines are the same AST shape
10973    /// and must bill the same *kinds* of thing — a receiver operand, the
10974    /// `->` operator, a field operand. Before the fix `this` was in
10975    /// neither arm, so the two shapes scored differently and `m1` billed
10976    /// a `->` with only one operand around it.
10977    ///
10978    /// Plain C++ that the mozcpp fork parses identically, so one source
10979    /// proves the same thing about both clones.
10980    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
10981    const CPP_THIS_RECEIVER_PARITY: &str = "struct S {
10982         int x;
10983         int m1() { return this->x; }
10984         int m2(S* p) { return p->x; }
10985     };";
10986
10987    /// #1361 fixture holding one representative position per kind of
10988    /// container the pinned `tree-sitter-cpp` can put a `this` in: the
10989    /// `->` receiver, a dereference inside a parenthesised expression, a
10990    /// bare return value, both lambda-capture spellings (`[this]` and
10991    /// `[=, this]`), a `decltype` trailing return type, a constructor
10992    /// body following a member-initialiser list, and a call argument.
10993    ///
10994    /// Representative, not exhaustive — `delete this;`, `throw this;`,
10995    /// `[&, this]`, a default member initialiser and a `this->template
10996    /// f<int>()` all parse cleanly too. Every one of them is the same
10997    /// childless `{this:215}` leaf under an unclassified wrapper, which
10998    /// is precisely why the arm needs no guard and why enumerating them
10999    /// would add rows without adding a distinct path (grammar-dispatch
11000    /// section 11).
11001    ///
11002    /// C++23's explicit object parameter (`int g(this S&& self)`) is
11003    /// deliberately absent: the pinned grammar cannot parse it and emits
11004    /// `type_identifier "this"` plus an `ERROR`, which
11005    /// `for_each_node_with_chain` rejects outright. That spelling is
11006    /// already an operand through `TypeIdentifier`, so the construct is
11007    /// unaffected by this arm either way.
11008    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
11009    const CPP_THIS_POSITIONS: &str = "struct S {
11010         int x;
11011         void g(S*);
11012         int arrow() { return this->x; }
11013         int deref() { return (*this).x; }
11014         S* ret() { return this; }
11015         void cap() { auto l = [this]() { return x; }; }
11016         void cap2() { auto l = [=, this]() { return x; }; }
11017         auto trail() -> decltype(this->x) { return x; }
11018         S(int v) : x(v) { this->x = v; }
11019         void call() { g(this); }
11020     };";
11021
11022    /// Asserts `CPP_THIS_RECEIVER_PARITY` for one C++-family language,
11023    /// through the metrics store *and* the text-keyed `--ops` store
11024    /// (the lesson-4 invariant `n2 == len(dedupe(ops.operands))`).
11025    ///
11026    /// The `--ops` half pins *which text* the new operand is billed
11027    /// under, which the counts alone cannot see: billing the enclosing
11028    /// `field_expression` instead of the `this` leaf would hold `n2` at
11029    /// 6 while the vocabulary silently became `this->x`.
11030    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
11031    #[track_caller]
11032    fn assert_this_receiver_parity<T: crate::MetricSuite>(file: &str, label: &str) {
11033        // Operators, keyed by kind_id except the text-keyed primitives:
11034        // `{` x3 (class body, `m1`, `m2`), `int` x3, `;` x4 (the field,
11035        // the two returns, the struct terminator), `(` x2, `return` x2,
11036        // `->` x2, the one `*` of `S* p`, and `struct` (#1552). #695
11037        // dropped every closing delimiter. n1 = 8, N1 = 18.
11038        //
11039        // Operands, keyed by source text: `S` x2 (the name and the
11040        // parameter type), `x` x3, `m1`, `m2`, `p` x2, and — the fix —
11041        // `this` x1. n2 = 6, N2 = 10. Before #1361: n2 5, N2 9, with
11042        // every operator count identical, which is the whole claim.
11043        let counts = format!("{label}: this->x and p->x are the same shape");
11044        assert_halstead_counts::<T>(CPP_THIS_RECEIVER_PARITY, file, [8, 18, 6, 10], &counts);
11045        assert_ops_operands::<T>(
11046            CPP_THIS_RECEIVER_PARITY,
11047            file,
11048            6,
11049            vec!["S", "x", "m1", "m2", "p", "this"],
11050        );
11051    }
11052
11053    /// Every C++ overloaded-operator name the grammar spells as a token
11054    /// of its own, plus the `co_await` expression and the conversion
11055    /// operator's `operator` keyword (#1296).
11056    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
11057    const CPP_OPERATOR_NAMES: &str =
11058        "struct S { int operator[](int); void operator()(); bool operator co_await(); };
11059long operator\"\"_x(unsigned long long);
11060S::operator bool() const;
11061S s; int a = s[1]; auto t = co_await s;
11062";
11063
11064    /// Asserts `CPP_OPERATOR_NAMES` bills each operator name, keyword and
11065    /// `co_await` as an operator under parser `T`.
11066    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
11067    #[track_caller]
11068    fn assert_cpp_operator_names<T: crate::MetricSuite>(label: &str) {
11069        // Operators (n1 = 17, N1 = 40): `operator` 5 (four
11070        // `operator_name`s and the `operator_cast`); the names `[]` 1,
11071        // `()` 1, `""` 1; `co_await` 2 (the name and the expression);
11072        // the `(` of a parameter list 5; `;` 9; `int` 3; `long` 3;
11073        // `bool` 2; `=` 2; `{` 1; `void` 1; `unsigned` 1; `::` 1; the
11074        // subscript `[` 1; `struct` 1 (#1552). `auto` and `const` are in
11075        // no arm.
11076        //
11077        // Operands (n2 = 6, N2 = 9): `s` 3, `S` 2 (the qualifier in
11078        // `S::operator bool` is a namespace identifier, which #1096
11079        // leaves unbilled), `_x`, `a`, `1`, `t`.
11080        //
11081        // Before #1296: (12, 30, 6, 9) with `struct`, the five operator
11082        // kinds billed nowhere.
11083        assert_halstead_counts::<T>(CPP_OPERATOR_NAMES, "ops.cpp", [17, 40, 6, 9], label);
11084
11085        // `operator[]` / `operator()` are kinds of their own, so they sit
11086        // beside the subscript `[` and the parameter-list `(` as two more
11087        // operators, each rendered as the operator it names so that every
11088        // listed entry is distinct.
11089        let ops = ops_of::<T>(CPP_OPERATOR_NAMES, "ops.cpp");
11090        for (name, entries) in [
11091            ("operator", 1),
11092            ("co_await", 1),
11093            ("\"\"", 1),
11094            ("operator[]", 1),
11095            ("operator()", 1),
11096            ("[]", 1),
11097            ("()", 1),
11098        ] {
11099            assert_eq!(
11100                ops.operators.iter().filter(|o| *o == name).count(),
11101                entries,
11102                "{label}: `{name}` must be {entries} operator entry; operators were {:?}",
11103                ops.operators
11104            );
11105            assert!(
11106                !ops.operands.iter().any(|o| o == name),
11107                "{label}: `{name}` must not be an operand; operands were {:?}",
11108                ops.operands
11109            );
11110        }
11111    }
11112
11113    /// Regression for #1296: a C++ operator name is a Halstead operator,
11114    /// as `operator +`'s `+` already was. One row per clone, so reverting
11115    /// either arm fails its own row (grammar-dispatch section 11); Mozcpp
11116    /// owns no file extension, so this row is its only coverage.
11117    #[cfg(all(feature = "cpp", feature = "mozcpp"))]
11118    #[test]
11119    fn cpp_operator_names_are_operators() {
11120        assert_cpp_operator_names::<CppParser>("cpp");
11121        assert_cpp_operator_names::<MozcppParser>("mozcpp");
11122    }
11123
11124    /// Asserts a coroutine's `co_yield` and `co_return` keywords bill as
11125    /// operators under parser `T`, as `return` does (#1547).
11126    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
11127    #[track_caller]
11128    fn assert_cpp_coroutine_keywords<T: crate::MetricSuite>(label: &str) {
11129        let source = "T gen() { co_yield 1; co_yield 2; co_return 3; }";
11130        // Operators (n1 = 5, N1 = 8): `()` 1, `{}` 1, `co_yield` 2,
11131        // `co_return` 1, `;` 3. Operands (n2 = 5, N2 = 5): `T`, `gen`,
11132        // `1`, `2`, `3`. Before #1547: (3, 5, 5, 5), both keywords
11133        // billed nowhere.
11134        assert_halstead_counts::<T>(source, "foo.cpp", [5, 8, 5, 5], label);
11135        let ops = ops_of::<T>(source, "foo.cpp");
11136        for keyword in ["co_yield", "co_return"] {
11137            assert!(
11138                ops.operators.iter().any(|o| o == keyword),
11139                "{label}: `{keyword}` must be an operator; operators were {:?}",
11140                ops.operators
11141            );
11142        }
11143    }
11144
11145    /// Regression for #1546: a zero-width node is billed in neither
11146    /// Halstead half. Error recovery inserts one for a token the source
11147    /// lacks, usually flagged MISSING, and every getter that classifies
11148    /// the kind billed it — an identifier as the empty-string operand.
11149    /// One fixture per language, each a parse on which recovery inserts
11150    /// an operand-classified zero-width node; before the fix every row
11151    /// billed `""`. C#'s and Elixir's are not flagged MISSING, which is
11152    /// why the guard keys on width: an `is_missing()` guard leaves those
11153    /// two rows billing `""`. Each row names its zero-width operand's
11154    /// kind and flag, so the two unflagged rows keep discriminating. The
11155    /// rows pin no counts, since the recovery trees are the grammars' to
11156    /// change (grammar-dispatch §6); the operator half, an inserted `;` or
11157    /// `)`, is anchored by `cpp_missing_tokens_in_valid_code_are_not_billed`.
11158    #[cfg(any(
11159        feature = "bash",
11160        feature = "c",
11161        feature = "cpp",
11162        feature = "csharp",
11163        feature = "elixir",
11164        feature = "go",
11165        feature = "groovy",
11166        feature = "irules",
11167        feature = "java",
11168        feature = "javascript",
11169        feature = "kotlin",
11170        feature = "lua",
11171        feature = "mozcpp",
11172        feature = "mozjs",
11173        feature = "objc",
11174        feature = "perl",
11175        feature = "php",
11176        feature = "python",
11177        feature = "ruby",
11178        feature = "rust",
11179        feature = "tcl",
11180        feature = "typescript",
11181    ))]
11182    #[test]
11183    fn zero_width_nodes_bill_neither_halstead_half() {
11184        // `(language, file, source, kind, is_missing)`: the last two name
11185        // the zero-width operand each fixture's recovery inserts, so a
11186        // grammar bump that recovers some other way fails the row rather
11187        // than passing it on an unrelated zero-width node.
11188        let cases: &[(LANG, &str, &str, &str, bool)] = &[
11189            #[cfg(feature = "bash")]
11190            (LANG::Bash, "foo.sh", "echo a $( )\n", "word", true),
11191            #[cfg(feature = "c")]
11192            (
11193                LANG::C,
11194                "foo.c",
11195                "int x; void () { x = 1; }\n",
11196                "identifier",
11197                true,
11198            ),
11199            #[cfg(feature = "cpp")]
11200            (
11201                LANG::Cpp,
11202                "foo.cpp",
11203                "void f(S *s) {\n  s->~D<F>();\n}\n",
11204                "identifier",
11205                true,
11206            ),
11207            #[cfg(feature = "csharp")]
11208            (
11209                LANG::Csharp,
11210                "foo.cs",
11211                "class A { int = 1; }\n",
11212                "identifier",
11213                false,
11214            ),
11215            #[cfg(feature = "elixir")]
11216            (LANG::Elixir, "foo.ex", "x = a.\n", "identifier", false),
11217            #[cfg(feature = "go")]
11218            (
11219                LANG::Go,
11220                "foo.go",
11221                "package m\nfunc f() { a.() }\n",
11222                "type_identifier",
11223                true,
11224            ),
11225            #[cfg(feature = "groovy")]
11226            (
11227                LANG::Groovy,
11228                "foo.groovy",
11229                "def x = a.\n",
11230                "identifier",
11231                true,
11232            ),
11233            #[cfg(feature = "irules")]
11234            (
11235                LANG::Irules,
11236                "foo.irule",
11237                "when HTTP_REQUEST { set x [expr {1 + }] }\n",
11238                "number",
11239                true,
11240            ),
11241            #[cfg(feature = "java")]
11242            (
11243                LANG::Java,
11244                "Foo.java",
11245                "class A { int = 1; }\n",
11246                "identifier",
11247                true,
11248            ),
11249            #[cfg(feature = "javascript")]
11250            (
11251                LANG::Javascript,
11252                "foo.js",
11253                "let {a: } = b;\n",
11254                "identifier",
11255                true,
11256            ),
11257            #[cfg(feature = "kotlin")]
11258            (
11259                LANG::Kotlin,
11260                "foo.kt",
11261                "fun f(: Int) {}\n",
11262                "identifier",
11263                true,
11264            ),
11265            #[cfg(feature = "lua")]
11266            (LANG::Lua, "foo.lua", "x = a.\n", "identifier", true),
11267            #[cfg(feature = "mozcpp")]
11268            (
11269                LANG::Mozcpp,
11270                "foo.cpp",
11271                "void f(S *s) {\n  s->~D<F>();\n}\n",
11272                "identifier",
11273                true,
11274            ),
11275            #[cfg(feature = "mozjs")]
11276            (
11277                LANG::Mozjs,
11278                "foo.jsm",
11279                "let {a: } = b;\n",
11280                "identifier",
11281                true,
11282            ),
11283            #[cfg(feature = "objc")]
11284            (
11285                LANG::Objc,
11286                "foo.m",
11287                "@interface A\n- (void)f:;\n@end\n",
11288                "identifier",
11289                true,
11290            ),
11291            #[cfg(feature = "perl")]
11292            (LANG::Perl, "foo.pl", "sub { $x = ; }\n", "identifier", true),
11293            #[cfg(feature = "php")]
11294            (LANG::Php, "foo.php", "<?php $x = $a->;\n", "name", true),
11295            #[cfg(feature = "python")]
11296            (
11297                LANG::Python,
11298                "foo.py",
11299                "for in x:\n  pass\n",
11300                "identifier",
11301                true,
11302            ),
11303            #[cfg(feature = "ruby")]
11304            (LANG::Ruby, "foo.rb", "x = a.\n", "identifier", true),
11305            #[cfg(feature = "rust")]
11306            (
11307                LANG::Rust,
11308                "foo.rs",
11309                "fn f() { let x = a.; }\n",
11310                "field_identifier",
11311                true,
11312            ),
11313            #[cfg(feature = "tcl")]
11314            (LANG::Tcl, "foo.tcl", "puts [expr {1 + }]\n", "number", true),
11315            #[cfg(feature = "typescript")]
11316            (
11317                LANG::Typescript,
11318                "foo.ts",
11319                "let {a: } = b;\n",
11320                "identifier",
11321                true,
11322            ),
11323            #[cfg(feature = "typescript")]
11324            (LANG::Tsx, "foo.tsx", "let {a: } = b;\n", "identifier", true),
11325        ];
11326        crate::test_support::assert_fixtures_present(cases);
11327
11328        let mut billed = Vec::new();
11329        for (lang, file, source, kind, missing) in cases {
11330            let ast = crate::test_support::parse_named(*lang, file, source);
11331            assert!(
11332                ast.root_node().preorder().any(|node| {
11333                    node.start_byte() == node.end_byte()
11334                        && node.kind() == *kind
11335                        && node.is_missing() == *missing
11336                }),
11337                "{lang:?}: `{source}` no longer recovers with a zero-width `{kind}` \
11338                 (is_missing: {missing}); find another fixture"
11339            );
11340            let ops = ast.ops().expect("ops walk must yield a top-level Ops");
11341            assert!(
11342                !ops.operands.is_empty(),
11343                "{lang:?}: `{source}` billed no operand at all"
11344            );
11345            billed.extend(ops.operands.iter().any(String::is_empty).then_some(*lang));
11346        }
11347        assert!(
11348            billed.is_empty(),
11349            "a zero-width node was billed as the empty operand in {billed:?}"
11350        );
11351    }
11352
11353    /// Asserts two valid C++ programs that tree-sitter-cpp 0.23.4 can only
11354    /// parse by inserting MISSING tokens bill only the tokens written
11355    /// (#1546), under parser `T`. Both come from the DeepSpeech corpus.
11356    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
11357    #[track_caller]
11358    fn assert_cpp_missing_tokens_unbilled<T: crate::MetricSuite>(label: &str) {
11359        // `openfst/dfs-visit.h:75`: an explicit destructor call through a
11360        // template name. Recovery reads `s->~D<F` as comparisons and
11361        // inserts a MISSING identifier inside the trailing `()`.
11362        // Operators (n1 = 9, N1 = 10): `void`, `()` 2, `*`, `{}`, `->`,
11363        // `~`, `<`, `>`, `;`. Operands (n2 = 5, N2 = 6): `f`, `S`, `s` 2,
11364        // `D`, `F`. Before #1546: (9, 10, 6, 7), the extra being `""`.
11365        let destructor = "void f(S *s) {\n  s->~D<F>();\n}\n";
11366        assert_halstead_counts::<T>(destructor, "foo.cpp", [9, 10, 5, 6], label);
11367        let ops = ops_of::<T>(destructor, "foo.cpp");
11368        assert_eq!(ops.operands, ["D", "F", "S", "f", "s"], "{label}");
11369
11370        // `kenlm/lm/search_trie.cc:286`: a `typename` functional cast
11371        // used as a statement. Recovery inserts a MISSING `)` after
11372        // `quant_` and a MISSING `;` after `order`. Operators (n1 = 9,
11373        // N1 = 11): `void`, `()` 3, `{}`, `::`, `,`, `-`, `.`, the one
11374        // written `;`, and `typename` (#1557). Operands (n2 = 7, N2 = 7):
11375        // `f`, `MiddlePointer`, `quant_`, `order`, `2`, `Write`, `w`.
11376        // Before #1546 the MISSING `;` was a second `;`.
11377        let cast = "void f() {\n  typename Quant::MiddlePointer(quant_, order - 2).Write(w);\n}\n";
11378        assert_halstead_counts::<T>(cast, "foo.cpp", [9, 11, 7, 7], label);
11379    }
11380
11381    /// Regression for #1546 on the two C++ clones. Mozcpp owns no file
11382    /// extension, so this row is its only coverage.
11383    #[cfg(all(feature = "cpp", feature = "mozcpp"))]
11384    #[test]
11385    fn cpp_missing_tokens_in_valid_code_are_not_billed() {
11386        assert_cpp_missing_tokens_unbilled::<CppParser>("cpp");
11387        assert_cpp_missing_tokens_unbilled::<MozcppParser>("mozcpp");
11388    }
11389
11390    /// Regression for #1547: `co_return` and `co_yield` are Halstead
11391    /// operators. One row per clone, so reverting either arm fails its
11392    /// own row; Mozcpp owns no file extension, so this row is its only
11393    /// coverage.
11394    #[cfg(all(feature = "cpp", feature = "mozcpp"))]
11395    #[test]
11396    fn cpp_coroutine_keywords_are_operators() {
11397        assert_cpp_coroutine_keywords::<CppParser>("cpp");
11398        assert_cpp_coroutine_keywords::<MozcppParser>("mozcpp");
11399    }
11400
11401    /// Regression for #1361: a C++ `this` is a Halstead operand.
11402    ///
11403    /// `Cpp::This` / `Mozcpp::This` were in neither arm of
11404    /// `CppCode::get_op_type` / `MozcppCode::get_op_type`, so a `this`
11405    /// contributed *nothing* — not an operator and not an operand —
11406    /// while eleven of the fourteen languages here bill their
11407    /// self-reference as an operand. Same shape as #1316's character
11408    /// literals, and the inverse of the #1351-#1355 over-counts.
11409    ///
11410    /// Each language asserts separately over the same source rather than
11411    /// sharing one call, so reverting one clone's arm fails that row
11412    /// alone (grammar-dispatch section 11). Mozcpp owns no file
11413    /// extension, so no integration snapshot reaches its clone; this row
11414    /// and `cpp_and_mozcpp_agree_on_this` in `tests/parity/` are the
11415    /// whole coverage that arm has.
11416    #[cfg(all(feature = "cpp", feature = "mozcpp"))]
11417    #[test]
11418    fn cpp_this_is_an_operand() {
11419        assert_this_receiver_parity::<CppParser>("this.cpp", "cpp");
11420        assert_this_receiver_parity::<MozcppParser>("this.cpp", "mozcpp");
11421    }
11422
11423    /// The `This` arm is unconditional: every `this` in
11424    /// `CPP_THIS_POSITIONS` classifies as an operand whatever encloses
11425    /// it (#1361).
11426    ///
11427    /// The counts in `cpp_this_is_an_operand` are measured on one
11428    /// position only. An arm reached through a parent-scoped guard —
11429    /// the shape the neighbouring `RawStringLiteral` `LPAREN` arm uses,
11430    /// and the shape a reviewer might reasonably add here — would pass
11431    /// that test and still drop `this` in a lambda capture or a
11432    /// `decltype`. This walk is what makes the arm's unconditional
11433    /// reach a measurement rather than an assumption.
11434    ///
11435    /// Named for the *property* rather than for the fixture: the fixture
11436    /// samples one position per container kind and is deliberately not
11437    /// exhaustive (see `CPP_THIS_POSITIONS`), so a name promising "every
11438    /// position" would claim more than it checks.
11439    #[cfg(all(feature = "cpp", feature = "mozcpp"))]
11440    #[test]
11441    fn cpp_this_is_an_operand_regardless_of_position() {
11442        #[cfg(any(feature = "cpp", feature = "mozcpp"))]
11443        fn check<L: LanguageInfo + Getter>(label: &str) {
11444            let mut seen = 0_usize;
11445            for_each_node_with_chain::<L>(CPP_THIS_POSITIONS.as_bytes(), |node, chain| {
11446                if node.kind() != "this" {
11447                    return;
11448                }
11449                seen += 1;
11450                // `_with_code` is the spelling `compute_halstead` calls.
11451                // The default forwards to the byte-less form, so the two
11452                // agree for both languages today — which is exactly why
11453                // asking the wrong one would read as correct right up
11454                // until one of them grew an override
11455                // (grammar-dispatch section 7).
11456                assert!(
11457                    matches!(
11458                        L::get_op_type_with_code(
11459                            node,
11460                            CPP_THIS_POSITIONS.as_bytes(),
11461                            Ancestors::known(chain)
11462                        ),
11463                        TokenRole::Operand
11464                    ),
11465                    "{label}: a `this` under `{}` is not an operand",
11466                    chain.last().map_or("<root>", Node::kind)
11467                );
11468            });
11469            // One per position: `->` receiver, `(*this)`, bare return,
11470            // `[this]`, `[=, this]`, `decltype(this->x)`, the
11471            // constructor body, and the call argument.
11472            assert_eq!(seen, 8, "{label}: fixture lost a `this` position");
11473        }
11474
11475        check::<CppCode>("cpp");
11476        check::<MozcppCode>("mozcpp");
11477    }
11478
11479    /// Pins the two grammar facts the #1361 operand arm rests on, across
11480    /// both fixtures and both languages.
11481    ///
11482    /// * **Grammar-dispatch section 1.** Every node the grammar spells
11483    ///   `this` carries the one `kind_id` the arm lists. That is the
11484    ///   weaker half of the alias evidence — an alias arises in a
11485    ///   *different* syntactic position, which no fixture can enumerate.
11486    ///   The strong half is that these generated enums carry
11487    ///   numeric-suffix aliases in quantity (`FunctionDefinition2..4`,
11488    ///   `QualifiedIdentifier2..4`, `LPAREN2`, `Try2`, `GT2` in this one
11489    ///   file) and neither spells a `This2` — `This = 215` is the sole
11490    ///   variant rendering `"this"` in `language_cpp.rs` and
11491    ///   `language_mozcpp.rs`. This loop is what notices if a grammar
11492    ///   bump changes that under an existing fixture.
11493    /// * **Grammar-dispatch section 5.** A `this` is a childless leaf and
11494    ///   no node containing it is classified. That is both halves of the
11495    ///   double-count question: nothing below it can be billed a second
11496    ///   time, and no `field_expression` / `pointer_expression` /
11497    ///   `lambda_capture_specifier` / `argument_list` wrapper bills the
11498    ///   same source text from above.
11499    #[cfg(all(feature = "cpp", feature = "mozcpp"))]
11500    #[test]
11501    fn cpp_this_is_a_childless_unaliased_leaf() {
11502        #[cfg(any(feature = "cpp", feature = "mozcpp"))]
11503        fn check<L: LanguageInfo + Getter>(this: u16, label: &str) {
11504            let mut seen = 0_usize;
11505            for source in [CPP_THIS_RECEIVER_PARITY, CPP_THIS_POSITIONS] {
11506                for_each_node_with_chain::<L>(source.as_bytes(), |node, chain| {
11507                    if node.kind() != "this" {
11508                        return;
11509                    }
11510                    seen += 1;
11511                    assert_eq!(
11512                        node.kind_id(),
11513                        this,
11514                        "{label}: a `this` carries kind_id {} rather than the {this} the \
11515                         operand arm lists — an alias the arm cannot see",
11516                        node.kind_id()
11517                    );
11518                    assert_eq!(
11519                        node.child_count(),
11520                        0,
11521                        "{label}: `this` grew children, which the operand arm would now \
11522                         double-count against"
11523                    );
11524                    assert!(!chain.is_empty(), "{label}: a `this` cannot be the root");
11525                    // Every ancestor, not just the parent. The claim is
11526                    // that *no node containing* a `this` is classified,
11527                    // and `(*this)` alone puts two wrappers between the
11528                    // leaf and the nearest classified node — checking
11529                    // only `chain.last()` would assert something
11530                    // narrower than the comment on the arm promises.
11531                    // `chain` is the *node's* ancestry, so the ancestor
11532                    // at `i` has `chain[..i]` for its own.
11533                    for (i, ancestor) in chain.iter().enumerate() {
11534                        assert!(
11535                            matches!(
11536                                L::get_op_type_with_code(
11537                                    ancestor,
11538                                    source.as_bytes(),
11539                                    Ancestors::known(&chain[..i])
11540                                ),
11541                                TokenRole::Unknown
11542                            ),
11543                            "{label}: `{}` contains a `this` and is itself classified, so the \
11544                             reference now counts twice",
11545                            ancestor.kind()
11546                        );
11547                    }
11548                });
11549            }
11550            // One `this` in the parity fixture, eight in the positions
11551            // fixture. Asserted so a fixture that stopped containing one
11552            // cannot make this test pass having checked nothing.
11553            assert_eq!(seen, 9, "{label}: fixtures lost a `this`");
11554        }
11555
11556        check::<CppCode>(Cpp::This as u16, "cpp");
11557        check::<MozcppCode>(Mozcpp::This as u16, "mozcpp");
11558    }
11559
11560    /// Builds a `HalsteadMaps` from explicit occurrence counts.
11561    ///
11562    /// The per-language tests above reach these maps only through a
11563    /// parse, which cannot produce a *chosen* overlap between a child
11564    /// and its parent — the cases `merge` exists to get right.
11565    fn halstead_maps_of<'a>(
11566        operators: &[(u16, u64)],
11567        primitive_operators: &[(&'a [u8], u64)],
11568        operands: &[(&'a [u8], u64)],
11569    ) -> HalsteadMaps<'a> {
11570        HalsteadMaps {
11571            operators: operators.iter().copied().collect(),
11572            primitive_operators: primitive_operators.iter().copied().collect(),
11573            operands: operands.iter().copied().collect(),
11574        }
11575    }
11576
11577    /// `HalsteadMaps::operators` must stay on the crate's integer hasher.
11578    ///
11579    /// Swapping a hasher moves no metric value, so every other test in
11580    /// this file passes just as well with #1108 reverted. Both halves
11581    /// here are needed: the typed binding stops compiling if the field
11582    /// goes back to a default-hasher `HashMap`, and the `type_name`
11583    /// comparison still fails at runtime if `IntKeyHashMap` itself is
11584    /// ever redefined to wrap `RandomState`.
11585    ///
11586    /// The two text-keyed maps are pinned to SipHash in the same test,
11587    /// because moving *them* would be a regression rather than an
11588    /// optimisation. `crate::int_hash`'s module doc is the single place
11589    /// that argues why analysed source text does not qualify.
11590    #[test]
11591    fn halstead_operator_map_uses_the_int_key_hasher() {
11592        use std::any::{type_name, type_name_of_val};
11593        use std::hash::BuildHasherDefault;
11594
11595        use crate::int_hash::IntKeyHasher;
11596
11597        let maps = HalsteadMaps::new();
11598
11599        let operators: &IntKeyHashMap<u16, u64> = &maps.operators;
11600        assert_eq!(
11601            type_name_of_val(operators.hasher()),
11602            type_name::<BuildHasherDefault<IntKeyHasher>>(),
11603            "the kind_id-keyed operator map must use the int_hash hasher"
11604        );
11605
11606        let siphash = type_name::<std::collections::hash_map::RandomState>();
11607        assert_eq!(
11608            type_name_of_val(maps.operands.hasher()),
11609            siphash,
11610            "operand keys come from the analysed source, so the keyed hash \
11611             is what stops a crafted file from flooding this map"
11612        );
11613        assert_eq!(
11614            type_name_of_val(maps.primitive_operators.hasher()),
11615            siphash,
11616            "primitive-operator keys come from the analysed source, so the \
11617             keyed hash is what stops a crafted file from flooding this map"
11618        );
11619    }
11620
11621    /// `merge` sums overlapping keys and adopts disjoint ones, in all
11622    /// three maps, and `finalize` reads the union back as n1/N1/n2/N2.
11623    ///
11624    /// Every count differs from every other and none is zero, so a
11625    /// dropped key, an overwrite where an addition belongs, or a map
11626    /// crossed with its neighbour all change the totals.
11627    #[test]
11628    fn halstead_maps_merge_sums_overlaps_and_adopts_disjoint_keys() {
11629        let mut parent = halstead_maps_of(
11630            &[(1, 2), (2, 3)],
11631            &[(b"int", 1)],
11632            &[(b"alpha", 4), (b"beta", 7)],
11633        );
11634        let child = halstead_maps_of(
11635            &[(2, 5), (7, 11)],
11636            &[(b"double", 13)],
11637            &[(b"alpha", 17), (b"gamma", 19)],
11638        );
11639
11640        parent.merge(&child);
11641
11642        // expected: operators {1: 2, 2: 3+5, 7: 11}; primitives
11643        // {int: 1, double: 13}; operands {alpha: 4+17, beta: 7,
11644        // gamma: 19}.
11645        assert_eq!(
11646            parent,
11647            halstead_maps_of(
11648                &[(1, 2), (2, 8), (7, 11)],
11649                &[(b"int", 1), (b"double", 13)],
11650                &[(b"alpha", 21), (b"beta", 7), (b"gamma", 19)],
11651            )
11652        );
11653
11654        let mut stats = Stats::default();
11655        parent.finalize(&mut stats);
11656        // expected: n1 = 3 kind ids + 2 primitives; N1 = (2+8+11) +
11657        // (1+13); n2 = 3 texts; N2 = 21+7+19.
11658        assert_eq!(stats.unique_operators(), 5);
11659        assert_eq!(stats.total_operators(), 35);
11660        assert_eq!(stats.unique_operands(), 3);
11661        assert_eq!(stats.total_operands(), 47);
11662    }
11663
11664    /// Merging an empty child leaves the parent untouched.
11665    ///
11666    /// A space with no operators or operands is the common case for a
11667    /// leaf getter or an empty function body, and `finalize` runs on
11668    /// the parent afterwards either way.
11669    #[test]
11670    fn halstead_maps_merge_of_empty_child_is_a_no_op() {
11671        let mut parent = halstead_maps_of(&[(3, 5)], &[(b"char", 2)], &[(b"delta", 9)]);
11672        let before = parent.clone();
11673
11674        parent.merge(&HalsteadMaps::new());
11675
11676        assert_eq!(parent, before);
11677
11678        let mut stats = Stats::default();
11679        parent.finalize(&mut stats);
11680        // expected: n1 = 1 kind id + 1 primitive; N1 = 5 + 2; n2 = 1;
11681        // N2 = 9.
11682        assert_eq!(stats.unique_operators(), 2);
11683        assert_eq!(stats.total_operators(), 7);
11684        assert_eq!(stats.unique_operands(), 1);
11685        assert_eq!(stats.total_operands(), 9);
11686    }
11687
11688    /// Folding a chain of nested spaces bottom-up must reach the union
11689    /// of every level, re-merging already-merged maps on the way up.
11690    ///
11691    /// This is what `spaces.rs` and `ops.rs` actually do: each space is
11692    /// merged into its parent as the walk pops it, so by the time the
11693    /// root sees a grandchild's counts they have already passed through
11694    /// one `merge`. The literal expectation below is what discriminates
11695    /// — the `nested == flat` cross-check on its own does not, because
11696    /// any entry-wise fold over the same levels agrees with itself
11697    /// however it is associated, including a broken one.
11698    #[test]
11699    fn halstead_maps_merge_folds_a_nested_chain() {
11700        let levels = [
11701            halstead_maps_of(&[(1, 1)], &[(b"int", 1)], &[(b"a", 1)]),
11702            halstead_maps_of(&[(1, 2), (2, 3)], &[], &[(b"a", 2), (b"b", 4)]),
11703            halstead_maps_of(&[(2, 5)], &[(b"long", 6)], &[(b"b", 7)]),
11704            halstead_maps_of(&[(3, 8)], &[(b"int", 9)], &[(b"c", 10)]),
11705        ];
11706
11707        // Bottom-up: the deepest level folds into its parent, that
11708        // result into *its* parent, and so on up to the root.
11709        let mut nested = levels[levels.len() - 1].clone();
11710        for level in levels.iter().rev().skip(1) {
11711            let mut outer = level.clone();
11712            outer.merge(&nested);
11713            nested = outer;
11714        }
11715
11716        // Flat: every level merged directly into the root.
11717        let mut flat = levels[0].clone();
11718        for level in &levels[1..] {
11719            flat.merge(level);
11720        }
11721
11722        // expected: every key summed across the four levels — operators
11723        // {1: 1+2, 2: 3+5, 3: 8}, primitives {int: 1+9, long: 6},
11724        // operands {a: 1+2, b: 4+7, c: 10}.
11725        assert_eq!(
11726            nested,
11727            halstead_maps_of(
11728                &[(1, 3), (2, 8), (3, 8)],
11729                &[(b"int", 10), (b"long", 6)],
11730                &[(b"a", 3), (b"b", 11), (b"c", 10)],
11731            )
11732        );
11733        assert_eq!(nested, flat);
11734
11735        let mut stats = Stats::default();
11736        nested.finalize(&mut stats);
11737        // expected: n1 = 3 kind ids + 2 primitives; N1 = (3+8+8) +
11738        // (10+6); n2 = 3 texts; N2 = 3+11+10.
11739        assert_eq!(stats.unique_operators(), 5);
11740        assert_eq!(stats.total_operators(), 35);
11741        assert_eq!(stats.unique_operands(), 3);
11742        assert_eq!(stats.total_operands(), 24);
11743    }
11744
11745    /// A `kind_id` at the top of the `u16` range must behave like any
11746    /// other key.
11747    ///
11748    /// The largest grammar in the workspace (`mozcpp`) tops out around
11749    /// 640 symbols, so nothing near `u16::MAX` occurs today — but the
11750    /// map is keyed by the raw id, and a dense-array representation
11751    /// (the shape #1108 considered and rejected) is exactly what such a
11752    /// key would break. Pinning it keeps that trade-off honest if the
11753    /// representation is ever revisited.
11754    #[test]
11755    fn halstead_maps_handle_the_full_kind_id_range() {
11756        let mut parent = halstead_maps_of(&[(0, 3), (u16::MAX, 5)], &[], &[]);
11757        parent.merge(&halstead_maps_of(&[(u16::MAX, 7)], &[], &[]));
11758
11759        let mut stats = Stats::default();
11760        parent.finalize(&mut stats);
11761        // expected: two distinct kind ids, occurrences 3 and 5+7.
11762        assert_eq!(stats.unique_operators(), 2);
11763        assert_eq!(stats.total_operators(), 15);
11764    }
11765}