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 = "cpp",
606        feature = "csharp",
607        feature = "groovy",
608        feature = "java",
609        feature = "kotlin",
610        feature = "mozcpp",
611        feature = "objc",
612        feature = "typescript"
613    ))]
614    #[track_caller]
615    fn assert_keywords_are_operands_only<T: crate::MetricSuite>(
616        source: &str,
617        file: &str,
618        keywords: &[&str],
619    ) {
620        let ops = ops_of::<T>(source, file);
621
622        for keyword in keywords {
623            assert!(
624                ops.operands.iter().any(|o| o == keyword),
625                "{file}: `{keyword}` must be an operand; operands were {:?}",
626                ops.operands
627            );
628            assert!(
629                !ops.operators.iter().any(|o| o == keyword),
630                "{file}: `{keyword}` must not be an operator; operators were {:?}",
631                ops.operators
632            );
633        }
634    }
635
636    /// How one space's vocabularies classify a single keyword.
637    #[derive(Debug, PartialEq, Eq)]
638    enum Role {
639        OperatorOnly,
640        OperandOnly,
641        Both,
642    }
643
644    /// Records `keyword`'s role in `ops` and in every space below it.
645    ///
646    /// A space that bills the keyword in neither vocabulary contributes
647    /// no row, so the result is the set of places the keyword actually
648    /// reaches — which is what distinguishes a parent gate from its own
649    /// inverse, and what the merged root vocabulary cannot show.
650    #[cfg(feature = "csharp")]
651    fn collect_keyword_roles(ops: &crate::ops::Ops, keyword: &str, out: &mut Vec<Role>) {
652        let operator = ops.operators.iter().any(|o| o == keyword);
653        let operand = ops.operands.iter().any(|o| o == keyword);
654        match (operator, operand) {
655            (true, true) => out.push(Role::Both),
656            (true, false) => out.push(Role::OperatorOnly),
657            (false, true) => out.push(Role::OperandOnly),
658            (false, false) => {}
659        }
660        for space in &ops.spaces {
661            collect_keyword_roles(space, keyword, out);
662        }
663    }
664
665    /// Pins the three grammar facts every #1380 operand arm rests on,
666    /// over a fixture that samples one self- or super-reference per
667    /// syntactic position the language allows.
668    ///
669    /// Each `keywords` row is `(spelling, kind_id, occurrences)`.
670    ///
671    /// * **Grammar-dispatch section 1.** Every node the grammar spells
672    ///   `this` / `super` / `base` / `this@` / `super@` carries the one
673    ///   `kind_id` its arm lists. An alias arises in a *different*
674    ///   syntactic position, which no fixture can enumerate, so this is
675    ///   the weaker half of the alias evidence — the strong half is that
676    ///   these generated enums carry numeric-suffix aliases in quantity
677    ///   and none spells a `This2`. What this loop catches is a grammar
678    ///   bump introducing one under an existing fixture.
679    /// * **Grammar-dispatch section 5.** Each is a childless leaf and no
680    ///   node *containing* it is classified, so the reference cannot be
681    ///   billed twice. Every ancestor is checked, not just the parent:
682    ///   Kotlin puts `this_expression` *and* `navigation_expression`
683    ///   between the leaf and the nearest classified node, and the claim
684    ///   the arms make is about containment, not parentage.
685    /// * **Grammar-dispatch section 11.** The arm is unconditional. A
686    ///   parent-scoped guard added later — the shape the two deliberate
687    ///   carve-outs use, and so the shape a reviewer might reasonably
688    ///   add — would satisfy the single-position count tests and still
689    ///   drop `this` in a method reference or a constructor delegation.
690    ///
691    /// The fixtures deliberately exclude the two gated positions (C#'s
692    /// indexer declarator, Java's wildcard bound); those are operators,
693    /// and their own tests pin them.
694    #[cfg(any(feature = "csharp", feature = "java", feature = "kotlin"))]
695    #[track_caller]
696    fn assert_self_reference_leaves<L: LanguageInfo + Getter>(
697        source: &str,
698        keywords: &[(&str, u16, usize)],
699        label: &str,
700    ) {
701        let code = source.as_bytes();
702        let mut seen = vec![0_usize; keywords.len()];
703
704        for_each_node_with_chain::<L>(code, |node, chain| {
705            let Some(index) = keywords.iter().position(|(kw, _, _)| *kw == node.kind()) else {
706                return;
707            };
708            let (keyword, kind_id, _) = keywords[index];
709            seen[index] += 1;
710
711            assert_eq!(
712                node.kind_id(),
713                kind_id,
714                "{label}: a `{keyword}` carries kind_id {} rather than the {kind_id} its arm \
715                 lists — an alias the arm cannot see",
716                node.kind_id()
717            );
718            assert_eq!(
719                node.child_count(),
720                0,
721                "{label}: `{keyword}` grew children, which the operand arm would now \
722                 double-count against"
723            );
724            for (depth, ancestor) in chain.iter().enumerate() {
725                assert!(
726                    matches!(
727                        L::get_op_type_with_code(ancestor, code, Ancestors::known(&chain[..depth])),
728                        TokenRole::Unknown
729                    ),
730                    "{label}: `{}` contains a `{keyword}` and is itself classified, so the \
731                     reference now counts twice",
732                    ancestor.kind()
733                );
734            }
735            // `_with_code` is the spelling `compute_halstead` calls. The
736            // default forwards to the byte-less form, so asking the wrong
737            // one reads as correct right up until a language grows an
738            // override (grammar-dispatch section 7).
739            assert!(
740                matches!(
741                    L::get_op_type_with_code(node, code, Ancestors::known(chain)),
742                    TokenRole::Operand
743                ),
744                "{label}: a `{keyword}` under `{}` is not an operand",
745                chain.last().map_or("<root>", Node::kind)
746            );
747        });
748
749        for ((keyword, _, expected), got) in keywords.iter().zip(&seen) {
750            assert_eq!(
751                got, expected,
752                "{label}: fixture holds {got} `{keyword}` rather than {expected}"
753            );
754        }
755    }
756
757    /// Asserts the root space's `[n1, N1, n2, N2]`, naming `label` when
758    /// it does not hold.
759    ///
760    /// The delimiter-invariance tests (#1256 Elixir, #1312 Ruby and
761    /// Perl) each loop over spellings of one literal and need the
762    /// spelling in the failure message; `check_metrics` expands to a
763    /// plain `fn` that cannot capture a loop variable, so they reach
764    /// for the closure-taking helper it wraps. Three copies of that
765    /// dance is two too many.
766    #[cfg(any(
767        feature = "bash",
768        feature = "c",
769        feature = "cpp",
770        feature = "csharp",
771        feature = "elixir",
772        feature = "groovy",
773        feature = "irules",
774        feature = "java",
775        feature = "javascript",
776        feature = "kotlin",
777        feature = "mozcpp",
778        feature = "mozjs",
779        feature = "objc",
780        feature = "perl",
781        feature = "php",
782        feature = "ruby",
783        feature = "tcl",
784        feature = "typescript",
785    ))]
786    fn assert_halstead_counts<T: crate::MetricSuite>(
787        source: &str,
788        file: &str,
789        expected: [u64; 4],
790        label: &str,
791    ) {
792        crate::test_support::check_func_space_only::<T, _>(
793            source,
794            file,
795            &[crate::Metric::Halstead],
796            |space| {
797                let halstead = &space.metrics.halstead;
798                assert_eq!(
799                    [
800                        halstead.unique_operators(),
801                        halstead.total_operators(),
802                        halstead.unique_operands(),
803                        halstead.total_operands(),
804                    ],
805                    expected,
806                    "{label}"
807                );
808            },
809        );
810    }
811
812    #[cfg(feature = "python")]
813    #[test]
814    fn python_operators_and_operands() {
815        check_metrics::<PythonParser>(
816            "def foo():
817                 def bar():
818                     def toto():
819                        a = 1 + 1
820                     b = 2 + a
821                 c = 3 + 3",
822            "foo.py",
823            |metric| {
824                // unique operators: def, (), :, =, +
825                // operators: def, (), : for each of the three headers,
826                //   then =, =, =, +, +, + (the `()` and `:` since #1486)
827                // unique operands: foo, bar, toto, a, b, c, 1, 2, 3
828                // operands: foo, bar, toto, a, b, c, 1, 1, 2, a, 3, 3
829                assert_eq!(metric.halstead.unique_operators(), 5);
830                assert_eq!(metric.halstead.total_operators(), 15);
831                insta::assert_json_snapshot!(
832                    metric.halstead,
833                    @r#"
834                {
835                  "unique_operators": 5,
836                  "total_operators": 15,
837                  "unique_operands": 9,
838                  "total_operands": 12,
839                  "length": 27,
840                  "estimated_program_length": 40.13896548741762,
841                  "purity_ratio": 1.4866283513858378,
842                  "vocabulary": 14,
843                  "volume": 102.79858289555531,
844                  "difficulty": 3.3333333333333335,
845                  "level": 0.3,
846                  "effort": 342.6619429851844,
847                  "time": 19.03677461028802,
848                  "bugs": 0.01632259960095138
849                }
850                "#
851                );
852            },
853        );
854    }
855
856    /// Pointer-arithmetic operators: `*` (dereference), `&` (address-of),
857    /// `->` (member-of-pointer), `+` (pointer + offset). Each is counted
858    /// once in `n1`; multiple uses bump `N1`. The headline integer values
859    /// (`u_operators`, `u_operands`) anchor the snapshot per the
860    /// snapshot-anchor policy.
861    #[cfg(feature = "c")]
862    #[test]
863    fn c_pointer_arithmetic_operators() {
864        check_metrics::<CParser>(
865            "int g(int* p, int* q) {
866                 return *(p + 1) + *q;
867             }",
868            "foo.c",
869            |metric| {
870                // Unique operators: int, *, (), {, }, +, ;, return  (= 8)
871                //   `*` covers both pointer-type and dereference; the grammar
872                //   does NOT split them.  `,` does not appear (only one
873                //   parameter on each side of the body).
874                // Unique operands: g, p, q, 1                       (= 4)
875                assert_eq!(metric.halstead.unique_operators(), 8);
876                assert_eq!(metric.halstead.unique_operands(), 4);
877                insta::assert_json_snapshot!(metric.halstead);
878            },
879        );
880    }
881
882    /// Bitwise (`&`, `|`, `^`, `~`, `<<`, `>>`) and logical (`&&`, `||`,
883    /// `!`) operators are distinct kind_ids and count as separate unique
884    /// operators in Halstead.  `&` (bitwise-and) and `&&` (logical-and)
885    /// must NOT collapse, even though both render as ampersands.
886    #[cfg(feature = "c")]
887    #[test]
888    fn c_bitwise_and_logical_operators() {
889        check_metrics::<CParser>(
890            "int f(int a, int b) {
891                 int x = (a & b) | (a ^ b);
892                 int y = ~a;
893                 int z = (a << 1) >> 2;
894                 return (a && b) || !x;
895             }",
896            "foo.c",
897            |metric| {
898                // Expect: 6 bitwise op kinds (& | ^ ~ << >>), 3 logical (&& || !).
899                // Plus int, (), {, }, =, ;, return, , — 8 syntactic / arithmetic
900                // operator kinds.  Six bitwise + three logical + eight = 17 unique
901                // operators is the upper bound; actuals depend on grammar collapse,
902                // so we assert a lower-bound and anchor via snapshot below.
903                let s = &metric.halstead;
904                assert!(
905                    s.unique_operators() >= 14,
906                    "expected >= 14 unique operators (bitwise + logical + syntax), got {}",
907                    s.unique_operators(),
908                );
909                assert_eq!(s.unique_operands(), 8); // f, a, b, x, y, z, 1, 2
910                insta::assert_json_snapshot!(metric.halstead);
911            },
912        );
913    }
914
915    /// Increment / decrement (`++`, `--`) and `sizeof` / cast operators
916    /// each contribute distinct unique operators.  C-style casts in the
917    /// tree-sitter grammar surface as `cast_expression` with the type
918    /// token classified as a primitive_type operator.
919    #[cfg(feature = "c")]
920    #[test]
921    fn c_increment_decrement_and_sizeof() {
922        check_metrics::<CParser>(
923            "void f(int* p) {
924                 int n = sizeof(int);
925                 ++p;
926                 --n;
927                 long w = (long) n;
928             }",
929            "foo.c",
930            |metric| {
931                // Unique operators include: void, int, long, *, =, sizeof, ++, --, (), {, }, ;
932                // Unique operands: f, p, n, w
933                let s = &metric.halstead;
934                assert!(
935                    s.unique_operators() >= 10,
936                    "expected >= 10 unique operators including ++ / -- / sizeof / cast, got {}",
937                    s.unique_operators(),
938                );
939                assert_eq!(s.unique_operands(), 4);
940                insta::assert_json_snapshot!(metric.halstead);
941            },
942        );
943    }
944
945    #[cfg(feature = "cpp")]
946    #[test]
947    fn cpp_operators_and_operands() {
948        // Define operators and operands for C/C++ grammar according to this specification:
949        // https://www.verifysoft.com/en_halstead_metrics.html
950        // The only difference with the specification above is that
951        // primitive types are treated as operators, since the definition of a
952        // primitive type can be seen as the creation of a slot of a certain size.
953        // i.e. The `int a;` definition creates a n-bytes slot.
954        check_metrics::<CppParser>(
955            "main()
956            {
957              int a, b, c, avg;
958              scanf(\"%d %d %d\", &a, &b, &c);
959              avg = (a + b + c) / 3;
960              printf(\"avg = %d\", avg);
961            }",
962            "foo.c",
963            |metric| {
964                // unique operators: (), {}, int, &, =, +, /, ,, ;
965                // unique operands: main, a, b, c, avg, scanf, "%d %d %d", 3, printf, "avg = %d"
966                insta::assert_json_snapshot!(
967                    metric.halstead,
968                    @r#"
969                {
970                  "unique_operators": 9,
971                  "total_operators": 24,
972                  "unique_operands": 10,
973                  "total_operands": 18,
974                  "length": 42,
975                  "estimated_program_length": 61.74860596185444,
976                  "purity_ratio": 1.470204903853677,
977                  "vocabulary": 19,
978                  "volume": 178.41295556463058,
979                  "difficulty": 8.1,
980                  "level": 0.1234567901234568,
981                  "effort": 1445.1449400735075,
982                  "time": 80.28583000408375,
983                  "bugs": 0.04260752914034329
984                }
985                "#
986                );
987            },
988        );
989    }
990
991    /// A `sized_type_specifier` carries its `unsigned`/`signed`/`long`/
992    /// `short` modifiers as bare keyword tokens (distinct kind_ids), not
993    /// as `primitive_type` children. Prior to issue #466 those tokens
994    /// fell through to the `Unknown` arm and were dropped from `n1`/`N1`,
995    /// so `unsigned int` collapsed to just `int` and `signed long`
996    /// contributed nothing. They must each count as a distinct operator,
997    /// while `long long`'s two `long` tokens fold to one `n1` entry but
998    /// two `N1` hits. Regression test for issue #466.
999    #[cfg(feature = "cpp")]
1000    #[test]
1001    fn cpp_sized_type_specifier_operators() {
1002        let source = "unsigned int u = 3; signed long b = 4; long long c = 5;";
1003        check_metrics::<CppParser>(source, "foo.cpp", |metric| {
1004            // Distinct operators (n1): unsigned, signed, long, int, =, ; = 6
1005            // Total operators (N1):
1006            //   unsigned(1) + int(1) + =(3) + ;(3) + signed(1) + long(3) = 12
1007            //   (`long` appears once in `signed long` and twice in `long long`)
1008            // Distinct/total operands: u, b, c, 3, 4, 5 = 6 / 6
1009            assert_eq!(metric.halstead.unique_operators(), 6);
1010            assert_eq!(metric.halstead.total_operators(), 12);
1011            assert_eq!(metric.halstead.unique_operands(), 6);
1012            assert_eq!(metric.halstead.total_operands(), 6);
1013        });
1014
1015        // Pin the lesson-4 `n1 == dedupe(ops.operators)` invariant: the
1016        // kind_id-keyed metrics store and the text-keyed `--ops` store are
1017        // independent, so a modifier classified in one but not the other
1018        // would diverge here.
1019        let path = PathBuf::from("foo.cpp");
1020        let parser = CppParser::new(source.as_bytes().to_vec(), &path, None);
1021        let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
1022        let unique_operators: HashSet<&str> = ops.operators.iter().map(String::as_str).collect();
1023        assert_eq!(
1024            unique_operators.len(),
1025            6,
1026            "dedupe(ops.operators) must equal n1; operators were {:?}",
1027            ops.operators
1028        );
1029        for modifier in ["unsigned", "signed", "long"] {
1030            assert!(
1031                unique_operators.contains(modifier),
1032                "sized_type_specifier modifier {modifier:?} missing from ops.operators: {:?}",
1033                ops.operators
1034            );
1035        }
1036    }
1037
1038    /// C++20 spaceship operator `<=>` (`Cpp::LTEQGT`) is a comparison
1039    /// operator and must be counted in Halstead, like its sibling
1040    /// comparison operators `<`, `>`, `<=`, `>=`, `==`, `!=`. Prior to
1041    /// this fix it fell through to the `Unknown` arm and was silently
1042    /// dropped from `n1` / `N1`, under-reporting volume / effort on any
1043    /// C++20+ codebase that defines `operator<=>`. Regression test for
1044    /// issue #197.
1045    #[cfg(feature = "cpp")]
1046    #[test]
1047    fn cpp_spaceship_operator_is_halstead_operator() {
1048        check_metrics::<CppParser>(
1049            "int f(int a, int b) {
1050                 return (a <=> b) != 0;
1051             }",
1052            "foo.cpp",
1053            |metric| {
1054                // Unique operators (grammar collapses matched delimiters
1055                // to a single kind_id): int, (), {}, <=>, !=, return, ;, ,
1056                //   `<=>` is the regression target — without the fix it
1057                //   would be Unknown and `u_operators` would be 7.
1058                // Unique operands: f, a, b, 0
1059                let s = &metric.halstead;
1060                assert_eq!(s.unique_operators(), 8);
1061                assert_eq!(s.unique_operands(), 4);
1062                insta::assert_json_snapshot!(
1063                    s,
1064                    @r#"
1065                {
1066                  "unique_operators": 8,
1067                  "total_operators": 11,
1068                  "unique_operands": 4,
1069                  "total_operands": 6,
1070                  "length": 17,
1071                  "estimated_program_length": 32.0,
1072                  "purity_ratio": 1.8823529411764706,
1073                  "vocabulary": 12,
1074                  "volume": 60.94436251225965,
1075                  "difficulty": 6.0,
1076                  "level": 0.16666666666666666,
1077                  "effort": 365.6661750735579,
1078                  "time": 20.31478750408655,
1079                  "bugs": 0.01704519358507665
1080                }
1081                "#
1082                );
1083            },
1084        );
1085    }
1086
1087    /// C++ compound subtract-assign `-=` (`Cpp::DASHEQ`) must be counted
1088    /// in Halstead like every other compound assignment (`+=`, `*=`,
1089    /// `/=`, etc.). Prior to the fix it fell through to the `Unknown`
1090    /// arm and was silently dropped from `n1` / `N1` — under-reporting
1091    /// volume / effort wherever C++ code subtracts in place. Regression
1092    /// test for issue #198.
1093    #[cfg(feature = "cpp")]
1094    #[test]
1095    fn cpp_dash_eq_is_halstead_operator() {
1096        check_metrics::<CppParser>("void f(int a, int b) { a -= b; }", "foo.cpp", |metric| {
1097            // Unique operators: void, (), {}, int, ,, -=, ;
1098            //   `-=` is the regression target — without the fix it
1099            //   would be Unknown and `u_operators` would be 6.
1100            // Unique operands: f, a, b
1101            let s = &metric.halstead;
1102            assert_eq!(s.unique_operators(), 7);
1103            assert_eq!(s.unique_operands(), 3);
1104        });
1105    }
1106
1107    /// C++ pointer-to-member access `.*` (`Cpp::DOTSTAR`) must be
1108    /// counted in Halstead. Prior to the fix it fell through to the
1109    /// `Unknown` arm and was silently dropped from `n1` / `N1`.
1110    /// Regression test for issue #198.
1111    ///
1112    /// The snippet uses an `operator.*` declaration because that is
1113    /// where the C++ tree-sitter grammar reliably emits a single
1114    /// `DOTSTAR` leaf; in expression position (`a.*b`) some grammar
1115    /// versions split the token into `DOT` + `STAR` and the regression
1116    /// would be masked.
1117    #[cfg(feature = "cpp")]
1118    #[test]
1119    fn cpp_dot_star_is_halstead_operator() {
1120        check_metrics::<CppParser>("struct S { void operator.*(int); };", "foo.cpp", |metric| {
1121            // Unique operators with fix: {}, ;, (), int, void, .*, the
1122            //   `operator` keyword (#1296) and `struct` (#1552).
1123            //   `.*` is the regression target — without the fix it
1124            //   falls through to `Unknown` and `u_operators` is 7.
1125            // Unique operands: S
1126            let s = &metric.halstead;
1127            assert_eq!(s.unique_operators(), 8);
1128            assert_eq!(s.unique_operands(), 1);
1129        });
1130    }
1131
1132    /// C++ pointer-to-member access through pointer `->*`
1133    /// (`Cpp::DASHGTSTAR`) must be counted in Halstead. Prior to the
1134    /// fix it fell through to the `Unknown` arm and was silently
1135    /// dropped from `n1` / `N1`. Regression test for issue #198.
1136    ///
1137    /// The snippet uses an `operator->*` declaration because that is
1138    /// where the C++ tree-sitter grammar reliably emits a single
1139    /// `DASHGTSTAR` leaf; in expression position (`a->*b`) the grammar
1140    /// splits the token into `DASHGT` + `STAR` and the regression would
1141    /// be masked.
1142    #[cfg(feature = "cpp")]
1143    #[test]
1144    fn cpp_dash_gt_star_is_halstead_operator() {
1145        check_metrics::<CppParser>(
1146            "struct S { void operator->*(int); };",
1147            "foo.cpp",
1148            |metric| {
1149                // Unique operators with fix: {}, ;, (), int, void, ->*,
1150                //   the `operator` keyword (#1296) and `struct` (#1552).
1151                //   `->*` is the regression target — without the fix it
1152                //   falls through to `Unknown` and `u_operators` is 7.
1153                // Unique operands: S
1154                let s = &metric.halstead;
1155                assert_eq!(s.unique_operators(), 8);
1156                assert_eq!(s.unique_operands(), 1);
1157            },
1158        );
1159    }
1160
1161    #[cfg(feature = "cpp")]
1162    #[test]
1163    fn cpp_raw_string_delimiter_is_not_an_operator() {
1164        // Regression: issue #1314, the C++ sibling of Elixir #1256 and
1165        // Ruby/Perl #1312. A `raw_string_literal` carries its `R"(`
1166        // opener as a bare `LPAREN` child — the kind id a call uses —
1167        // so `auto a = R"(raw)";` reported a `()` operator with no call
1168        // in the source.
1169        //
1170        // The fixture holds both sides at once: two raw strings and one
1171        // real call. A guard widened past the literal would drop
1172        // `f(a)`'s parenthesis and fail here rather than silently
1173        // passing.
1174        //
1175        // The second literal uses the custom-delimiter form to pin that
1176        // shape too — it adds a `raw_string_delimiter` child but keeps
1177        // the same `(` — and its distinct text makes n2 differ from N2.
1178        //
1179        // expected: operators `;` × 3, `=` × 3, `int`, `()` × 1 →
1180        // n1 = 4, N1 = 8. Operands the two literals, `a` × 2, `b`, `c`,
1181        // `f` → n2 = 6, N2 = 7. Before the guard the two openers added
1182        // two more `()` → N1 = 10.
1183        check_metrics::<CppParser>(
1184            "auto a = R\"(raw)\";\nauto b = R\"tag(raw)tag\";\nint c = f(a);\n",
1185            "foo.cpp",
1186            |metric| {
1187                assert_eq!(metric.halstead.unique_operators(), 4);
1188                assert_eq!(metric.halstead.total_operators(), 8);
1189                assert_eq!(metric.halstead.unique_operands(), 6);
1190                assert_eq!(metric.halstead.total_operands(), 7);
1191            },
1192        );
1193    }
1194
1195    #[cfg(feature = "rust")]
1196    #[test]
1197    fn rust_operators_and_operands() {
1198        check_metrics::<RustParser>(
1199            "fn main() {
1200              let a = 5; let b = 5; let c = 5;
1201              let avg = (a + b + c) / 3;
1202              println!(\"{}\", avg);
1203            }",
1204            "foo.rs",
1205            |metric| {
1206                // unique operators: fn, (), {}, let, =, +, /, ;, !, ,
1207                // unique operands: main, a, b, c, avg, 5, 3, println, "{}"
1208                insta::assert_json_snapshot!(
1209                    metric.halstead,
1210                    @r#"
1211                {
1212                  "unique_operators": 10,
1213                  "total_operators": 23,
1214                  "unique_operands": 9,
1215                  "total_operands": 15,
1216                  "length": 38,
1217                  "estimated_program_length": 61.74860596185444,
1218                  "purity_ratio": 1.624963314785643,
1219                  "vocabulary": 19,
1220                  "volume": 161.42124551085624,
1221                  "difficulty": 8.333333333333334,
1222                  "level": 0.12,
1223                  "effort": 1345.177045923802,
1224                  "time": 74.7320581068779,
1225                  "bugs": 0.040619232256751396
1226                }
1227                "#
1228                );
1229            },
1230        );
1231    }
1232
1233    #[cfg(feature = "rust")]
1234    #[test]
1235    fn rust_aliased_primitive_type_classification() {
1236        // Regression for issue #95 (lesson #2): the Rust grammar emits 17
1237        // distinct `kind_id`s for `primitive_type` (one base plus 16
1238        // numeric-suffixed alias variants). `RustCode::is_primitive` in
1239        // `big-code-analysis-ast/src/checker.rs` must list every variant;
1240        // if a future regression omits one, primitive type names emitted
1241        // in that aliased position silently drop into the kind_id-keyed
1242        // operators bucket instead of the text-keyed primitive_operators
1243        // map, miscounting Halstead n1.
1244        //
1245        // The snippet exercises every primitive scalar type across many
1246        // syntactic positions (function parameter types, return types,
1247        // let-binding annotations, `as` casts, const items, type aliases,
1248        // struct fields, function pointer types, tuple types, array types,
1249        // reference types, generic type arguments). Empirically, ordinary
1250        // Rust source emits the base `Rust::PrimitiveType` variant from
1251        // all of these positions; the 16 suffixed alias variants are
1252        // produced by specific grammar productions not reachable from
1253        // user-written code. Mutation-verified: dropping
1254        // `Rust::PrimitiveType` from `is_primitive` fails this test
1255        // (u_operators 30→15). Dropping any single suffixed variant
1256        // currently leaves the test passing; if a future grammar bump
1257        // makes any suffixed variant reachable from idiomatic source,
1258        // extend the snippet so the test fires for that variant too.
1259        check_metrics::<RustParser>(
1260            "const C: u8 = 0;
1261            type T = i64;
1262            struct S { x: u32, y: u64 }
1263            fn g(p: fn(u8) -> u16) -> bool { let _ = p(0); true }
1264            fn f(a: u8, b: u16, c: u32, d: u64) -> u128 {
1265                let _x: i8 = 0;
1266                let _y: i16 = 0;
1267                let _z: i32 = 0;
1268                let _w: i64 = 0;
1269                let _v: i128 = 0;
1270                let _p: f32 = 1.0;
1271                let _q: f64 = 2.0;
1272                let _r: bool = true;
1273                let _s: char = 'x';
1274                let _t: usize = 0;
1275                let _u: isize = 0;
1276                let _arr: [u32; 4] = [0; 4];
1277                let _ref: &u8 = &0;
1278                let _tup: (u32, u64) = (0, 0);
1279                let _opt: Option<u32> = None;
1280                a as u128 + b as u128 + c as u128 + d
1281            }",
1282            "foo.rs",
1283            |metric| {
1284                // Headline: u_operators is the load-bearing assertion —
1285                // the 16 distinct primitive type names dedupe by text in
1286                // the primitive_operators map. Total operators (N1) and
1287                // operand counts pin the rest of the Halstead state.
1288                // Grew from 30 → 33 with the issue #394 fix: `const`,
1289                // `type`, and `struct` keywords are now classified as
1290                // operators (one occurrence each). #1486 adds the
1291                // annotation `:` — one entry, 23 occurrences: `C`, the
1292                // two fields, `p`, the four params of `f`, and the
1293                // fifteen `let` bindings. #1559 bills `Option<u32>`'s
1294                // brackets as one `<>` pair: one entry and one
1295                // occurrence fewer.
1296                assert_eq!(metric.halstead.unique_operators(), 33);
1297                assert_eq!(metric.halstead.total_operators(), 143);
1298                // u_operands / operands grew (was 31/50 before #390): the
1299                // fix now classifies TypeIdentifier (`T`, `S`, `Option`)
1300                // and FieldIdentifier (struct fields `x`, `y`) as operands
1301                // alongside the existing primitive type names.
1302                assert_eq!(metric.halstead.unique_operands(), 36);
1303                assert_eq!(metric.halstead.total_operands(), 55);
1304            },
1305        );
1306    }
1307
1308    #[cfg(feature = "rust")]
1309    #[test]
1310    fn rust_field_identifier_is_operand() {
1311        // Regression for issue #390: prior to the fix, `FieldIdentifier`
1312        // (e.g. the `x` / `y` in `p.x`, `p.y`) fell through to
1313        // `TokenRole::Unknown`, so the field names were not counted
1314        // as operands. Both C++ and Go already classify FieldIdentifier
1315        // as an operand. After the fix:
1316        //   unique operators: fn, (), {}, let, =, +, ;, ., ,, :
1317        //   unique operands : main, p, Point, x, y, sum, 0, 1
1318        // Field names `x` and `y` each appear twice (`p.x + p.y` and
1319        // the struct literal `Point { x: 0, y: 1 }`).
1320        check_metrics::<RustParser>(
1321            "fn main() {
1322              let p = Point { x: 0, y: 1 };
1323              let sum = p.x + p.y;
1324            }",
1325            "foo.rs",
1326            |metric| {
1327                // Headline: pre-fix, FieldIdentifier (`x`, `y`) and
1328                // TypeIdentifier (`Point`) fell through to Unknown, so
1329                // u_operands was 5 (main, p, sum, 0, 1). After the
1330                // fix, +Point, +x, +y → 8 distinct names.
1331                assert_eq!(metric.halstead.unique_operands(), 8);
1332                assert_eq!(metric.halstead.total_operands(), 12);
1333                // N1: fn, (), {} ×2, let ×2, = ×2, +, ; ×2, . ×2, `,`,
1334                // and the struct literal's two field `:` (#1486) = 16.
1335                assert_eq!(metric.halstead.unique_operators(), 10);
1336                assert_eq!(metric.halstead.total_operators(), 16);
1337                insta::assert_json_snapshot!(
1338                    metric.halstead,
1339                    @r#"
1340                {
1341                  "unique_operators": 10,
1342                  "total_operators": 16,
1343                  "unique_operands": 8,
1344                  "total_operands": 12,
1345                  "length": 28,
1346                  "estimated_program_length": 57.219280948873624,
1347                  "purity_ratio": 2.043545748174058,
1348                  "vocabulary": 18,
1349                  "volume": 116.75790004038474,
1350                  "difficulty": 7.5,
1351                  "level": 0.13333333333333333,
1352                  "effort": 875.6842503028855,
1353                  "time": 48.64912501682697,
1354                  "bugs": 0.03051010983631521
1355                }
1356                "#
1357                );
1358            },
1359        );
1360    }
1361
1362    #[cfg(feature = "rust")]
1363    #[test]
1364    fn rust_type_identifier_is_operand() {
1365        // Regression for issue #390: `TypeIdentifier` (e.g. `Vec`,
1366        // `HashMap`, `String` when used as a path name) was dropped to
1367        // `TokenRole::Unknown` for Rust. C++ and Go classify them as
1368        // operands. After the fix, u_operands = 8:
1369        //   main, v, m, Vec, HashMap, new, K, V
1370        // (`i32` is a primitive type, classified as an operator.)
1371        //
1372        // Also covers issue #394: `::` is now an operator. The snippet
1373        // has two `::` tokens (`Vec::new`, `HashMap::new`), so n1 grew
1374        // from 10 → 11 and N1 from 17 → 19. #1486 then billed the two
1375        // annotation `:` tokens as their own entry: 12 and 21. #1559
1376        // billed each generic's brackets as one `<>` pair, replacing the
1377        // `<` and `>` entries and their four occurrences: 11 and 19.
1378        check_metrics::<RustParser>(
1379            "fn main() {
1380              let v: Vec<i32> = Vec::new();
1381              let m: HashMap<K, V> = HashMap::new();
1382            }",
1383            "foo.rs",
1384            |metric| {
1385                // Headline: u_operands includes `Vec`, `HashMap`, `K`,
1386                // `V` (and `i32` as a primitive operator). Without the
1387                // fix, Vec/HashMap/K/V silently dropped to Unknown.
1388                assert_eq!(metric.halstead.unique_operands(), 8);
1389                assert_eq!(metric.halstead.total_operands(), 11);
1390                // `::` appears twice (Vec::new, HashMap::new); without
1391                // the #394 fix u_operators was 10 and operators 17.
1392                // `:` appears twice and stays distinct from `::`.
1393                assert_eq!(metric.halstead.unique_operators(), 11);
1394                assert_eq!(metric.halstead.total_operators(), 19);
1395                insta::assert_json_snapshot!(
1396                    metric.halstead,
1397                    @r#"
1398                {
1399                  "unique_operators": 11,
1400                  "total_operators": 19,
1401                  "unique_operands": 8,
1402                  "total_operands": 11,
1403                  "length": 30,
1404                  "estimated_program_length": 62.05374780501027,
1405                  "purity_ratio": 2.068458260167009,
1406                  "vocabulary": 19,
1407                  "volume": 127.43782540330756,
1408                  "difficulty": 7.5625,
1409                  "level": 0.1322314049586777,
1410                  "effort": 963.7485546125134,
1411                  "time": 53.54158636736186,
1412                  "bugs": 0.03252279825177962
1413                }
1414                "#
1415                );
1416            },
1417        );
1418    }
1419
1420    /// #1486: Rust's single `:` is vocabulary in every position it is
1421    /// spelled, and stays a separate entry from `::`.
1422    #[cfg(feature = "rust")]
1423    #[test]
1424    fn rust_colon_is_operator_1486() {
1425        const ROWS: &[(&str, &str, [u64; 2])] = &[
1426            // `let`, `:`, `u8`, `=`, `;`
1427            ("let annotation", "let x: u8 = 1;\n", [5, 5]),
1428            // `struct`, `{}`, `:`, `u8`
1429            ("field declaration", "struct S { a: u8 }\n", [4, 4]),
1430            // `const`, `:` twice, `=`, `{}`, `;`
1431            ("field initializer", "const C: S = S { a: 1 };\n", [5, 6]),
1432            // `fn`, `{}` twice, `()`, `let`, `{}` (pattern), `:`, `=`, `;`
1433            ("field pattern", "fn f() { let S { a: b } = s; }\n", [7, 8]),
1434            // `fn`, `()`, `:`, `u8`, `{}`
1435            ("parameter", "fn f(x: u8) {}\n", [5, 5]),
1436            // `fn`, `<>`, `:`, `()`, `{}`
1437            ("trait bound", "fn f<T: Clone>() {}\n", [5, 5]),
1438            // `fn`, `<>`, `()`, `where`, `:`, `{}`
1439            ("where clause", "fn f<T>() where T: Clone {}\n", [6, 6]),
1440            // `fn`, `()`, `{}` twice, `:`, `loop`, `break`, `;`
1441            ("loop label", "fn f() { 'a: loop { break 'a; } }\n", [7, 8]),
1442            // `fn`, `()`, `{}`, `let`, `=`, `|` twice, `:`, `u8`, `;`
1443            (
1444                "closure parameter",
1445                "fn f() { let c = |q: u8| q; }\n",
1446                [9, 10],
1447            ),
1448            // `fn`, `()` twice, `{}`, `let`, `:`, `<>`, `u8`, `=`, `::`,
1449            // `;` — `:` and `::` are two entries
1450            (
1451                "beside a path",
1452                "fn f() { let v: Vec<u8> = Vec::new(); }\n",
1453                [10, 11],
1454            ),
1455        ];
1456        assert_operator_rows::<RustParser>("foo.rs", ROWS);
1457    }
1458
1459    #[cfg(feature = "rust")]
1460    #[test]
1461    fn rust_path_separator_is_operator() {
1462        // Regression for issue #394: `::` (`COLONCOLON`) was missing
1463        // from the Rust `get_op_type` operator arm even though C++,
1464        // Java, C#, and Kotlin all classify it as an operator. Path-
1465        // heavy code (`std::collections::HashMap`, `Vec::new`,
1466        // `T::method`) had every `::` silently dropped into
1467        // TokenRole::Unknown.
1468        //
1469        // Snippet has three `::` tokens (`std::collections::HashMap`,
1470        // counted as two `::` separators, plus `HashMap::new`).
1471        check_metrics::<RustParser>(
1472            "fn main() {
1473              let m = std::collections::HashMap::new();
1474            }",
1475            "foo.rs",
1476            |metric| {
1477                // `::` appears 3 times across the two path expressions
1478                // (`std::collections::HashMap` contributes two; the
1479                // `HashMap::new` contributes one). Pre-fix all three
1480                // dropped to Unknown: u_operators would be 6 (no `::`
1481                // distinct) and total_operators() would be 7 (minus 3 `::`
1482                // occurrences). With the fix u_operators=7 and
1483                // operators=10.
1484                //
1485                // unique operators (post-fix): fn, LPAREN, LBRACE,
1486                // let, =, ::, ;. unique operands: main, m, std,
1487                // collections, HashMap, new.
1488                assert_eq!(metric.halstead.unique_operators(), 7);
1489                assert_eq!(metric.halstead.total_operators(), 10);
1490                assert_eq!(metric.halstead.unique_operands(), 6);
1491                assert_eq!(metric.halstead.total_operands(), 6);
1492            },
1493        );
1494    }
1495
1496    #[cfg(feature = "rust")]
1497    #[test]
1498    fn rust_declaration_keywords_are_operators() {
1499        // Regression for issue #394: the Rust impl already accepted 17
1500        // keywords as operators (As, Async, Await, …, Fn) but omitted
1501        // 14 declaration / visibility keywords. The fix adds `Const`,
1502        // `Static`, `Enum`, `Struct`, `Trait`, `Impl`, `Use`, `Mod`,
1503        // `Pub`, `Type`, `Union`, `Where`, `Extern`, `Dyn`.
1504        //
1505        // Snippet exercises `use`, `pub`, `struct`, and `impl` (one of
1506        // each); together they account for 4 new operator occurrences
1507        // and 4 new unique operators.
1508        check_metrics::<RustParser>(
1509            "use std::fmt;
1510            pub struct S;
1511            impl S { fn n() -> u8 { 0 } }",
1512            "foo.rs",
1513            |metric| {
1514                // expected: unique operators (11) = use, ::, ;, pub,
1515                // struct, impl, LBRACE, fn, LPAREN, DASHGT, u8. Without
1516                // the #394 fix, `use`, `pub`, `struct`, and `impl`
1517                // would each drop to Unknown and u_operators would be
1518                // 7. unique operands (5): std, fmt, S, n, 0.
1519                assert_eq!(metric.halstead.unique_operators(), 11);
1520                assert_eq!(metric.halstead.total_operators(), 13);
1521                assert_eq!(metric.halstead.unique_operands(), 5);
1522                assert_eq!(metric.halstead.total_operands(), 6);
1523            },
1524        );
1525    }
1526
1527    #[cfg(feature = "javascript")]
1528    #[test]
1529    fn javascript_operators_and_operands() {
1530        check_metrics::<JavascriptParser>(
1531            "function main() {
1532              var a, b, c, avg;
1533              a = 5; b = 5; c = 5;
1534              avg = (a + b + c) / 3;
1535              console.log(\"{}\", avg);
1536            }",
1537            "foo.js",
1538            |metric| {
1539                // unique operators: function, (), {}, var, =, +, /, ,, ., ;
1540                // unique operands: main, a, b, c, avg, 3, 5, console, log, "{}"
1541                // `console.log` is the `.` operator applied to the two
1542                // identifier leaves; the composite `member_expression`
1543                // text is deliberately not a third operand (#1263), so
1544                // n2/N2 are 10/20 rather than the pre-#1263 11/21.
1545                insta::assert_json_snapshot!(
1546                    metric.halstead,
1547                    @r#"
1548                {
1549                  "unique_operators": 10,
1550                  "total_operators": 24,
1551                  "unique_operands": 10,
1552                  "total_operands": 20,
1553                  "length": 44,
1554                  "estimated_program_length": 66.43856189774725,
1555                  "purity_ratio": 1.5099673158578921,
1556                  "vocabulary": 20,
1557                  "volume": 190.16483617504394,
1558                  "difficulty": 10.0,
1559                  "level": 0.1,
1560                  "effort": 1901.6483617504396,
1561                  "time": 105.64713120835775,
1562                  "bugs": 0.05116412536051621
1563                }
1564                "#
1565                );
1566            },
1567        );
1568    }
1569
1570    #[cfg(feature = "mozjs")]
1571    #[test]
1572    fn mozjs_operators_and_operands() {
1573        check_metrics::<MozjsParser>(
1574            "function main() {
1575              var a, b, c, avg;
1576              a = 5; b = 5; c = 5;
1577              avg = (a + b + c) / 3;
1578              console.log(\"{}\", avg);
1579            }",
1580            "foo.js",
1581            |metric| {
1582                // unique operators: function, (), {}, var, =, +, /, ,, ., ;
1583                // unique operands: main, a, b, c, avg, 3, 5, console, log, "{}"
1584                // `console.log` is the `.` operator applied to the two
1585                // identifier leaves; the composite `member_expression`
1586                // text is deliberately not a third operand (#1263), so
1587                // n2/N2 are 10/20 rather than the pre-#1263 11/21.
1588                insta::assert_json_snapshot!(
1589                    metric.halstead,
1590                    @r#"
1591                {
1592                  "unique_operators": 10,
1593                  "total_operators": 24,
1594                  "unique_operands": 10,
1595                  "total_operands": 20,
1596                  "length": 44,
1597                  "estimated_program_length": 66.43856189774725,
1598                  "purity_ratio": 1.5099673158578921,
1599                  "vocabulary": 20,
1600                  "volume": 190.16483617504394,
1601                  "difficulty": 10.0,
1602                  "level": 0.1,
1603                  "effort": 1901.6483617504396,
1604                  "time": 105.64713120835775,
1605                  "bugs": 0.05116412536051621
1606                }
1607                "#
1608                );
1609            },
1610        );
1611    }
1612
1613    #[cfg(feature = "typescript")]
1614    #[test]
1615    fn typescript_operators_and_operands() {
1616        check_metrics::<TypescriptParser>(
1617            "function main() {
1618              var a, b, c, avg;
1619              a = 5; b = 5; c = 5;
1620              avg = (a + b + c) / 3;
1621              console.log(\"{}\", avg);
1622            }",
1623            "foo.ts",
1624            |metric| {
1625                // unique operators: function, (), {}, var, =, +, /, ,, ., ;
1626                // unique operands: main, a, b, c, avg, 3, 5, console, log, "{}"
1627                // `console.log` is the `.` operator applied to the two
1628                // identifier leaves; the composite `member_expression`
1629                // text is deliberately not a third operand (#1263), so
1630                // n2/N2 are 10/20 rather than the pre-#1263 11/21.
1631                insta::assert_json_snapshot!(
1632                    metric.halstead,
1633                    @r#"
1634                {
1635                  "unique_operators": 10,
1636                  "total_operators": 24,
1637                  "unique_operands": 10,
1638                  "total_operands": 20,
1639                  "length": 44,
1640                  "estimated_program_length": 66.43856189774725,
1641                  "purity_ratio": 1.5099673158578921,
1642                  "vocabulary": 20,
1643                  "volume": 190.16483617504394,
1644                  "difficulty": 10.0,
1645                  "level": 0.1,
1646                  "effort": 1901.6483617504396,
1647                  "time": 105.64713120835775,
1648                  "bugs": 0.05116412536051621
1649                }
1650                "#
1651                );
1652            },
1653        );
1654    }
1655
1656    #[cfg(feature = "typescript")]
1657    #[test]
1658    fn tsx_operators_and_operands() {
1659        check_metrics::<TsxParser>(
1660            "function main() {
1661              var a, b, c, avg;
1662              a = 5; b = 5; c = 5;
1663              avg = (a + b + c) / 3;
1664              console.log(\"{}\", avg);
1665            }",
1666            "foo.ts",
1667            |metric| {
1668                // unique operators: function, (), {}, var, =, +, /, ,, ., ;
1669                // unique operands: main, a, b, c, avg, 3, 5, console, log, "{}"
1670                // `console.log` is the `.` operator applied to the two
1671                // identifier leaves; the composite `member_expression`
1672                // text is deliberately not a third operand (#1263), so
1673                // n2/N2 are 10/20 rather than the pre-#1263 11/21.
1674                insta::assert_json_snapshot!(
1675                    metric.halstead,
1676                    @r#"
1677                {
1678                  "unique_operators": 10,
1679                  "total_operators": 24,
1680                  "unique_operands": 10,
1681                  "total_operands": 20,
1682                  "length": 44,
1683                  "estimated_program_length": 66.43856189774725,
1684                  "purity_ratio": 1.5099673158578921,
1685                  "vocabulary": 20,
1686                  "volume": 190.16483617504394,
1687                  "difficulty": 10.0,
1688                  "level": 0.1,
1689                  "effort": 1901.6483617504396,
1690                  "time": 105.64713120835775,
1691                  "bugs": 0.05116412536051621
1692                }
1693                "#
1694                );
1695            },
1696        );
1697    }
1698
1699    #[cfg(feature = "javascript")]
1700    #[test]
1701    fn javascript_template_string_plain_is_operand() {
1702        // Regression: issue #192. A backtick-delimited `` `hello` ``
1703        // without `${...}` is semantically identical to `"hello"` /
1704        // `'hello'` and must contribute exactly one operand — before
1705        // the fix `TemplateString` fell through to `TokenRole::Unknown`
1706        // and contributed zero. expected: operands are `f` (function
1707        // name) and the wrapping `` `hello` `` template literal →
1708        // u_operands = 2, N2 = 2 (matches the equivalent
1709        // `function f() { return "hello"; }` baseline).
1710        check_metrics::<JavascriptParser>("function f() { return `hello`; }", "foo.js", |metric| {
1711            assert_eq!(metric.halstead.unique_operands(), 2);
1712            assert_eq!(metric.halstead.total_operands(), 2);
1713        });
1714    }
1715
1716    /// Regression for #695. The `get` / `set` property-accessor keywords
1717    /// are operators, matching the C# getter's `Get | Set | Init | Add |
1718    /// Remove` accessor arm. Before #695 the JS family classified them as
1719    /// operands, so the same accessor keyword landed in opposite Halstead
1720    /// groups across languages. This pins them in the operator store and
1721    /// out of the operand store.
1722    #[cfg(feature = "javascript")]
1723    #[test]
1724    fn js_get_set_accessors_are_operators() {
1725        let source = "class C { get x() { return 1; } set x(v) { this._x = v; } }";
1726        let path = PathBuf::from("foo.js");
1727        let parser = JavascriptParser::new(source.as_bytes().to_vec(), &path, None);
1728        let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
1729        assert!(
1730            ops.operators.iter().any(|o| o.as_str() == "get")
1731                && ops.operators.iter().any(|o| o.as_str() == "set"),
1732            "`get`/`set` accessors must be operators; operators were {:?}",
1733            ops.operators
1734        );
1735        assert!(
1736            !ops.operands.iter().any(|o| o.as_str() == "get")
1737                && !ops.operands.iter().any(|o| o.as_str() == "set"),
1738            "`get`/`set` accessors must not be operands; operands were {:?}",
1739            ops.operands
1740        );
1741    }
1742
1743    #[cfg(feature = "javascript")]
1744    #[test]
1745    fn javascript_template_string_interpolation_no_double_count() {
1746        // Regression: issue #192. An interpolated template literal
1747        // `` `Hi ${name}!` `` used to fall through to `Unknown`,
1748        // dropping the wrapper from the count entirely; the inner
1749        // `name` was still walked and counted via the
1750        // `TemplateSubstitution` child. Mirrors #183 (C#), #191
1751        // (Kotlin), #199 (Perl): the wrapper is skipped when a
1752        // `TemplateSubstitution` child is present so the inner
1753        // expression is not double-counted.
1754        //
1755        // expected: for `function f(name) { return ` + "`Hi ${name}!`"
1756        // + `; }`, operands are `f` and `name` (twice — `name` as the
1757        // parameter, then again inside the interpolation), so
1758        // u_operands = 2 and N2 = 3. Without the wrapper-skip guard
1759        // the wrapping literal would also be counted, lifting
1760        // u_operands to 3 and N2 to 4.
1761        check_metrics::<JavascriptParser>(
1762            "function f(name) { return `Hi ${name}!`; }",
1763            "foo.js",
1764            |metric| {
1765                assert_eq!(metric.halstead.unique_operands(), 2);
1766                assert_eq!(metric.halstead.total_operands(), 3);
1767            },
1768        );
1769    }
1770
1771    #[cfg(feature = "mozjs")]
1772    #[test]
1773    fn mozjs_template_string_plain_is_operand() {
1774        // Regression: issue #192. Mirrors
1775        // `javascript_template_string_plain_is_operand` for the
1776        // Firefox-mode dialect — the four JS-family `get_op_type`
1777        // impls share the same template-literal handling.
1778        check_metrics::<MozjsParser>("function f() { return `hello`; }", "foo.js", |metric| {
1779            assert_eq!(metric.halstead.unique_operands(), 2);
1780            assert_eq!(metric.halstead.total_operands(), 2);
1781        });
1782    }
1783
1784    #[cfg(feature = "mozjs")]
1785    #[test]
1786    fn mozjs_template_string_interpolation_no_double_count() {
1787        // Regression: issue #192. Mirrors
1788        // `javascript_template_string_interpolation_no_double_count`
1789        // for the Firefox-mode dialect.
1790        check_metrics::<MozjsParser>(
1791            "function f(name) { return `Hi ${name}!`; }",
1792            "foo.js",
1793            |metric| {
1794                assert_eq!(metric.halstead.unique_operands(), 2);
1795                assert_eq!(metric.halstead.total_operands(), 3);
1796            },
1797        );
1798    }
1799
1800    /// One JSX shape per row of #1483's decision: the source, the
1801    /// expected `n2` / `N2`, and the deduplicated operand vocabulary.
1802    /// Every row is a single element bound to `a`, so the operands
1803    /// outside the element's content are always `a` plus the tag names.
1804    #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
1805    const JSX_TEXT_CASES: &[(&str, &str, [u64; 2], &[&str])] = &[
1806        // Element text is one operand. N2: a, p x2 (both tags), hi.
1807        ("text", "const a = <p>hi</p>;\n", [3, 4], &["a", "p", "hi"]),
1808        // The same-line space between `<b/>` and `<i/>` is a jsx_text
1809        // node of its own, and bills nothing. Billing it would key an
1810        // empty operand: (5, 6).
1811        (
1812            "whitespace",
1813            "const a = <p><b/> <i/></p>;\n",
1814            [4, 5],
1815            &["a", "p", "b", "i"],
1816        ),
1817        // `Start` at two indentation depths is one distinct operand,
1818        // because the key is the trimmed text. Untrimmed: (5, 7).
1819        (
1820            "indentation",
1821            "const a = (\n  <p>\n    Start\n    <span>\n      Start\n    </span>\n  </p>\n);\n",
1822            [4, 7],
1823            &["a", "p", "span", "Start"],
1824        ),
1825        // An entity splits the text into three siblings, each an
1826        // operand. Without the entity arm: (4, 5).
1827        (
1828            "entity in text",
1829            "const a = <p>x &amp; y</p>;\n",
1830            [5, 6],
1831            &["a", "p", "x", "&amp;", "y"],
1832        ),
1833        // Inside an attribute string the entity is part of the string
1834        // operand and bills nothing of its own. Without the parent
1835        // guard: (5, 5).
1836        (
1837            "entity in attribute",
1838            "const a = <p title=\"x &amp; y\" />;\n",
1839            [4, 4],
1840            &["a", "p", "title", "\"x &amp; y\""],
1841        ),
1842        // An expression child is unchanged by #1483: its identifier was
1843        // already an operand.
1844        (
1845            "expression",
1846            "const a = <p>{v}</p>;\n",
1847            [3, 4],
1848            &["a", "p", "v"],
1849        ),
1850    ];
1851
1852    /// Asserts every row of [`JSX_TEXT_CASES`] under parser `T`, through
1853    /// the metrics store (`n2` / `N2`) and the `--ops` vocabulary.
1854    #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
1855    #[track_caller]
1856    fn assert_jsx_text_operands<T: crate::MetricSuite>(file: &str) {
1857        for (label, source, expected, vocabulary) in JSX_TEXT_CASES {
1858            crate::test_support::check_func_space_only::<T, _>(
1859                source,
1860                file,
1861                &[crate::Metric::Halstead],
1862                |space| {
1863                    let halstead = &space.metrics.halstead;
1864                    assert_eq!(
1865                        [halstead.unique_operands(), halstead.total_operands()],
1866                        *expected,
1867                        "{file}: {label}"
1868                    );
1869                },
1870            );
1871            let ops = ops_of::<T>(source, file);
1872            let got: BTreeSet<&str> = ops.operands.iter().map(String::as_str).collect();
1873            let want: BTreeSet<&str> = vocabulary.iter().copied().collect();
1874            assert_eq!(got, want, "{file}: {label} operand vocabulary");
1875        }
1876    }
1877
1878    /// Regression for #1483: JSX element text and its character
1879    /// references are operands. One row per grammar, so reverting one
1880    /// invocation's `jsx:` argument fails its own row.
1881    #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
1882    #[test]
1883    fn jsx_text_is_an_operand() {
1884        // A whitespace-only node bills nothing, so no count can tell the
1885        // "whitespace" row's fixture from one that stopped spelling the
1886        // same-line space. Pin the node's presence directly.
1887        let (_, source, _, _) = JSX_TEXT_CASES
1888            .iter()
1889            .find(|(label, ..)| *label == "whitespace")
1890            .expect("the whitespace row exists");
1891        let parser =
1892            JavascriptParser::new(source.as_bytes().to_vec(), &PathBuf::from("foo.jsx"), None);
1893        assert!(
1894            parser.root().preorder().any(|n| {
1895                n.kind_id() == Javascript::JsxText as u16
1896                    && source.as_bytes()[n.start_byte()..n.end_byte()]
1897                        .trim_ascii()
1898                        .is_empty()
1899            }),
1900            "the whitespace row must carry a whitespace-only jsx_text node"
1901        );
1902
1903        assert_jsx_text_operands::<JavascriptParser>("foo.jsx");
1904        assert_jsx_text_operands::<MozjsParser>("foo.js");
1905        assert_jsx_text_operands::<TsxParser>("foo.tsx");
1906    }
1907
1908    /// The `.ts` grammar's enum carries `JsxText` in the externals it
1909    /// shares with TSX, but no production emits it, which is why the
1910    /// TypeScript invocation passes no `jsx:` argument (#1483). Pinned
1911    /// so a grammar that starts emitting it cannot go unbilled quietly.
1912    #[cfg(feature = "typescript")]
1913    #[test]
1914    fn typescript_never_emits_jsx_text() {
1915        let parser = TypescriptParser::new(
1916            b"const a = <p>hi</p>;\nlet b = <T>(x);\n".to_vec(),
1917            &PathBuf::from("foo.ts"),
1918            None,
1919        );
1920        // The positive half: the parse reached the angle-bracket syntax
1921        // TSX would read as JSX, so the absence below is a property of
1922        // the grammar rather than of a parse that produced nothing.
1923        assert!(
1924            ast_has_kind_id(&parser, Typescript::TypeAssertion as u16),
1925            "fixture lost the `<T>(x)` type assertion"
1926        );
1927        assert!(
1928            !ast_has_kind_id(&parser, Typescript::JsxText as u16),
1929            "the .ts grammar now emits jsx_text; give its invocation a `jsx:` argument"
1930        );
1931    }
1932
1933    #[cfg(feature = "typescript")]
1934    #[test]
1935    fn typescript_template_string_plain_is_operand() {
1936        // Regression: issue #192. Mirrors
1937        // `javascript_template_string_plain_is_operand` for
1938        // TypeScript — the four JS-family `get_op_type` impls share
1939        // the same template-literal handling.
1940        //
1941        // The `: string` annotation contributes no operand — its
1942        // keyword counts once, as the text-keyed operator (#1261) — so
1943        // the operands are `f` and `` `hello` `` (2 each). The headline
1944        // of this test — that the plain template literal contributes
1945        // one operand — is unaffected.
1946        check_metrics::<TypescriptParser>(
1947            "function f(): string { return `hello`; }",
1948            "foo.ts",
1949            |metric| {
1950                assert_eq!(metric.halstead.unique_operands(), 2);
1951                assert_eq!(metric.halstead.total_operands(), 2);
1952            },
1953        );
1954    }
1955
1956    #[cfg(feature = "typescript")]
1957    #[test]
1958    fn typescript_template_string_interpolation_no_double_count() {
1959        // Regression: issue #192. Mirrors
1960        // `javascript_template_string_interpolation_no_double_count`
1961        // for TypeScript.
1962        //
1963        // The `: string` annotations contribute no operands (#1261).
1964        // Unique operands: `f`, `name` (2). Total operands: `f`, `name`
1965        // (param), `name` (in the interpolation) (3). The interpolation
1966        // guard from #192 still holds — the wrapping `` `Hi ${name}!` ``
1967        // is `Unknown`, not double-counted.
1968        check_metrics::<TypescriptParser>(
1969            "function f(name: string): string { return `Hi ${name}!`; }",
1970            "foo.ts",
1971            |metric| {
1972                assert_eq!(metric.halstead.unique_operands(), 2);
1973                assert_eq!(metric.halstead.total_operands(), 3);
1974            },
1975        );
1976    }
1977
1978    #[cfg(feature = "typescript")]
1979    #[test]
1980    fn tsx_template_string_plain_is_operand() {
1981        // Regression: issue #192. Mirrors
1982        // `javascript_template_string_plain_is_operand` for the
1983        // TSX (TypeScript + JSX) variant.
1984        //
1985        // TSX's type-keyword `string` (`String3`) contributes no
1986        // operand, mirroring TS::String2 (#1261): operands are `f` and
1987        // `` `hello` `` (2 each).
1988        check_metrics::<TsxParser>(
1989            "function f(): string { return `hello`; }",
1990            "foo.tsx",
1991            |metric| {
1992                assert_eq!(metric.halstead.unique_operands(), 2);
1993                assert_eq!(metric.halstead.total_operands(), 2);
1994            },
1995        );
1996    }
1997
1998    #[cfg(feature = "typescript")]
1999    #[test]
2000    fn tsx_template_string_interpolation_no_double_count() {
2001        // Regression: issue #192. Mirrors
2002        // `javascript_template_string_interpolation_no_double_count`
2003        // for the TSX (TypeScript + JSX) variant.
2004        //
2005        // The `: string` annotations contribute no `String3` operands
2006        // (#1261); see `typescript_template_string_…` for the count
2007        // derivation.
2008        check_metrics::<TsxParser>(
2009            "function f(name: string): string { return `Hi ${name}!`; }",
2010            "foo.tsx",
2011            |metric| {
2012                assert_eq!(metric.halstead.unique_operands(), 2);
2013                assert_eq!(metric.halstead.total_operands(), 3);
2014            },
2015        );
2016    }
2017
2018    /// The JS-family regex fixture, asserted against all four grammars.
2019    ///
2020    /// `impl_js_family_get_op_type!` is instantiated four times against
2021    /// four distinct `kind_id` enums (`SLASH` 87/81/90/87, `Regex`
2022    /// 224/250/264/225), so each expansion is a separate compiled arm
2023    /// and a drift in one grammar is invisible if only one is checked
2024    /// (grammar-dispatch section 11).
2025    #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2026    fn assert_js_family_counts(source: &str, expected: [u64; 4]) {
2027        assert_halstead_counts::<JavascriptParser>(source, "foo.js", expected, "javascript");
2028        assert_halstead_counts::<MozjsParser>(source, "foo.jsm", expected, "mozjs");
2029        assert_halstead_counts::<TypescriptParser>(source, "foo.ts", expected, "typescript");
2030        assert_halstead_counts::<TsxParser>(source, "foo.tsx", expected, "tsx");
2031    }
2032
2033    #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2034    #[test]
2035    fn js_family_regex_delimiters_are_not_operators() {
2036        // Regression: issue #1314, the JS-family sibling of Elixir
2037        // #1256 and Ruby/Perl #1312. A `regex` literal spells both of
2038        // its delimiters `SLASH` — the kind id real division uses — so
2039        // `const a = /abc/g;` reported a `/` operator with no division
2040        // in the source, and n1/N1 counted the literal's punctuation as
2041        // arithmetic.
2042        //
2043        // The same fixture pins the second, independent half: `Regex`
2044        // was in neither arm, so the literal contributed no operand
2045        // either and reached the vocabulary from *neither* side.
2046        //
2047        // expected: operators `const`, `=`, `;`, `let` → n1 = 4;
2048        // `const` `=` `;` on line 1, `let` `=` `;` on line 2, `=` `;`
2049        // on line 3 → N1 = 8. Operands `a`, `/abc/g`, `b` → n2 = 3,
2050        // with `a` used three times and `b` twice → N2 = 6.
2051        //
2052        // Before the fix: n1 = 5 and N1 = 10 (the two fabricated `/`),
2053        // n2 = 2 and N2 = 5 (no operand for the literal).
2054        //
2055        // The four values are deliberately distinct so no transposition
2056        // of the unique-vs-total axes inside `assert_halstead_counts`
2057        // can pass (#1312).
2058        assert_js_family_counts("const a = /abc/g;\nlet b = a;\nb = a;\n", [4, 8, 3, 6]);
2059    }
2060
2061    #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2062    #[test]
2063    fn js_family_division_survives_the_regex_guard() {
2064        // Control for #1314: the guard is scoped to a `Regex` parent,
2065        // so real division must still count. This fixture holds both
2066        // sides at once — two divisions and one regex literal — so a
2067        // guard widened to every `SLASH` fails here rather than
2068        // silently passing the test above.
2069        //
2070        // expected: operators `const`, `=`, `;`, `/` → n1 = 4; two
2071        // `const`, two `=`, two `;` and two `/` → N1 = 8. Operands
2072        // `q`, `a`, `b`, `c`, `r`, `/x/` → n2 = N2 = 6.
2073        assert_js_family_counts("const q = a / b / c;\nconst r = /x/;\n", [4, 8, 6, 6]);
2074    }
2075
2076    #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2077    #[test]
2078    fn js_regex_delimiter_guard_is_parent_scoped_is_unobservable() {
2079        // Companion to the two above, and a statement of what they do
2080        // *not* cover. Ruby's guard has
2081        // `ruby_regex_guard_is_parent_scoped_not_ancestor_scoped`
2082        // because a division inside `#{…}` sits under a `Regex`
2083        // ancestor without being its child. No JS fixture can do that:
2084        // a regex literal admits no nested expression at all, its
2085        // `regex_pattern` and `regex_flags` children being leaves. So
2086        // the ancestor-scoped mutant of this guard — the one #1256's
2087        // post-mortem says survives every ordinary fixture — is
2088        // unobservable here. Measured, not assumed.
2089        //
2090        // Rather than write a fixture that would pass under both
2091        // spellings and read as coverage, pin the grammar property the
2092        // claim rests on: within a fixture that puts a division, a
2093        // template substitution and a regex in one file, every `SLASH`
2094        // reachable *below* a `Regex` is its immediate child. Should a
2095        // bump start nesting expressions inside a regex, this turns red
2096        // and the distinction becomes both observable and worth a real
2097        // test.
2098        //
2099        // Checked against all four grammars, not just JavaScript: the
2100        // guard is instantiated four times against four distinct enums,
2101        // and the property this test exists to watch could hold in one
2102        // and lapse in another.
2103        let source = b"const a = /abc/g;\nconst q = x / y;\nconst t = `p ${x / y} ${/zz/} q`;\n";
2104        assert_regex_slashes_are_immediate_children::<crate::langs::JavascriptCode>(
2105            source,
2106            Javascript::SLASH as u16,
2107            Javascript::Regex as u16,
2108            "javascript",
2109        );
2110        assert_regex_slashes_are_immediate_children::<crate::langs::MozjsCode>(
2111            source,
2112            Mozjs::SLASH as u16,
2113            Mozjs::Regex as u16,
2114            "mozjs",
2115        );
2116        assert_regex_slashes_are_immediate_children::<crate::langs::TypescriptCode>(
2117            source,
2118            Typescript::SLASH as u16,
2119            Typescript::Regex as u16,
2120            "typescript",
2121        );
2122        assert_regex_slashes_are_immediate_children::<crate::langs::TsxCode>(
2123            source,
2124            Tsx::SLASH as u16,
2125            Tsx::Regex as u16,
2126            "tsx",
2127        );
2128    }
2129
2130    /// Asserts every `slash` token below a `regex` node in `source` is
2131    /// that node's *immediate* child, for one grammar.
2132    ///
2133    /// Backs `js_regex_delimiter_guard_is_parent_scoped_is_unobservable`
2134    /// — see there for why the property is worth pinning.
2135    #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2136    fn assert_regex_slashes_are_immediate_children<L: crate::traits::LanguageInfo>(
2137        source: &[u8],
2138        slash: u16,
2139        regex: u16,
2140        label: &str,
2141    ) {
2142        let mut slashes_below_a_regex = 0;
2143        let visited = for_each_node_with_chain::<L>(source, |node: &Node<'_>, chain| {
2144            if node.kind_id() != slash {
2145                return;
2146            }
2147            let Some(depth) = chain.iter().position(|a| a.kind_id() == regex) else {
2148                return;
2149            };
2150            slashes_below_a_regex += 1;
2151            assert_eq!(
2152                depth,
2153                chain.len() - 1,
2154                "{label}: a slash at row {} has a regex ancestor that is not its parent, so \
2155                 the parent-vs-ancestor mutant is now observable and needs a real test",
2156                node.start_row()
2157            );
2158        });
2159        assert!(visited > 20, "{label}: fixture is too small to prove much");
2160        // Without this the assertion above is vacuous whenever the
2161        // fixture stops containing a regex at all — the failure mode a
2162        // filter that matches nothing always has.
2163        assert_eq!(
2164            slashes_below_a_regex, 4,
2165            "{label}: expected the two regex literals' four delimiters; the fixture no \
2166             longer exercises what this test claims"
2167        );
2168    }
2169
2170    // Issue #281: optional chaining (`?.`) was double-counted as a
2171    // Halstead operator in TypeScript and TSX because the grammar
2172    // exposes both an `optional_chain` named wrapper AND a child
2173    // `?.` token, and both were classified as `Operator`. The fix
2174    // counts only the bare `?.` token (`QMARKDOT`) in TS/TSX so each
2175    // textual `?.` contributes exactly once, matching JS / MozJS
2176    // (whose grammars expose only `OptionalChain` — the `?.` token
2177    // itself).
2178    //
2179    // The four assertions below all compare against the same totals:
2180    // for `function f(a) { return a?.b?.c; }` the operator stream is
2181    // `function`, `(`, `{`, `return`, `?.`, `?.`, `;` (7 total, 6
2182    // unique — `LPAREN`/`LBRACE` count once, closing tokens are not
2183    // in the operator set). Before the fix, TS/TSX reported 9/7
2184    // instead of 7/6.
2185    #[cfg(feature = "javascript")]
2186    #[test]
2187    fn javascript_optional_chain_not_double_counted_in_halstead_281() {
2188        check_metrics::<JavascriptParser>("function f(a) { return a?.b?.c; }", "foo.js", |m| {
2189            assert_eq!(m.halstead.unique_operators(), 6);
2190            assert_eq!(m.halstead.total_operators(), 7);
2191        });
2192    }
2193
2194    #[cfg(feature = "mozjs")]
2195    #[test]
2196    fn mozjs_optional_chain_not_double_counted_in_halstead_281() {
2197        check_metrics::<MozjsParser>("function f(a) { return a?.b?.c; }", "foo.js", |m| {
2198            assert_eq!(m.halstead.unique_operators(), 6);
2199            assert_eq!(m.halstead.total_operators(), 7);
2200        });
2201    }
2202
2203    #[cfg(feature = "typescript")]
2204    #[test]
2205    fn typescript_optional_chain_not_double_counted_in_halstead_281() {
2206        // The TS grammar wraps member-expression `?.` in an
2207        // `optional_chain` named node containing the bare `?.`
2208        // token; classifying both as `Operator` double-counted the
2209        // chain. We now count only the bare token, so TS matches JS.
2210        check_metrics::<TypescriptParser>("function f(a) { return a?.b?.c; }", "foo.ts", |m| {
2211            assert_eq!(m.halstead.unique_operators(), 6);
2212            assert_eq!(m.halstead.total_operators(), 7);
2213        });
2214    }
2215
2216    #[cfg(feature = "typescript")]
2217    #[test]
2218    fn tsx_optional_chain_not_double_counted_in_halstead_281() {
2219        check_metrics::<TsxParser>("function f(a) { return a?.b?.c; }", "foo.tsx", |m| {
2220            assert_eq!(m.halstead.unique_operators(), 6);
2221            assert_eq!(m.halstead.total_operators(), 7);
2222        });
2223    }
2224
2225    // Issue #299: parity guard for the JS-family `get_op_type` macro
2226    // on the optional-chain operator token (#281's prior regression
2227    // surface). All four languages must classify the bare `?.` token
2228    // identically — `OptionalChain` in JS/MozJS, `QMARKDOT` in
2229    // TS/TSX — and emit the same totals for
2230    // `function f(a) { return a?.b?.c; }`:
2231    //
2232    // * Operators: `function`, `(`, `{`, `return`, `?.`, `?.`, `;`
2233    //   (7 total, 6 unique).
2234    // * Operands: `f`, `a` (parameter), `a`, `b`, `c` — the identifier
2235    //   and property leaves only (5 total, 4 unique). Until #1263 the
2236    //   two wrapping member expressions (`a?.b`, `a?.b?.c`) were
2237    //   classified as `MemberExpression*` operands on top of the leaves
2238    //   they contain, making this 7 total / 6 unique.
2239    //
2240    // Verified by test-via-revert: dropping `OptionalChain` from
2241    // JS/MozJS, or `QMARKDOT` from TS/TSX, trips the test
2242    // (u_operators 6→5). This input does NOT exercise every operand
2243    // alias in the per-language `operand_extras` (`Identifier2`, the
2244    // JS/MozJS/TSX string-literal `String2`); drift in
2245    // those is out of scope for this regression guard and would need a
2246    // separate fixture. The `PredefinedType` operator path (`: void`
2247    // double-count) is now covered by `ts_void_return_type_single_operator_453`
2248    // below.
2249    #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2250    #[test]
2251    fn js_family_get_op_type_parity_optional_chain_member_299() {
2252        // Non-capturing closure (coerced to the `fn` pointer that
2253        // `check_metrics` accepts) avoids the
2254        // `clippy::needless_pass_by_value` warning that a free `fn`
2255        // taking `CodeMetrics` by value would trigger.
2256        #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2257        const SRC: &str = "function f(a) { return a?.b?.c; }";
2258        let check = |m: crate::CodeMetrics| {
2259            assert_eq!(m.halstead.unique_operators(), 6);
2260            assert_eq!(m.halstead.total_operators(), 7);
2261            assert_eq!(m.halstead.unique_operands(), 4);
2262            assert_eq!(m.halstead.total_operands(), 5);
2263        };
2264
2265        check_metrics::<JavascriptParser>(SRC, "foo.js", check);
2266        check_metrics::<MozjsParser>(SRC, "foo.js", check);
2267        check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2268        check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2269    }
2270
2271    // Issue #1263: a member access contributes its leaves and the `.`
2272    // operator, never the `member_expression` composite as well. The
2273    // classification does not stop the walk, so `a` and `b` were always
2274    // counted; listing the wrapper billed a third operand keyed on the
2275    // whole `a.b` text, which no other language here does.
2276    //
2277    // expected, for `var r = a.b;`:
2278    //
2279    // * Operators: `var`, `=`, `.`, `;` — 4 total, 4 unique.
2280    // * Operands: `r`, `a`, `b` — 3 total, 3 unique. Before the fix
2281    //   the `member_expression` wrapper added `a.b`, making both 4.
2282    //
2283    // All four JS-family languages are asserted because
2284    // `impl_js_family_get_op_type!` emits one shared operand arm: the
2285    // lockstep is the point of the macro, and a per-language extras
2286    // list is exactly where a future edit could break it.
2287    #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2288    #[test]
2289    fn js_family_member_access_counts_leaves_not_the_composite_1263() {
2290        #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2291        const SRC: &str = "var r = a.b;";
2292        let check = |m: crate::CodeMetrics| {
2293            assert_eq!(m.halstead.unique_operators(), 4);
2294            assert_eq!(m.halstead.total_operators(), 4);
2295            assert_eq!(m.halstead.unique_operands(), 3);
2296            assert_eq!(m.halstead.total_operands(), 3);
2297        };
2298
2299        check_metrics::<JavascriptParser>(SRC, "foo.js", check);
2300        check_metrics::<MozjsParser>(SRC, "foo.js", check);
2301        check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2302        check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2303    }
2304
2305    // Issue #1263, the grammar-dispatch section 6 half: dropping
2306    // `MemberExpression*` from the operand arm would have regressed
2307    // private-field access to *zero* operands for the field, because
2308    // `PrivatePropertyIdentifier` — the `#x` leaf — was in no operand
2309    // list and the composite `this.#x` had been its only count. Adding
2310    // the leaf also fixes the declaration site `#x = 1`, which no
2311    // wrapper covered and which therefore counted nothing at all.
2312    //
2313    // expected, for `class C { #x = 1; m() { return this.#x; } }`:
2314    //
2315    // * Operators: `class`, `{`×2 (class body, method body), `=`,
2316    //   `;`×2, `(`, `return`, `.` — 9 total, 7 unique. (`class` joined
2317    //   the JS-family operator arm in #1552.)
2318    // * Operands: `C`, `#x`, `1`, `m`, `this`, `#x` — 6 total, 5
2319    //   unique in all four grammars. Under TS/TSX the class *name* `C`
2320    //   is a `type_identifier`, which those getters did not classify
2321    //   until #1557, so they read 5/4 before it.
2322    #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2323    #[test]
2324    fn js_family_private_field_leaf_is_the_operand_1263() {
2325        #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2326        const SRC: &str = "class C { #x = 1; m() { return this.#x; } }";
2327        let check = |m: crate::CodeMetrics| {
2328            assert_eq!(m.halstead.unique_operators(), 7);
2329            assert_eq!(m.halstead.total_operators(), 9);
2330            assert_eq!(m.halstead.unique_operands(), 5);
2331            assert_eq!(m.halstead.total_operands(), 6);
2332        };
2333
2334        check_metrics::<JavascriptParser>(SRC, "foo.js", check);
2335        check_metrics::<MozjsParser>(SRC, "foo.js", check);
2336        check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2337        check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2338    }
2339
2340    // Issue #1263, the other section 6 half: `meta_property` is the one
2341    // composite the leaves-not-composites drop has to keep. `import.meta`
2342    // / `new.target` have no classified leaf — `meta` and `target` are
2343    // anonymous tokens in no arm — so with `MemberExpression*` gone the
2344    // meta-object contributed no operand at all while `this.env.x` still
2345    // yielded three.
2346    //
2347    // expected operands, for `var t = import.meta.url; function f() {
2348    // return new.target; }`: `t`, `import.meta`, `url`, `f`,
2349    // `new.target` — 5 total, 5 unique. Operators are deliberately not
2350    // asserted: the `import` / `new` keyword tokens inside the
2351    // meta-property keep their pre-existing operator classification,
2352    // which this fixture neither pins nor contests.
2353    #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2354    #[test]
2355    fn js_family_meta_property_is_one_operand_1263() {
2356        #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2357        const SRC: &str = "var t = import.meta.url; function f() { return new.target; }";
2358        let check = |m: crate::CodeMetrics| {
2359            assert_eq!(m.halstead.unique_operands(), 5);
2360            assert_eq!(m.halstead.total_operands(), 5);
2361        };
2362
2363        check_metrics::<JavascriptParser>(SRC, "foo.js", check);
2364        check_metrics::<MozjsParser>(SRC, "foo.js", check);
2365        check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2366        check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2367    }
2368
2369    // Issue #1263: TS/TSX `nested_identifier` (`namespace N.M`) is the
2370    // same container/leaf double-count as `member_expression`.
2371    //
2372    // expected, for `namespace N.M { }`:
2373    //
2374    // * Operators: `namespace`, `.`, `{` — 3 total, 3 unique.
2375    //   (`namespace` joined the TS operator extras in #1557.)
2376    // * Operands: `N`, `M` — 2 total, 2 unique. Before the fix the
2377    //   `nested_identifier` added `N.M`, making both 3.
2378    #[cfg(feature = "typescript")]
2379    #[test]
2380    fn ts_nested_identifier_counts_leaves_not_the_composite_1263() {
2381        const SRC: &str = "namespace N.M { }";
2382        let check = |m: crate::CodeMetrics| {
2383            assert_eq!(m.halstead.unique_operators(), 3);
2384            assert_eq!(m.halstead.total_operators(), 3);
2385            assert_eq!(m.halstead.unique_operands(), 2);
2386            assert_eq!(m.halstead.total_operands(), 2);
2387        };
2388
2389        check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2390        check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2391    }
2392
2393    // Issue #1261 (inverting the #313 pin): the `"string"` type-keyword
2394    // aliases the TS / TSX grammars expose must contribute NO operand.
2395    // #313 put them in `operand_extras` for parity with the then-wider
2396    // `Checker::is_string`, but the `predefined_type` wrapper already
2397    // counts as the text-keyed `"string"` operator, so one `: string`
2398    // token tallied as operator AND operand while `: number` counted
2399    // once. #1261 drops the aliases from both `operand_extras` and
2400    // `is_string`, so the keyword counts once, as the operator.
2401    //
2402    // For the input `let x: string = "y";`:
2403    //
2404    // * TypeScript emits `Typescript::String2` for the `string` type
2405    //   keyword (kind_id 135, in the type-keyword block of the enum).
2406    // * TSX emits `Tsx::String3` for the same role (kind_id 141).
2407    //
2408    // Verified by test-via-revert: restoring `String2` to TS's
2409    // `operand_extras` (or `String3` to TSX's) trips this test on
2410    // `u_operands` / `operands` for the affected language.
2411    #[cfg(feature = "typescript")]
2412    #[test]
2413    fn ts_family_type_keyword_counts_once_1261() {
2414        const SRC: &str = "let x: string = \"y\";";
2415        // Operators (n1 = 5, N1 = 5):
2416        //   `let`, `:`, `=`, `;`, plus `string` (PredefinedType wrapper,
2417        //   routed through `is_primitive` so it's keyed by its lexeme
2418        //   `"string"` in `primitive_operators`).
2419        // Operands (n2 = 2, N2 = 2):
2420        //   `x` and the `"y"` literal. Under #313 the type-keyword
2421        //   child of `predefined_type` added a third, phantom
2422        //   `"string"` operand (n2 = 3 / N2 = 3).
2423        let check = |m: crate::CodeMetrics| {
2424            assert_eq!(m.halstead.unique_operators(), 5);
2425            assert_eq!(m.halstead.total_operators(), 5);
2426            assert_eq!(m.halstead.unique_operands(), 2);
2427            assert_eq!(m.halstead.total_operands(), 2);
2428        };
2429
2430        check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2431        check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2432    }
2433
2434    // Issue #1261 regression, the issue's reproducer plus a literal
2435    // whose contents spell the keyword: `: string` and `: number` must
2436    // contribute symmetrically — one text-keyed operator each, zero
2437    // operands — while a string *literal* `"string"` stays an operand
2438    // (distinct from the keyword: TS kind `String`, TSX kind `String2`,
2439    // both quoted in the operand key).
2440    #[cfg(feature = "typescript")]
2441    #[test]
2442    fn ts_family_string_annotation_symmetric_with_number_1261() {
2443        const SRC: &str = "let x: string = \"a\";\nlet y: number = 1;\nlet s = \"string\";";
2444        // Operators (n1 = 6, N1 = 13):
2445        //   `let` ×3, `:` ×2, `=` ×3, `;` ×3, `string` ×1, `number` ×1.
2446        //   Pre-fix N1 was identical — the wrapper operator was always
2447        //   counted; the defect was the extra operand below.
2448        // Operands (n2 = 6, N2 = 6):
2449        //   `x`, `"a"`, `y`, `1`, `s`, `"string"` — one each. Pre-fix
2450        //   the `: string` keyword added a bare `string` operand
2451        //   (n2 = 7 / N2 = 7) that `: number` had no analogue of.
2452        let check = |m: crate::CodeMetrics| {
2453            assert_eq!(m.halstead.unique_operators(), 6);
2454            assert_eq!(m.halstead.total_operators(), 13);
2455            assert_eq!(m.halstead.unique_operands(), 6);
2456            assert_eq!(m.halstead.total_operands(), 6);
2457        };
2458
2459        check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2460        check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2461    }
2462
2463    /// Drift marker for #1261 (lesson 34 / grammar-dispatch §2): the
2464    /// anonymous `string` type-keyword token appears **only** as a
2465    /// `predefined_type` child.
2466    ///
2467    /// That is the whole argument for classifying the keyword without a
2468    /// parent guard — the wrapper is guaranteed to be there to carry the
2469    /// operator. The two tests above measure the consequence and would
2470    /// still pass if the grammar started emitting the keyword somewhere
2471    /// else, as long as their own two fixtures kept their counts; this one
2472    /// measures the premise, over every position #1261's dump probes
2473    /// covered: annotation, parameter and return type, union member,
2474    /// generic argument, and template-literal type. A string *literal*
2475    /// spelling `"string"` is a different kind and must not be confused
2476    /// for the keyword, so one is in the fixture too.
2477    #[cfg(feature = "typescript")]
2478    #[test]
2479    fn ts_family_type_keyword_only_appears_under_predefined_type_1261() {
2480        // Exercises each position the keyword can take. Valid in both
2481        // grammars: no angle-bracket cast, which TSX would read as JSX.
2482        const SRC: &str = "const a: string = \"string\";\n\
2483                           function f(x: string): string {\n\
2484                               return x;\n\
2485                           }\n\
2486                           type U = string | number;\n\
2487                           type A = Array<string>;\n\
2488                           type M = Map<string, number>;\n\
2489                           type T = `id-${string}`;\n";
2490        // Seven type positions: `a`, `x`, `f`'s return, the union member,
2491        // `Array`'s argument, `Map`'s first argument, and the template
2492        // placeholder. The `"string"` initialiser is a literal, not the
2493        // keyword, and must not be among them.
2494        const EXPECTED_OCCURRENCES: usize = 7;
2495
2496        fn keyword_occurrences<P: ParserTrait>(
2497            path: &str,
2498            keyword: u16,
2499            predefined_type: u16,
2500        ) -> usize {
2501            let parser = P::new(SRC.as_bytes().to_vec(), &PathBuf::from(path), None);
2502            parser
2503                .root()
2504                .preorder()
2505                .filter(|node| node.kind_id() == keyword)
2506                .inspect(|node| {
2507                    assert_eq!(
2508                        node.parent().map(|parent| parent.kind_id()),
2509                        Some(predefined_type),
2510                        "the `string` type keyword surfaced outside \
2511                         `predefined_type` in {path}; the operator is carried \
2512                         by the wrapper, so `get_op_type` needs a parent guard \
2513                         before that arm can be trusted (#1261)",
2514                    );
2515                })
2516                .count()
2517        }
2518
2519        // Kind ids re-read from the generated enums, not carried over: TS
2520        // `String2` = 135, TSX `String3` = 141 (TSX's `String2` = 261 is the
2521        // string-literal production and stays an operand).
2522        assert_eq!(
2523            keyword_occurrences::<TypescriptParser>(
2524                "foo.ts",
2525                Typescript::String2 as u16,
2526                Typescript::PredefinedType as u16,
2527            ),
2528            EXPECTED_OCCURRENCES,
2529            "TypeScript no longer emits the `string` type keyword in every \
2530             position #1261 probed",
2531        );
2532        assert_eq!(
2533            keyword_occurrences::<TsxParser>(
2534                "foo.tsx",
2535                Tsx::String3 as u16,
2536                Tsx::PredefinedType as u16,
2537            ),
2538            EXPECTED_OCCURRENCES,
2539            "TSX no longer emits the `string` type keyword in every position \
2540             #1261 probed",
2541        );
2542    }
2543
2544    // Issue #453: a `void` return type must contribute exactly one
2545    // Halstead operator. The TS / TSX grammars parse `: void` as a
2546    // `predefined_type` wrapper around an inner `void` token. `is_primitive`
2547    // routes the wrapper into the text-keyed `primitive_operators` map as
2548    // `"void"`, while the inner `Void` token is independently a standalone
2549    // expression operator (`void 0`). Pre-fix both classified as operators
2550    // and one source `void` counted as TWO distinct Halstead operators.
2551    // The fix suppresses the wrapper when its child is a `Void` token, so
2552    // only the inner token carries the operator — matching expression
2553    // `void 0` and keeping the kind_id-keyed count consistent.
2554    //
2555    // For `function f(): void { return; }`:
2556    //
2557    // * Operators (n1 = 7, N1 = 7): `function`, `()`, `{}`, `:`, `return`,
2558    //   `;`, and a single `void`. (The untyped form is n1 = 5; the `: void`
2559    //   annotation adds the `:` operator and one `void`, NOT two — the
2560    //   issue's "n1 = 6" target overlooked the annotation colon.)
2561    //
2562    // Verified by test-via-revert: removing the `predefined_void` guard
2563    // restores the pre-fix `u_operators` 7 -> 8 with a duplicate `"void"`
2564    // (one kind_id-keyed, one in `primitive_operators`). Both `metrics()`
2565    // and the `ops`-list dedup invariant (`ts_void_return_and_expression_*`
2566    // in `ops.rs`) are pinned per lesson 4.
2567    #[cfg(feature = "typescript")]
2568    #[test]
2569    fn ts_void_return_type_single_operator_453() {
2570        const SRC: &str = "function f(): void { return; }";
2571        let check = |m: crate::CodeMetrics| {
2572            assert_eq!(m.halstead.unique_operators(), 7);
2573            assert_eq!(m.halstead.total_operators(), 7);
2574        };
2575
2576        check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2577        check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2578    }
2579
2580    // Issue #453 over-suppression guard: expression `void 0` (a
2581    // `unary_expression`, NOT a `predefined_type` wrapper) must still
2582    // count `void` as exactly one operator. The fix keys only on a
2583    // `predefined_type` whose child is a `Void` token, so the bare
2584    // expression operator is untouched.
2585    //
2586    // For `const x = void 0;`:
2587    //
2588    // * Operators (n1 = 4, N1 = 4): `const`, `=`, `void`, `;`.
2589    // * Operands (n2 = 2, N2 = 2): `x`, `0`.
2590    #[cfg(feature = "typescript")]
2591    #[test]
2592    fn ts_void_expression_still_single_operator_453() {
2593        const SRC: &str = "const x = void 0;";
2594        let check = |m: crate::CodeMetrics| {
2595            assert_eq!(m.halstead.unique_operators(), 4);
2596            assert_eq!(m.halstead.total_operators(), 4);
2597            assert_eq!(m.halstead.unique_operands(), 2);
2598            assert_eq!(m.halstead.total_operands(), 2);
2599        };
2600
2601        check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2602        check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2603    }
2604
2605    #[cfg(feature = "python")]
2606    #[test]
2607    fn python_wrong_operators() {
2608        // Not valid Python — the parser recovers `()` as a tuple, the `[`
2609        // as a bare token and `{}` as a dict — but each opener is still a
2610        // delimiter the source spells, so since #1486 the three are
2611        // billed once each as `()`, `[]` and `{}`. The closers never are.
2612        check_metrics::<PythonParser>("()[]{}", "foo.py", |metric| {
2613            assert_eq!(metric.halstead.unique_operators(), 3);
2614            assert_eq!(metric.halstead.total_operators(), 3);
2615            insta::assert_json_snapshot!(
2616                metric.halstead,
2617                @r#"
2618            {
2619              "unique_operators": 3,
2620              "total_operators": 3,
2621              "unique_operands": 0,
2622              "total_operands": 0,
2623              "length": 3,
2624              "estimated_program_length": 4.754887502163468,
2625              "purity_ratio": 1.584962500721156,
2626              "vocabulary": 3,
2627              "volume": 4.754887502163468,
2628              "difficulty": 0.0,
2629              "level": 0.0,
2630              "effort": 0.0,
2631              "time": 0.0,
2632              "bugs": 0.0
2633            }
2634            "#
2635            );
2636        });
2637    }
2638
2639    #[cfg(feature = "python")]
2640    #[test]
2641    fn python_check_metrics() {
2642        check_metrics::<PythonParser>(
2643            "def f():
2644                 pass",
2645            "foo.py",
2646            |metric| {
2647                // expected operators: def, (), :, pass — one each (#1486
2648                // added the `()` and the block `:`).
2649                assert_eq!(metric.halstead.unique_operators(), 4);
2650                assert_eq!(metric.halstead.total_operators(), 4);
2651                insta::assert_json_snapshot!(
2652                    metric.halstead,
2653                    @r#"
2654                {
2655                  "unique_operators": 4,
2656                  "total_operators": 4,
2657                  "unique_operands": 1,
2658                  "total_operands": 1,
2659                  "length": 5,
2660                  "estimated_program_length": 8.0,
2661                  "purity_ratio": 1.6,
2662                  "vocabulary": 5,
2663                  "volume": 11.60964047443681,
2664                  "difficulty": 2.0,
2665                  "level": 0.5,
2666                  "effort": 23.21928094887362,
2667                  "time": 1.289960052715201,
2668                  "bugs": 0.002712967490108627
2669                }
2670                "#
2671                );
2672            },
2673        );
2674    }
2675
2676    #[cfg(feature = "java")]
2677    #[test]
2678    fn java_operators_and_operands() {
2679        check_metrics::<JavaParser>(
2680            "public class Main {
2681            public static void main(string args[]) {
2682                  int a, b, c, avg;
2683                  a = 5; b = 5; c = 5;
2684                  avg = (a + b + c) / 3;
2685                  MessageFormat.format(\"{0}\", avg);
2686                }
2687            }",
2688            "foo.java",
2689            |metric| {
2690                // Operators (n1=14): public static class {} void () [] , . ; int = + /
2691                // (`public` x2 and `static` since #1565)
2692                // Operands (n2=13): Main main string args a b c avg 5 3 MessageFormat format "{0}"
2693                // (`string` is the parameter's type name, an operand since #1560)
2694                assert_eq!(metric.halstead.unique_operators(), 14);
2695                assert_eq!(metric.halstead.total_operators(), 30);
2696                insta::assert_json_snapshot!(
2697                    metric.halstead,
2698                    @r#"
2699                {
2700                  "unique_operators": 14,
2701                  "total_operators": 30,
2702                  "unique_operands": 13,
2703                  "total_operands": 23,
2704                  "length": 53,
2705                  "estimated_program_length": 101.40868524464065,
2706                  "purity_ratio": 1.913371419710201,
2707                  "vocabulary": 27,
2708                  "volume": 252.0090376146638,
2709                  "difficulty": 12.384615384615385,
2710                  "level": 0.08074534161490683,
2711                  "effort": 3121.035004304683,
2712                  "time": 173.3908335724824,
2713                  "bugs": 0.07118871793971378
2714                }
2715                "#
2716                );
2717            },
2718        );
2719    }
2720
2721    #[cfg(feature = "java")]
2722    #[test]
2723    fn java_primitive_types_and_booleans() {
2724        check_metrics::<JavaParser>(
2725            "public class Prims {
2726                byte a = 1;
2727                short b = 2;
2728                int c = 3;
2729                long d = 4;
2730                char e = 'x';
2731                float f = 1.0f;
2732                double g = 2.0;
2733                boolean h = true;
2734                boolean i = false;
2735            }",
2736            "foo.java",
2737            |metric| {
2738                // Verifies all 8 Java primitive-type keywords (byte, short, int, long,
2739                // char, float, double, boolean) are counted as distinct operators, and
2740                // that true/false are counted as operands. The other five operators
2741                // are `public` (#1565), `class` (#1552), `{}`, `=` and `;`.
2742                assert_eq!(metric.halstead.unique_operators(), 13);
2743                insta::assert_json_snapshot!(
2744                    metric.halstead,
2745                    @r#"
2746                {
2747                  "unique_operators": 13,
2748                  "total_operators": 30,
2749                  "unique_operands": 19,
2750                  "total_operands": 19,
2751                  "length": 49,
2752                  "estimated_program_length": 128.8163390912623,
2753                  "purity_ratio": 2.6289048794135166,
2754                  "vocabulary": 32,
2755                  "volume": 245.0,
2756                  "difficulty": 6.5,
2757                  "level": 0.15384615384615385,
2758                  "effort": 1592.5,
2759                  "time": 88.47222222222223,
2760                  "bugs": 0.04545674902336332
2761                }
2762                "#
2763                );
2764            },
2765        );
2766    }
2767
2768    // #1380: `this` and `super` were swept into the operator arm under a
2769    // `// Operator: … keywords` heading — classification by lexical
2770    // class rather than by Halstead role. A member access is
2771    // `<receiver> <op> <field>`, so billing the receiver as an operator
2772    // scored `this.x` as a binary operator with one operand while `p.x`
2773    // is one operator and two operands. Both are operands now, matching
2774    // the eleven other languages that classify a self-reference.
2775    #[cfg(feature = "java")]
2776    #[test]
2777    fn java_self_and_super_references_are_operands() {
2778        let source = "class T {\n    int f() { return this.x + super.y; }\n}";
2779        // Operators (n1=8, N1=10): class (#1552), {} x2, int, (),
2780        // return, . x2, +, ;
2781        // Operands (n2=6, N2=6): T, f, this, x, super, y
2782        // Before the fix this read (10, 12, 4, 4) — `this` and `super`
2783        // billed into the operator side of both counts.
2784        assert_halstead_counts::<JavaParser>(
2785            source,
2786            "foo.java",
2787            [8, 10, 6, 6],
2788            "java self/super references",
2789        );
2790        assert_keywords_are_operands_only::<JavaParser>(source, "foo.java", &["this", "super"]);
2791    }
2792
2793    // The `super` of a wildcard type bound denotes no value, so it keeps
2794    // the operator classification `? extends T`'s `extends` already has
2795    // — the two keywords are siblings under the same `wildcard` node and
2796    // nothing but the parent separates them from a real self-reference
2797    // (#1380). The explicit receiver parameter in the same fixture is
2798    // the independent path through the operand arm (grammar-dispatch
2799    // section 11): only it can put `this` in the operand vocabulary
2800    // here, and only the wildcard can put `super` in the operator one.
2801    #[cfg(feature = "java")]
2802    #[test]
2803    fn java_wildcard_super_bound_stays_an_operator() {
2804        let source = "import java.util.List;\n\
2805                      class T {\n    \
2806                      void m(T T.this, List<? super String> a, List<? extends String> b) { }\n\
2807                      }";
2808        let ops = ops_of::<JavaParser>(source, "foo.java");
2809
2810        assert!(
2811            ops.operators.iter().any(|o| o == "super"),
2812            "`? super String` must keep `super` an operator; operators were {:?}",
2813            ops.operators
2814        );
2815        assert!(
2816            !ops.operands.iter().any(|o| o == "super"),
2817            "a wildcard bound must not bill `super` as an operand; operands were {:?}",
2818            ops.operands
2819        );
2820        assert!(
2821            ops.operators.iter().any(|o| o == "extends"),
2822            "the fixture must still contain the `? extends String` \
2823             bound this arm mirrors; operators were {:?}",
2824            ops.operators
2825        );
2826        assert_keywords_are_operands_only::<JavaParser>(source, "foo.java", &["this"]);
2827    }
2828
2829    /// One `this` and one `super` per container the pinned
2830    /// `tree-sitter-java` can put them in: a delegating constructor
2831    /// call, an explicit superclass constructor call, a field access, a
2832    /// method-invocation receiver, a method reference, a call argument,
2833    /// and the two qualified forms an inner class allows.
2834    #[cfg(feature = "java")]
2835    const JAVA_SELF_POSITIONS: &str = "class Pos extends P {
2836        int x;
2837        Pos() { this(1); }
2838        Pos(int v) { super(v); this.x = v; }
2839        int a() { return this.x; }
2840        int b() { return super.h(); }
2841        java.util.function.IntSupplier c() { return this::a; }
2842        void d() { g(this); }
2843        void g(Object o) { }
2844        class In {
2845            int e() { return Pos.this.x; }
2846            int f() { return Pos.super.h(); }
2847        }
2848    }";
2849
2850    #[cfg(feature = "java")]
2851    #[test]
2852    fn java_self_and_super_leaves_are_unaliased_and_unconditional() {
2853        assert_self_reference_leaves::<JavaCode>(
2854            JAVA_SELF_POSITIONS,
2855            &[
2856                ("this", Java::This as u16, 6),
2857                ("super", Java::Super as u16, 3),
2858            ],
2859            "java",
2860        );
2861    }
2862
2863    // Groovy's `Super` arm, pinned from all three sides. The pinned
2864    // grammar emits the `super` token only as a `wildcard` bound, where it
2865    // is an operator exactly as in Java above; every super-*reference* is
2866    // a plain `identifier`, and so an operand.
2867    //
2868    // Since #1419 the arm carries Java's `Wildcard` parent gate, so the
2869    // bound half below exercises the gate's operator branch — inverting
2870    // the gate fails it. The gate's operand branch is unreachable under
2871    // the pin (only error recovery on invalid Groovy, `List<? super>`,
2872    // detaches a `super` token from its `wildcard`), so it is
2873    // deliberately untested rather than pinned against an invalid
2874    // fixture. The node census is the drift marker for it: a grammar bump
2875    // that routes a reference to `Groovy::Super` fails here by name.
2876    #[cfg(feature = "groovy")]
2877    #[test]
2878    fn groovy_wildcard_super_bound_stays_an_operator() {
2879        let bounds =
2880            "class T {\n    void m(List<? super String> a, List<? extends String> b) { }\n}";
2881        let ops = ops_of::<GroovyParser>(bounds, "foo.groovy");
2882        assert!(
2883            ops.operators.iter().any(|o| o == "super"),
2884            "`? super String` must keep `super` an operator; operators were {:?}",
2885            ops.operators
2886        );
2887        assert!(
2888            !ops.operands.iter().any(|o| o == "super"),
2889            "a wildcard bound must not bill `super` as an operand; operands were {:?}",
2890            ops.operands
2891        );
2892        assert!(
2893            ops.operators.iter().any(|o| o == "extends"),
2894            "the fixture must still contain the `? extends String` bound this arm \
2895             mirrors; operators were {:?}",
2896            ops.operators
2897        );
2898
2899        let mixed = "class T extends B {\n    void m(List<? super String> a) { super.h() }\n}";
2900        let mut tokens = 0;
2901        for_each_node_with_chain::<GroovyCode>(mixed.as_bytes(), |node, chain| {
2902            if node.kind_id() == Groovy::Super as u16 {
2903                tokens += 1;
2904                assert_eq!(
2905                    chain.last().map(Node::kind_id),
2906                    Some(Groovy::Wildcard as u16),
2907                    "groovy: a `super` token outside a wildcard bound now reaches \
2908                     the gate's operand branch, which the pinned grammar made \
2909                     unreachable — re-check the arm against the new grammar (#1419)"
2910                );
2911            }
2912        });
2913        assert_eq!(
2914            tokens, 1,
2915            "the fixture's one wildcard bound must still spell `super`, and its \
2916             `super.h()` reference must not"
2917        );
2918
2919        // The reference direction, on valid input the gate must leave
2920        // alone: every super-reference form the grammar accepts stays an
2921        // operand, so splitting `Super` out of the operator alternation
2922        // cannot have moved one. `this` rides along for the same reason
2923        // Java's test carries it.
2924        let refs = "class A extends B {\n    A() { super(1) }\n    \
2925                    def g() { return super.h() }\n    \
2926                    def i() { return A.super.h() }\n    \
2927                    def j() { return super::h }\n    \
2928                    def k() { return super?.h() }\n    \
2929                    def l() { return this.x }\n}";
2930        assert_keywords_are_operands_only::<GroovyParser>(refs, "foo.groovy", &["super", "this"]);
2931    }
2932
2933    #[cfg(feature = "groovy")]
2934    #[test]
2935    fn groovy_operators_and_operands() {
2936        check_metrics::<GroovyParser>(
2937            "class Main {
2938                static void main(String[] args) {
2939                    int a, b, c, avg;
2940                    a = 5; b = 5; c = 5;
2941                    avg = (a + b + c) / 3;
2942                    println(avg);
2943                }
2944            }",
2945            "foo.groovy",
2946            |metric| {
2947                // Groovy mirror of `java_operators_and_operands`. The juxt
2948                // call `println avg` exercises `juxt_function_call` in
2949                // place of Java's `MessageFormat.format(...)`. amaanq's
2950                // grammar inherits Java's tokenisation, so n1/N1/n2/N2
2951                // shapes match Java up to those substitutions.
2952                // The dekobon grammar parses the primitive type names
2953                // `void` and `int` as `type_identifier`, the kind of
2954                // `String`; since #1568 their spelling bills them as
2955                // operators, as Java's keyword tokens are, while
2956                // `String` stays an operand.
2957                // `class` is an operator since #1552, `static` since
2958                // #1565, and `void` and `int` since #1568.
2959                assert_eq!(metric.halstead.unique_operators(), 12);
2960                assert_eq!(metric.halstead.unique_operands(), 11);
2961                // `bugs` is `effort.powf(2/3) / 3000`, and the platform libm's
2962                // `powf` differs in the last ulp: macOS rendered this value
2963                // as `…92342184` where Linux and Windows gave `…923421845`.
2964                // Round it so the snapshot pins the metric, not the libm.
2965                insta::assert_json_snapshot!(
2966                    metric.halstead,
2967                    { ".bugs" => insta::rounded_redaction(12) },
2968                    @r#"
2969                {
2970                  "unique_operators": 12,
2971                  "total_operators": 26,
2972                  "unique_operands": 11,
2973                  "total_operands": 21,
2974                  "length": 47,
2975                  "estimated_program_length": 81.07329781366414,
2976                  "purity_ratio": 1.7249637832694498,
2977                  "vocabulary": 23,
2978                  "volume": 212.60741193467962,
2979                  "difficulty": 11.454545454545455,
2980                  "level": 0.0873015873015873,
2981                  "effort": 2435.3212639790577,
2982                  "time": 135.29562577661432,
2983                  "bugs": 0.06033688972
2984                }
2985                "#
2986                );
2987            },
2988        );
2989    }
2990
2991    #[cfg(feature = "groovy")]
2992    #[test]
2993    fn groovy_primitive_types_and_booleans() {
2994        check_metrics::<GroovyParser>(
2995            "class Prims {
2996                byte a = 1
2997                short b = 2
2998                int c = 3
2999                long d = 4
3000                char e = 'x'
3001                float f = 1.0f
3002                double g = 2.0
3003                boolean h = true
3004                boolean i = false
3005            }",
3006            "foo.groovy",
3007            |metric| {
3008                // The dekobon grammar consolidates the 8 primitive
3009                // type names (`byte`, `short`, `int`, `long`, `char`,
3010                // `float`, `double`, `boolean`) under `type_identifier`;
3011                // since #1568 each bills as a distinct operator keyed by
3012                // its text, as Java's `java_primitive_ops` twin does.
3013                // Numeric literals collapse to one `NumberLiteral` shape
3014                // (no Hex/Octal/Binary/Decimal split), and `'x'` parses
3015                // as `StringLiteral` (Groovy single-quoted strings)
3016                // rather than as `CharacterLiteral`.
3017                // expected: operators `=` x9, `class`, `{}` and the eight
3018                // primitives (`boolean` twice) — n1 11, N1 20; operands
3019                // `Prims`, the nine names and nine values — n2 19, N2 19.
3020                assert_eq!(metric.halstead.unique_operators(), 11);
3021                assert_eq!(metric.halstead.total_operators(), 20);
3022                assert_eq!(metric.halstead.unique_operands(), 19);
3023                assert_eq!(metric.halstead.total_operands(), 19);
3024                insta::assert_json_snapshot!(
3025                    metric.halstead,
3026                    @r#"
3027                {
3028                  "unique_operators": 11,
3029                  "total_operators": 20,
3030                  "unique_operands": 19,
3031                  "total_operands": 19,
3032                  "length": 39,
3033                  "estimated_program_length": 118.76437056043838,
3034                  "purity_ratio": 3.045240270780471,
3035                  "vocabulary": 30,
3036                  "volume": 191.36873322873222,
3037                  "difficulty": 5.5,
3038                  "level": 0.18181818181818182,
3039                  "effort": 1052.5280327580272,
3040                  "time": 58.47377959766818,
3041                  "bugs": 0.0344906352248725
3042                }
3043                "#
3044                );
3045            },
3046        );
3047    }
3048
3049    // Issue #1263 swept Groovy alongside the JS family and C#: its
3050    // operand arm listed `QualifiedName` (a `package` / `import` path)
3051    // and `QualifiedType` on top of the identifier leaves the walker
3052    // already reached.
3053    //
3054    // Only the `QualifiedName` half was observable. The runtime emits
3055    // `qualified_type` as the *alias* `QualifiedType2` (kind_id 228),
3056    // which the arm never named — a lesson-2 miss that, by accident,
3057    // made that half already leaves-only and is why #1263's issue body
3058    // recorded Groovy as compliant. Both kinds are gone rather than
3059    // completed.
3060    //
3061    // expected, for `package com.example`: operators `.` (1/1);
3062    // operands `com`, `example` (2/2). Pre-fix the `qualified_name`
3063    // added `com.example`, making the operand counts 3/3.
3064    #[cfg(feature = "groovy")]
3065    #[test]
3066    fn groovy_qualified_name_counts_leaves_not_the_composite_1263() {
3067        check_metrics::<GroovyParser>("package com.example", "foo.groovy", |metric| {
3068            assert_eq!(metric.halstead.unique_operators(), 1);
3069            assert_eq!(metric.halstead.total_operators(), 1);
3070            assert_eq!(metric.halstead.unique_operands(), 2);
3071            assert_eq!(metric.halstead.total_operands(), 2);
3072        });
3073    }
3074
3075    // The type half of the same arm (#1352). #1263 dropped
3076    // `QualifiedType` alongside `QualifiedName`, but only the latter
3077    // was pinned: adding `QualifiedType2` back to the operand arm
3078    // failed none of the 3,523 tests then in the lib targets, so the
3079    // leaves-only reading of a qualified *type* was correct by accident
3080    // rather than by contract. The tempting "fix" for the alias miss
3081    // #1263 recorded is to complete the list with 228, which is exactly
3082    // the double count #1263 removed — this test is what stops that.
3083    //
3084    // The kind assertions are the grammar-dispatch section 1 / 2 drift
3085    // marker: if a grammar bump renumbers the alias, the operand
3086    // assertions below would keep passing while measuring a construct
3087    // this arm no longer describes.
3088    //
3089    // expected, for `java.util.List x = null`: operators `.` × 2 and
3090    // `=` → n1 = 2, N1 = 3; operands `java`, `util`, `List`, `x`,
3091    // `null` → n2 = 5, N2 = 5. Listing the wrapper would add the whole
3092    // span `java.util.List`, making the operand counts 6/6.
3093    #[cfg(feature = "groovy")]
3094    #[test]
3095    fn groovy_qualified_type_counts_leaves_not_the_composite_1352() {
3096        const SOURCE: &str = "java.util.List x = null";
3097
3098        let parser = GroovyParser::new(
3099            SOURCE.as_bytes().to_vec(),
3100            &PathBuf::from("foo.groovy"),
3101            None,
3102        );
3103        assert!(
3104            ast_has_kind_id(&parser, Groovy::QualifiedType2 as u16),
3105            "the dekobon grammar no longer emits `qualified_type` as the \
3106             alias `QualifiedType2`; re-derive the Groovy operand arm \
3107             before trusting the counts below",
3108        );
3109        assert!(
3110            !ast_has_kind_id(&parser, Groovy::QualifiedType as u16),
3111            "the unsuffixed `QualifiedType` is now reachable; it is a \
3112             second wrapper this arm must keep excluded",
3113        );
3114
3115        check_metrics::<GroovyParser>(SOURCE, "foo.groovy", |metric| {
3116            assert_eq!(metric.halstead.unique_operators(), 2);
3117            assert_eq!(metric.halstead.total_operators(), 3);
3118            assert_eq!(metric.halstead.unique_operands(), 5);
3119            assert_eq!(metric.halstead.total_operands(), 5);
3120        });
3121    }
3122
3123    #[cfg(feature = "groovy")]
3124    #[test]
3125    fn groovy_closure_operators_and_operands() {
3126        // `twice`, not the original `double`: Groovy reserves the
3127        // primitive type names, so `def double` is not valid Groovy and
3128        // `double` would bill as the primitive operator (#1568).
3129        check_metrics::<GroovyParser>("def twice = { x -> x * 2 }", "foo.groovy", |metric| {
3130            // Closure with arrow-style parameter list.
3131            // Distinct operators: def, =, {}, ->, * = 5.
3132            // Distinct operands: twice, x, 2 = 3.
3133            assert_eq!(metric.halstead.unique_operators(), 5);
3134            assert_eq!(metric.halstead.unique_operands(), 3);
3135        });
3136    }
3137
3138    /// Regression for issue #247: every Groovy-specific operator the
3139    /// prior amaanq grammar dropped to ERROR or mis-shaped as a Java
3140    /// node now parses as a distinct lexer token in the dekobon
3141    /// grammar, so Halstead counts each one. The fixture below
3142    /// exercises Elvis `?:`, safe-nav `?.`, safe-chain `??.`,
3143    /// spread-dot `*.`, method-pointer `.&`, direct-field `.@`,
3144    /// identity `===` / `!==`, spaceship `<=>`, regex `=~` / `==~`,
3145    /// exclusive ranges `..<` / `<..` / `<..<`, `as` coercion, and
3146    /// `?[` safe index — every distinct operator kind must appear in
3147    /// `u_operators` (the count grows by exactly the number of new
3148    /// distinct operator tokens introduced).
3149    #[cfg(feature = "groovy")]
3150    #[test]
3151    fn groovy_dekobon_operator_coverage_247() {
3152        check_metrics::<GroovyParser>(
3153            "def f(a, b, list, s) {
3154                def x = a ?: b
3155                def y = a?.field
3156                def z = a??.field
3157                def items = list*.size()
3158                def ptr = a.&size
3159                def fld = a.@field
3160                def id1 = a === b
3161                def id2 = a !== b
3162                def ship = a <=> b
3163                def find = s =~ /pat/
3164                def match = s ==~ /^pat\\$/
3165                def r1 = 0..<10
3166                def r2 = 0<..10
3167                def r3 = 0<..<10
3168                def cast = a as String
3169                def safe = list?[0]
3170                return x
3171            }",
3172            "foo.groovy",
3173            |metric| {
3174                // Exact pin: with the dekobon Groovy grammar this
3175                // fixture exercises 16 Groovy-specific tokens (`?:`,
3176                // `?.`, `??.`, `*.`, `.&`, `.@`, `===`, `!==`, `<=>`,
3177                // `=~`, `==~`, `..<`, `<..`, `<..<`, `as`, `?[`) plus
3178                // 6 ambient Java-shaped operators the fixture also
3179                // uses (`def`, `=`, `,`, `{}`, `()`, `return`), for a
3180                // total of 22 distinct operator kinds. A regression
3181                // that drops any one of the 16 #247 operators would
3182                // push the count below 22 and fail this assertion. The
3183                // complementary AST walk below pins each #247
3184                // operator's identity individually so a grammar change
3185                // that adds an unrelated operator (lifting
3186                // `u_operators` to 23) still flags the loss of a #247
3187                // operator at the per-token level.
3188                //
3189                // Was 23 until #1314. The extra entry was a `/` — the
3190                // fixture's two slashy literals (`/pat/`, `/^pat\$/`)
3191                // each spelled their closing delimiter with the
3192                // division kind, and the arm now guards them. The
3193                // enumeration above was wrong in two ways at that
3194                // count: it listed an ambient `[`, which this fixture
3195                // never emits (`list?[0]` is the single `?[` token),
3196                // and omitted the fabricated `/` that made up the
3197                // difference. Both are corrected here.
3198                assert_eq!(
3199                    metric.halstead.unique_operators(),
3200                    22,
3201                    "u_operators changed; check whether a #247 operator was dropped or an unrelated operator added (and update the comment / token list above accordingly)",
3202                );
3203            },
3204        );
3205    }
3206
3207    #[cfg(feature = "groovy")]
3208    #[test]
3209    fn groovy_gstring_no_double_count() {
3210        // Issue #454: before the fix Groovy had no interpolation guard
3211        // at all — `StringLiteral` was classified as a plain operand, so
3212        // a GString counted the wrapping literal AND descended into its
3213        // interpolated expression, double-counting the inner identifier
3214        // in N2. The fix routes `StringLiteral` through
3215        // `string_operand_type` with both GString interpolation child
3216        // kinds (`gstring_brace_interpolation` / `gstring_dollar_-
3217        // interpolation`), so the wrapper is Unknown and only the inner
3218        // expression contributes.
3219        //
3220        // `def greet(name) {\n  return "Hi ${name}"\n}\n`
3221        //   operands by token text: `greet` × 1, `name` × 2 (param +
3222        //   inside `${name}`). The wrapping `"Hi ${name}"` is suppressed
3223        //   → u_operands = 2 (`greet`, `name`), N2 = 3. Without the fix
3224        //   the wrapping literal would also count → u_operands = 3,
3225        //   N2 = 4.
3226        let src = "def greet(name) {\n  return \"Hi ${name}\"\n}\n";
3227        check_metrics::<GroovyParser>(src, "foo.groovy", |metric| {
3228            assert_eq!(metric.halstead.unique_operands(), 2);
3229            assert_eq!(metric.halstead.total_operands(), 3);
3230        });
3231        assert_ops_operands::<GroovyParser>(src, "foo.groovy", 2, vec!["greet", "name"]);
3232    }
3233
3234    #[cfg(feature = "groovy")]
3235    #[test]
3236    fn groovy_gstring_dollar_form_no_double_count() {
3237        // Issue #454: the short `$name` GString form emits a distinct
3238        // `gstring_dollar_interpolation` child whose inner `identifier`
3239        // text is `$name` (the grammar's identifier node spans the
3240        // leading `$`). The wrapper is suppressed; the inner `$name`
3241        // operand is distinct from the bare `name` param.
3242        //
3243        // `def greet(name) {\n  return "Hi $name"\n}\n`
3244        //   operands: `greet`, `name` (param), `$name` (interp) →
3245        //   u_operands = 3, N2 = 3. Without the fix the wrapping
3246        //   `"Hi $name"` would also count → u_operands = 4, N2 = 4.
3247        let src = "def greet(name) {\n  return \"Hi $name\"\n}\n";
3248        check_metrics::<GroovyParser>(src, "foo.groovy", |metric| {
3249            assert_eq!(metric.halstead.unique_operands(), 3);
3250            assert_eq!(metric.halstead.total_operands(), 3);
3251        });
3252        assert_ops_operands::<GroovyParser>(src, "foo.groovy", 3, vec!["greet", "name", "$name"]);
3253    }
3254
3255    // `Groovy::Identifier2` (141) is the grammar's external token
3256    // `_gstring_dollar_path` — the `$a.b` path of a dollar interpolation —
3257    // whose public symbol is `identifier`, so the parser reports it as
3258    // the unsuffixed `Identifier` and never as 141 (#1475). That is why
3259    // the operand arm lists `Identifier` but not `Identifier2`; if a
3260    // grammar bump starts emitting 141, the dotted path drops out of N2
3261    // with no error.
3262    //
3263    // expected: operands `greet`, `name` (param), `$name.b` (the whole
3264    // path, one node) → n2 = 3, N2 = 3.
3265    #[cfg(feature = "groovy")]
3266    #[test]
3267    fn groovy_gstring_dollar_path_is_the_public_identifier() {
3268        let src = "def greet(name) {\n  return \"Hi $name.b\"\n}\n";
3269        let parser = GroovyParser::new(src.as_bytes().to_vec(), &PathBuf::from("foo.groovy"), None);
3270        assert!(
3271            ast_has_kind_id(&parser, Groovy::GstringDollarInterpolation as u16),
3272            "the fixture no longer spells a dollar interpolation",
3273        );
3274        assert!(
3275            !ast_has_kind_id(&parser, Groovy::Identifier2 as u16),
3276            "the hidden `_gstring_dollar_path` token is now emitted as \
3277             `Identifier2`; add it to the Groovy operand arm",
3278        );
3279        check_metrics::<GroovyParser>(src, "foo.groovy", |metric| {
3280            assert_eq!(metric.halstead.unique_operands(), 3);
3281            assert_eq!(metric.halstead.total_operands(), 3);
3282        });
3283        assert_ops_operands::<GroovyParser>(src, "foo.groovy", 3, vec!["greet", "name", "$name.b"]);
3284    }
3285
3286    #[cfg(feature = "groovy")]
3287    #[test]
3288    fn groovy_plain_string_still_operand() {
3289        // Counterpart to `groovy_gstring_no_double_count`: a plain
3290        // non-interpolated literal has neither GString interpolation
3291        // child and must still contribute exactly one operand.
3292        //
3293        // `def f() {\n  return "plain"\n}\n`
3294        //   operands: `f`, `"plain"` → u_operands = 2, N2 = 2.
3295        let src = "def f() {\n  return \"plain\"\n}\n";
3296        check_metrics::<GroovyParser>(src, "foo.groovy", |metric| {
3297            assert_eq!(metric.halstead.unique_operands(), 2);
3298            assert_eq!(metric.halstead.total_operands(), 2);
3299        });
3300        assert_ops_operands::<GroovyParser>(src, "foo.groovy", 2, vec!["f", "\"plain\""]);
3301    }
3302
3303    #[cfg(feature = "groovy")]
3304    #[test]
3305    fn groovy_slashy_string_delimiter_is_not_an_operator() {
3306        // Regression: issue #1314, the Groovy sibling of Elixir #1256
3307        // and Ruby/Perl #1312. A slashy string is a `StringLiteral`
3308        // whose closing delimiter is a `SLASH` — the kind id real
3309        // division uses — so `def b = /xyz/` reported a `/` operator
3310        // with no division in the source. Only the closer is a child
3311        // (the grammar folds the opening `/` into the literal's span),
3312        // so this fabricated one `/` per literal rather than Ruby's two.
3313        //
3314        // expected: operators `def` × 3, `=` × 3 → n1 = 2, N1 = 6.
3315        // Operands `b`, `/xyz/` × 2, `c`, `s` → n2 = 4, N2 = 6. Before
3316        // the guard the two closers added `/` → n1 = 3, N1 = 8.
3317        check_metrics::<GroovyParser>(
3318            "def b = /xyz/\ndef c = /xyz/\ndef s = b\n",
3319            "foo.groovy",
3320            |metric| {
3321                assert_eq!(metric.halstead.unique_operators(), 2);
3322                assert_eq!(metric.halstead.total_operators(), 6);
3323                assert_eq!(metric.halstead.unique_operands(), 4);
3324                assert_eq!(metric.halstead.total_operands(), 6);
3325            },
3326        );
3327    }
3328
3329    #[cfg(feature = "groovy")]
3330    #[test]
3331    fn groovy_division_survives_the_slashy_guard() {
3332        // Control for #1314: the guard is scoped to a `StringLiteral`
3333        // parent, so real division must still count. Both sides are in
3334        // one fixture — two divisions and one slashy literal — so a
3335        // guard widened to every `SLASH` fails here rather than
3336        // silently passing the test above.
3337        //
3338        // expected: operators `def` × 2, `=` × 2, `/` × 2 → n1 = 3,
3339        // N1 = 6. Operands `q`, `a`, `b`, `c`, `r`, `/x/` → n2 = N2 = 6.
3340        check_metrics::<GroovyParser>("def q = a / b / c\ndef r = /x/\n", "foo.groovy", |metric| {
3341            assert_eq!(metric.halstead.unique_operators(), 3);
3342            assert_eq!(metric.halstead.total_operators(), 6);
3343            assert_eq!(metric.halstead.unique_operands(), 6);
3344            assert_eq!(metric.halstead.total_operands(), 6);
3345        });
3346    }
3347
3348    #[cfg(feature = "groovy")]
3349    #[test]
3350    fn groovy_slashy_guard_is_parent_scoped_not_ancestor_scoped() {
3351        // The input that separates the parent-scoped guard from the
3352        // ancestor-scanning mutant of it — the mutant #1256's
3353        // post-mortem says survives every ordinary fixture. Groovy is
3354        // one of only two languages in #1314 where such an input
3355        // exists at all: a slashy string may carry a GString
3356        // interpolation, so `/x${a / b}y/` puts a real division under a
3357        // `StringLiteral` *ancestor* while its parent is the
3358        // `binary_expression`. An ancestor scan swallows it; the parent
3359        // check leaves it alone. (The JS, C++ and Tcl/iRules guards
3360        // have no such input — see
3361        // `js_regex_delimiter_guard_is_parent_scoped_is_unobservable`.)
3362        //
3363        // expected: operators `def` × 2, `=` × 2, `/` (the
3364        // interpolated division) → n1 = 3, N1 = 5. The wrapping literal
3365        // is not an operand — it carries an interpolation, so
3366        // `string_operand_type` yields `Unknown` and the inner
3367        // expression's operands carry the count (#454) — leaving `r`,
3368        // `a`, `b`, `s` → n2 = 4, with `a` twice → N2 = 5. Under the
3369        // ancestor-scoped mutant the division vanishes: n1 = 2, N1 = 4.
3370        check_metrics::<GroovyParser>(
3371            "def r = /x${a / b}y/\ndef s = a\n",
3372            "foo.groovy",
3373            |metric| {
3374                assert_eq!(metric.halstead.unique_operators(), 3);
3375                assert_eq!(metric.halstead.total_operators(), 5);
3376                assert_eq!(metric.halstead.unique_operands(), 4);
3377                assert_eq!(metric.halstead.total_operands(), 5);
3378            },
3379        );
3380    }
3381
3382    #[cfg(feature = "groovy")]
3383    #[test]
3384    fn groovy_every_string_spelling_scores_alike() {
3385        // Companion to the three above (#1314). Groovy has five ways to
3386        // write an inert one-character string, and the choice is
3387        // spelling rather than semantics, so all five must score
3388        // identically. Before the guard the two slashy forms reported
3389        // an extra `/` operator that the other three did not — the
3390        // author's delimiter choice moved n1/N1.
3391        //
3392        // The dollar-slashy and quoted forms are no-change controls:
3393        // `$/…/$` closes with `/$` (kind 144) and `"…"` with `"` (134),
3394        // neither of which the operator arm classifies. `'x'` is a
3395        // childless leaf. The escaped-slash row is the one that would
3396        // regress if the guard were ever narrowed to a literal whose
3397        // *only* child is the closer.
3398        //
3399        // expected per spelling: operators `def` × 3, `=` × 3 → n1 = 2,
3400        // N1 = 6; operands `a`, the literal, `b`, `c` → n2 = 4, with
3401        // `a` used three times → N2 = 6.
3402        for literal in ["/x/", "$/x/$", "'x'", "\"x\"", r"/esc\/aped/"] {
3403            assert_halstead_counts::<GroovyParser>(
3404                &format!("def a = {literal}\ndef b = a\ndef c = a\n"),
3405                "foo.groovy",
3406                [2, 6, 4, 6],
3407                &format!("literal {literal}"),
3408            );
3409        }
3410    }
3411
3412    #[cfg(feature = "csharp")]
3413    #[test]
3414    fn csharp_operators_and_operands() {
3415        // After issue #286, `void`, `string`, and `int` count as three
3416        // distinct Halstead operators rather than collapsing into one
3417        // `PredefinedType` kind_id entry, lifting u_operators from 13
3418        // to 15. Total operators (N1) is unchanged because the same
3419        // nodes are still counted, just keyed by lexeme.
3420        check_metrics::<CsharpParser>(
3421            "public class Main {
3422                public static void Run(string[] args) {
3423                    int a, b, c, avg;
3424                    a = 5; b = 5; c = 5;
3425                    avg = (a + b + c) / 3;
3426                    System.Console.WriteLine(\"{0}\", avg);
3427                }
3428            }",
3429            "foo.cs",
3430            |metric| {
3431                assert_eq!(metric.halstead.unique_operators(), 15);
3432                assert_eq!(metric.halstead.total_operators(), 32);
3433                assert_eq!(metric.halstead.unique_operands(), 13);
3434                assert_eq!(metric.halstead.total_operands(), 23);
3435                // Pin every Halstead field; values are whatever the
3436                // classifier produces and become the regression spec.
3437                insta::assert_json_snapshot!(metric.halstead);
3438            },
3439        );
3440    }
3441
3442    // Issue #1263: C#'s three name *containers* — `qualified_name`
3443    // (`System.Text`), `generic_name` (`List<int>`) and
3444    // `alias_qualified_name` (`global::Foo`) — were operands alongside
3445    // every leaf the walker already reached, so one occurrence of each
3446    // billed twice. `member_access_expression` never was, which is why
3447    // `csharp_operators_and_operands`' `System.Console.WriteLine` is
3448    // unaffected by this change: the bug lived in the *name* grammar,
3449    // not in member access.
3450    //
3451    // Three fixtures rather than one, so a regression names which
3452    // container came back. Each is hand-tallied; the removed composite
3453    // is called out per case.
3454    #[cfg(feature = "csharp")]
3455    #[test]
3456    fn csharp_name_containers_count_leaves_not_the_composite_1263() {
3457        // expected: operators `using`, `.`, `;` (3/3); operands
3458        // `System`, `Text` (2/2). Pre-fix the `qualified_name` added
3459        // `System.Text`, making the operand counts 3/3.
3460        check_metrics::<CsharpParser>("using System.Text;", "foo.cs", |metric| {
3461            assert_eq!(metric.halstead.unique_operators(), 3);
3462            assert_eq!(metric.halstead.total_operators(), 3);
3463            assert_eq!(metric.halstead.unique_operands(), 2);
3464            assert_eq!(metric.halstead.total_operands(), 2);
3465        });
3466
3467        // expected: operators `class`, `{`×2, `void`, `(`, `;`, `<>`,
3468        // `int` — 8 total, 7 unique (`int` is the text-keyed primitive
3469        // operator, per #286; `<>` is the type-argument pair, #1559). Operands `C`, `M`, `List`, `l`
3470        // — 4/4. Pre-fix the `generic_name` added `List<int>`, making
3471        // them 5/5.
3472        check_metrics::<CsharpParser>(
3473            "class C { void M() { List<int> l; } }",
3474            "foo.cs",
3475            |metric| {
3476                assert_eq!(metric.halstead.unique_operators(), 7);
3477                assert_eq!(metric.halstead.total_operators(), 8);
3478                assert_eq!(metric.halstead.unique_operands(), 4);
3479                assert_eq!(metric.halstead.total_operands(), 4);
3480            },
3481        );
3482
3483        // expected: operators `class`, `{`×2, `void`, `(`, `;`, `=`,
3484        // `::`, `.` — 9 total, 8 unique. `var` has no operator arm.
3485        // Operands `C`, `M`, `x`, `global`, `Foo`, `Bar` — 6/6. Pre-fix
3486        // the `alias_qualified_name` added `global::Foo`, making them
3487        // 7/7. The `::` staying an operator is what makes the leaf-only
3488        // tally lossless here, so it is asserted by the operator count
3489        // rather than assumed.
3490        check_metrics::<CsharpParser>(
3491            "class C { void M() { var x = global::Foo.Bar; } }",
3492            "foo.cs",
3493            |metric| {
3494                assert_eq!(metric.halstead.unique_operators(), 8);
3495                assert_eq!(metric.halstead.total_operators(), 9);
3496                assert_eq!(metric.halstead.unique_operands(), 6);
3497                assert_eq!(metric.halstead.total_operands(), 6);
3498            },
3499        );
3500    }
3501
3502    #[cfg(feature = "csharp")]
3503    #[test]
3504    fn csharp_primitive_types_and_booleans() {
3505        // After issue #286: each of `byte`, `short`, `int`, `long`,
3506        // `char`, `float`, `double`, `bool`, `object` is now a distinct
3507        // Halstead operator (9 primitives) rather than collapsing into
3508        // one `PredefinedType` kind_id entry. u_operators rises from 6
3509        // to 14 (5 non-primitive operators + 9 distinct primitives);
3510        // total operators (N1) is unchanged because the same nodes are
3511        // still counted, just keyed by lexeme.
3512        //
3513        // N2 dropped 23 → 21 with issue #1253: `true` and `false` each
3514        // reached the walker twice — once as `boolean_literal`, once as
3515        // the keyword leaf under it — so each added one spurious
3516        // occurrence. n2 is unchanged at 21 because operands are keyed
3517        // by source text, so the duplicate collapsed into the existing
3518        // vocabulary entry; that is exactly why the inflation was
3519        // invisible in n2. Every operand here is distinct, so
3520        // N2 == n2 == 21 after the fix.
3521        check_metrics::<CsharpParser>(
3522            "public class Prims {
3523                byte a = 1;
3524                short b = 2;
3525                int c = 3;
3526                long d = 4;
3527                char e = 'x';
3528                float f = 1.0f;
3529                double g = 2.0;
3530                bool h = true;
3531                bool i = false;
3532                object j = null;
3533            }",
3534            "foo.cs",
3535            |metric| {
3536                assert_eq!(metric.halstead.unique_operators(), 14);
3537                assert_eq!(metric.halstead.total_operators(), 33);
3538                assert_eq!(metric.halstead.unique_operands(), 21);
3539                assert_eq!(metric.halstead.total_operands(), 21);
3540                insta::assert_json_snapshot!(metric.halstead);
3541            },
3542        );
3543    }
3544
3545    #[cfg(feature = "csharp")]
3546    #[test]
3547    fn csharp_boolean_literal_counts_once() {
3548        // Regression: issue #1253. `boolean_literal: choice('true',
3549        // 'false')` wraps the keyword leaf, and both kinds sat in the
3550        // operand arm, so every `true` / `false` occurrence added +1 to
3551        // N2. Operands are keyed by source text, so the duplicate
3552        // collapsed into the same vocabulary entry and n2 stayed
3553        // correct — which is why nothing caught it.
3554        //
3555        // Source repeats `true` so N2 exceeds n2 and the assertions can
3556        // tell "counted once per occurrence" from "deduplicated into
3557        // the vocabulary".
3558        //
3559        // Operands by text key: `A`, `M`, `a`, `b`, `c`, `d`, `true` × 2,
3560        // `false`, `null` ⇒ n2 = 9, N2 = 10. Before the fix the keyword
3561        // leaves added one occurrence per boolean ⇒ N2 = 13.
3562        check_metrics::<CsharpParser>(
3563            "class A {\n    void M() {\n        bool a = true;\n        bool b = false;\n        bool c = true;\n        object d = null;\n    }\n}\n",
3564            "foo.cs",
3565            |metric| {
3566                assert_eq!(metric.halstead.unique_operands(), 9);
3567                assert_eq!(metric.halstead.total_operands(), 10);
3568            },
3569        );
3570    }
3571
3572    #[cfg(feature = "csharp")]
3573    #[test]
3574    fn csharp_operator_true_names_an_operator_not_a_value() {
3575        // C#'s overloadable-operator list emits a bare `true` / `false`
3576        // token with no `boolean_literal` wrapper — `operator_declaration`
3577        // is the grammar's only such position. There the keyword names
3578        // the operator being declared, as `+` does in `operator +`, so it
3579        // is an operator (#1296); the companion test above pins the
3580        // wrapped literal, which bills only its wrapper (#1253).
3581        //
3582        // Each declaration names one boolean and returns the other, so
3583        // the fixture carries each keyword once as a name and once as a
3584        // value.
3585        //
3586        // Operators (n1 = 11, N1 = 18): class 1, {} 1, public 2,
3587        // static 2, bool 2, operator 2, true 1, false 1, () 2, => 2,
3588        // ; 2. Operands (n2 = 4, N2 = 7): A × 3 (class name, two
3589        // parameter types), a × 2, and the literals false and true once
3590        // each. Before #1296 the two names were operands instead:
3591        // (9, 16, 4, 9). A blanket exclusion of the leaf would give
3592        // (9, 16, 4, 7), and no guard at all (11, 18, 4, 9).
3593        let source = "class A {\n    public static bool operator true(A a) => false;\n    public static bool operator false(A a) => true;\n}\n";
3594        assert_halstead_counts::<CsharpParser>(source, "foo.cs", [11, 18, 4, 7], "operator true");
3595
3596        // `true` and `false` are each an operator *and* an operand here,
3597        // once apiece: the name and the literal land in different halves.
3598        let ops = ops_of::<CsharpParser>(source, "foo.cs");
3599        for keyword in ["true", "false"] {
3600            for (half, list) in [("operators", &ops.operators), ("operands", &ops.operands)] {
3601                assert_eq!(
3602                    list.iter().filter(|o| *o == keyword).count(),
3603                    1,
3604                    "`{keyword}` must appear once among the {half}; they were {list:?}"
3605                );
3606            }
3607        }
3608
3609        // The fallback arm: outside both positions — here a bare `true`
3610        // under error recovery, the only other parent the grammar
3611        // produces — the keyword keeps its role as a value.
3612        assert_halstead_counts::<CsharpParser>("true\n", "foo.cs", [0, 0, 1, 1], "bare true");
3613    }
3614
3615    #[cfg(feature = "csharp")]
3616    #[test]
3617    fn csharp_predefined_types_keyed_by_lexeme() {
3618        // Regression: issue #286. The C# grammar emits one `PredefinedType`
3619        // kind_id for every keyword type (`int`, `string`, `bool`, …).
3620        // Without keying by source text the entire family collapses into
3621        // a single Halstead operator (n1 += 1) instead of one per distinct
3622        // keyword. This test pins the post-fix behaviour using four
3623        // distinct primitives — `int`, `string`, `bool`, `object` —
3624        // appearing as parameter types so no other operators interact
3625        // with the count.
3626        //
3627        // expected: operators are `class`, `void`, `M`, `{}`, `()`, `,`
3628        // (×3 between 4 params), plus the four distinct predefined types
3629        // → u_operators = 5 + 4 = 9. Without the fix the four primitives
3630        // collapse to one entry, giving u_operators = 6.
3631        check_metrics::<CsharpParser>(
3632            "class C { void M(int a, string b, bool c, object d) {} }",
3633            "foo.cs",
3634            |metric| {
3635                // The headline assertion: four distinct primitive
3636                // keywords contribute four distinct operators, not one.
3637                assert_eq!(metric.halstead.unique_operators(), 9);
3638            },
3639        );
3640    }
3641
3642    #[cfg(feature = "csharp")]
3643    #[test]
3644    fn csharp_interpolated_string_no_double_count() {
3645        // Regression: issue #183. A C# `$"Hi {name}!"` used to be
3646        // classified as a Halstead operand (the wrapping
3647        // `InterpolatedStringExpression`) AND have its inner
3648        // `Interpolation`'s identifier classified as an operand too.
3649        // The fix routes `InterpolatedStringExpression` through a
3650        // conditional: when it has an `Interpolation` child, the inner
3651        // identifier already carries the operand contribution and the
3652        // wrapper is treated as `Unknown`; when it does not (static
3653        // `$"hello"`), the wrapper still counts as one operand.
3654        //
3655        // expected: operand contributions for
3656        //   `class C { void M(string name) { string s = $"Hi {name}!"; } }`
3657        // — `C` (class), `M` (method), `name` (param), `s` (local),
3658        // and the inner `name` (inside `{...}`). With the fix,
3659        // u_operands = 4 (C, M, name, s); N2 = 5 (`name` twice).
3660        // Without the fix, the wrapping `$"Hi {name}!"` would also
3661        // count → u_operands = 5, N2 = 6.
3662        check_metrics::<CsharpParser>(
3663            "class C { void M(string name) { string s = $\"Hi {name}!\"; } }",
3664            "foo.cs",
3665            |metric| {
3666                assert_eq!(metric.halstead.unique_operands(), 4);
3667                assert_eq!(metric.halstead.total_operands(), 5);
3668            },
3669        );
3670    }
3671
3672    #[cfg(feature = "csharp")]
3673    #[test]
3674    fn csharp_static_interpolated_string_is_operand() {
3675        // Regression: issue #183. A `$"..."` with no `{...}` is
3676        // semantically identical to `"..."` and must still contribute
3677        // exactly one operand — the conditional `is_child(Interpolation)`
3678        // check distinguishes it from a true interpolation. expected:
3679        // operands are `C`, `M`, `s`, `$"hello"` → u_operands = 4, N2 = 4.
3680        // A naive "always Unknown" fix would yield u_operands = 3, N2 = 3,
3681        // diverging from the plain-string equivalent below.
3682        check_metrics::<CsharpParser>(
3683            "class C { void M() { string s = $\"hello\"; } }",
3684            "foo.cs",
3685            |metric| {
3686                assert_eq!(metric.halstead.unique_operands(), 4);
3687                assert_eq!(metric.halstead.total_operands(), 4);
3688            },
3689        );
3690    }
3691
3692    #[cfg(feature = "csharp")]
3693    #[test]
3694    fn csharp_plain_string_still_operand() {
3695        // The fix for #183 only changes how `InterpolatedStringExpression`
3696        // is classified; plain `StringLiteral` (and `VerbatimStringLiteral`
3697        // / `RawStringLiteral`) must still contribute exactly one operand
3698        // each. expected: operands are `C`, `M`, `s`, `"hi"` →
3699        // u_operands = 4, N2 = 4.
3700        check_metrics::<CsharpParser>(
3701            "class C { void M() { string s = \"hi\"; } }",
3702            "foo.cs",
3703            |metric| {
3704                assert_eq!(metric.halstead.unique_operands(), 4);
3705                assert_eq!(metric.halstead.total_operands(), 4);
3706            },
3707        );
3708    }
3709
3710    /// #1573: an interpolation's alignment `,` and format `:` delimit the
3711    /// field, as Python's format-spec `:` does (#1486), and bill nothing.
3712    /// The alignment value is an ordinary expression, so `8`, `Width`
3713    /// and `-w` keep their vocabulary. The guard reads the token's
3714    /// parent: the last seven rows put the same `,` / `:` inside and
3715    /// outside a hole in a position that is not a clause, and each keeps
3716    /// billing. Pre-fix, every row with a clause billed one extra `,`
3717    /// and/or `:`.
3718    ///
3719    /// Each row is `(label, source, operators, operands)`, the two
3720    /// vocabularies space-separated. No row repeats a token, so the
3721    /// expected `[n1, N1, n2, N2]` is the two vocabulary sizes, each twice.
3722    #[cfg(feature = "csharp")]
3723    #[test]
3724    fn csharp_interpolation_clause_delimiters_are_not_operators_1573() {
3725        const ROWS: &[(&str, &str, &str, &str)] = &[
3726            ("control", "s = $\"{x}\";", "; =", "s x"),
3727            ("format", "s = $\"{x:F2}\";", "; =", "s x"),
3728            ("alignment", "s = $\"{x,8}\";", "; =", "8 s x"),
3729            ("both", "s = $\"{x,8:F2}\";", "; =", "8 s x"),
3730            // a `const` width is a `constant_expression` the spec allows
3731            ("named width", "s = $\"{x,Width}\";", "; =", "Width s x"),
3732            ("negated width", "s = $\"{x,-w}\";", "- ; =", "s w x"),
3733            // the format string's `,` is text, not a token
3734            ("comma in format", "s = $\"{x:#,##0}\";", "; =", "s x"),
3735            ("raw", "s = $$\"\"\"{{x,8:F2}}\"\"\";", "; =", "8 s x"),
3736            ("verbatim", "s = $@\"{x:F2}\";", "; =", "s x"),
3737            ("call in hole", "s = $\"{f(a, b)}\";", "() , ; =", "a b f s"),
3738            (
3739                "ternary in hole",
3740                "s = $\"{(a ? b : c)}\";",
3741                "() : ; = ?",
3742                "a b c s",
3743            ),
3744            (
3745                "named arg in hole",
3746                "s = $\"{f(x: 1)}\";",
3747                "() : ; =",
3748                "1 f s x",
3749            ),
3750            ("ternary", "s = a ? b : c;", ": ; = ?", "a b c s"),
3751            ("named argument", "f(x: 1);", "() : ;", "1 f x"),
3752            (
3753                "case label",
3754                "switch (x) { case 1: break; }",
3755                "() : ; break case switch {}",
3756                "1 x",
3757            ),
3758            ("argument list", "f(a, b);", "() , ;", "a b f"),
3759        ];
3760        for (label, source, operators, operands) in ROWS {
3761            // `bca ops` sorts its vocabulary, so the observation is
3762            // compared as returned; the rows are written in that order.
3763            let operators: Vec<&str> = operators.split(' ').collect();
3764            let operands: Vec<&str> = operands.split(' ').collect();
3765            let ops = ops_of::<CsharpParser>(source, "foo.cs");
3766            assert_eq!(ops.operators, operators, "{label}: {source:?}");
3767            assert_eq!(ops.operands, operands, "{label}: {source:?}");
3768            let n1 = operators.len() as u64;
3769            let n2 = operands.len() as u64;
3770            assert_halstead_counts::<CsharpParser>(source, "foo.cs", [n1, n1, n2, n2], label);
3771        }
3772    }
3773
3774    // C# half of #1380 — see `java_self_and_super_references_are_operands`
3775    // for the structural argument. `base` moves with `this`: both are
3776    // receivers of a member access.
3777    #[cfg(feature = "csharp")]
3778    #[test]
3779    fn csharp_self_and_base_references_are_operands() {
3780        let source = "class T {\n    int F() { return this.x + base.y; }\n}";
3781        // Operators (n1=8, N1=10): class, {} x2, int, (), return, . x2, +, ;
3782        // Operands (n2=6, N2=6): T, F, this, x, base, y
3783        // Before the fix this read (10, 12, 4, 4).
3784        assert_halstead_counts::<CsharpParser>(
3785            source,
3786            "foo.cs",
3787            [8, 10, 6, 6],
3788            "csharp self/base references",
3789        );
3790        assert_keywords_are_operands_only::<CsharpParser>(source, "foo.cs", &["this", "base"]);
3791    }
3792
3793    // An `indexer_declaration` spells the member's *name* with the same
3794    // `this` token kind the receiver uses (both are kind 91), so a
3795    // blanket move would have billed a declarator keyword as a value.
3796    // The declaration keeps the operator classification that the
3797    // `operator` keyword of an overload declaration already has (#1380).
3798    //
3799    // The fixture carries both uses, which is what makes the gate
3800    // observable: `this` has to reach *both* vocabularies from one file.
3801    // Asserting only the operand side would pass with the gate deleted,
3802    // and only the operator side would pass with the whole #1380 change
3803    // reverted (grammar-dispatch section 11).
3804    #[cfg(feature = "csharp")]
3805    #[test]
3806    fn csharp_indexer_declaration_keyword_is_not_a_self_reference() {
3807        let source = "class C {\n    int[] _a;\n    \
3808                      public int this[int i] { get { return this._a[i]; } }\n}";
3809
3810        // Asserted per space, not through the root. The root merges
3811        // every space's vocabulary, and the fixture has one declarator
3812        // `this` and one receiver `this`, so at the root `this` is a
3813        // member of *both* vocabularies whether the gate is right or
3814        // exactly backwards — and every Halstead count is bit-identical
3815        // under the swap, one occurrence on each side either way. The
3816        // space tree is where the two uses stay apart: the declarator
3817        // sits in the class's own vocabulary and the receiver in the
3818        // accessor's.
3819        let mut roles = Vec::new();
3820        collect_keyword_roles(
3821            &ops_of::<CsharpParser>(source, "foo.cs"),
3822            "this",
3823            &mut roles,
3824        );
3825
3826        // `Both` can only arise from two `this` nodes in one space that
3827        // the getter classifies differently — it is the gate's whole
3828        // effect, and it disappears the moment `this` gets one role.
3829        // `OperandOnly` is the accessor, the innermost space and the one
3830        // holding the receiver alone; it is what flips when the gate is
3831        // inverted rather than dropped. Correct gives
3832        // `[Both, Both, OperandOnly]`; inverting gives
3833        // `[Both, Both, OperatorOnly]`; dropping the gate gives three
3834        // `OperandOnly`; reverting #1380 gives three `OperatorOnly`.
3835        // Asserting the whole sequence rather than two `contains` calls:
3836        // the comment above already derives it, and a `contains` pair
3837        // leaves the length unasserted, so a space appearing or vanishing
3838        // goes unnoticed and `[Both, X, OperandOnly]` passes for any `X`.
3839        assert_eq!(
3840            roles,
3841            vec![Role::Both, Role::Both, Role::OperandOnly],
3842            "the declarator and the receiver must disagree in the two outer spaces \
3843             and the accessor must bill `this` as an operand alone",
3844        );
3845    }
3846
3847    /// One `this` and one `base` per container the pinned
3848    /// `tree-sitter-c-sharp` can put them in: a delegating constructor
3849    /// initializer, a base-constructor initializer, a member access, an
3850    /// element access, and a call argument. No `indexer_declaration` —
3851    /// that position is the gated one and is an operator.
3852    #[cfg(feature = "csharp")]
3853    const CSHARP_SELF_POSITIONS: &str = "class Pos : B {
3854        int[] _a;
3855        public Pos() : this(1) { }
3856        public Pos(int x) : base(x) { }
3857        int A() { return this._a[0]; }
3858        int C() { return base.H(); }
3859        int E() { return base[0]; }
3860        int F() { return this[0]; }
3861        void G() { M(this); }
3862        void M(object o) { }
3863    }";
3864
3865    #[cfg(feature = "csharp")]
3866    #[test]
3867    fn csharp_self_and_base_leaves_are_unaliased_and_unconditional() {
3868        assert_self_reference_leaves::<CsharpCode>(
3869            CSHARP_SELF_POSITIONS,
3870            &[
3871                ("this", Csharp::This as u16, 4),
3872                ("base", Csharp::Base as u16, 3),
3873            ],
3874            "csharp",
3875        );
3876    }
3877
3878    /// Every spelling of the six keywords `_parameter_type_with_modifiers`
3879    /// aliases to `modifier`, plus two controls that were billed before
3880    /// #1418: the bare `params` of a parameter array (kind 94, never
3881    /// aliased) and the bare `out` of a call argument (kind 56).
3882    ///
3883    /// `D` is declared *and* called so the argument `out` has a caller
3884    /// to sit in, which is what makes the two spellings of one keyword
3885    /// meet in one file.
3886    #[cfg(feature = "csharp")]
3887    const CSHARP_PARAMETER_MODIFIERS: &str = "static class E {
3888    static void A(this Foo f) { }
3889    static void B(scoped ref Foo f) { }
3890    static void C(ref readonly Foo f) { }
3891    static void D(out int x) { x = 1; }
3892    static void G(in Foo f) { }
3893    static void H(params int[] xs) { }
3894    static void I(ref int y) { D(out y); }
3895}";
3896
3897    /// Each of the five operator keywords in *both* of its kind
3898    /// spellings — the childless `modifier` of a parameter, and the
3899    /// keyword's own token elsewhere: `ref` as a ref local, ref
3900    /// initializer and ref argument (kind 28); `readonly` as the leaf
3901    /// under a field's `modifier` wrapper (48); `in` as the `foreach`
3902    /// separator and an `in` argument (55); `out` as an out argument
3903    /// (56); `scoped` as a scoped ref local (93).
3904    ///
3905    /// Separate from [`CSHARP_PARAMETER_MODIFIERS`] because that fixture
3906    /// pairs the two spellings for `out` alone, which left four of the
3907    /// five bare kinds `CsharpCode::is_primitive` lists with no input
3908    /// (#1418).
3909    #[cfg(feature = "csharp")]
3910    const CSHARP_MODIFIER_BOTH_SPELLINGS: &str = "class S {
3911    static readonly int q = 0;
3912    static void M(scoped ref int a, ref readonly int b, in int c, out int d) { d = q; }
3913    static void N(int[] xs) {
3914        ref int r = ref xs[0];
3915        scoped ref int s = ref xs[1];
3916        foreach (var e in xs) { }
3917        M(ref r, in s, in s, out r);
3918    }
3919}";
3920
3921    /// The six aliased parameter modifiers reach a Halstead half, and
3922    /// each reaches the same one its bare token reaches elsewhere
3923    /// (#1418).
3924    #[cfg(feature = "csharp")]
3925    #[test]
3926    fn csharp_parameter_modifiers_are_classified_1418() {
3927        // Operators (n1 = 15, N1 = 48): class 1; {} 8 (the class body
3928        // and seven method bodies); static 8 (the class and seven
3929        // methods); void 7; () 8 (seven parameter lists and the call);
3930        // int 3 (`out int x`, `int[]`, `ref int y`); [] 1; = 1; ; 2;
3931        // scoped 1; ref 3; readonly 1; out 2 (the parameter and the
3932        // argument); in 1; params 1.
3933        //
3934        // Operands (n2 = 15, N2 = 24): E 1; A/B/C/G/H/I 1 each; D 2
3935        // (declaration and call); this 1; Foo 4; f 4; x 2; 1 1; xs 1;
3936        // y 2.
3937        //
3938        // Before the fix this read (11, 41, 14, 23). The eight childless
3939        // `modifier` nodes were billed nowhere, which cost seven
3940        // operator occurrences and one operand occurrence, and emptied
3941        // `scoped`, `ref`, `readonly` and `in` out of the operator
3942        // vocabulary entirely — `out` survived only because the call
3943        // argument spells it with the unaliased kind.
3944        assert_halstead_counts::<CsharpParser>(
3945            CSHARP_PARAMETER_MODIFIERS,
3946            "foo.cs",
3947            [15, 48, 15, 24],
3948            "csharp parameter modifiers",
3949        );
3950
3951        // The receiver is an operand, matching every other C# `this`
3952        // (#1380). Both halves matter: an arm listing it twice would
3953        // still put it among the operands.
3954        assert_keywords_are_operands_only::<CsharpParser>(
3955            CSHARP_PARAMETER_MODIFIERS,
3956            "foo.cs",
3957            &["this"],
3958        );
3959
3960        let ops = ops_of::<CsharpParser>(CSHARP_PARAMETER_MODIFIERS, "foo.cs");
3961        for keyword in ["ref", "out", "in", "scoped", "readonly", "params"] {
3962            assert!(
3963                ops.operators.iter().any(|o| o == keyword),
3964                "`{keyword}` must be an operator; operators were {:?}",
3965                ops.operators
3966            );
3967            assert!(
3968                !ops.operands.iter().any(|o| o == keyword),
3969                "`{keyword}` must not be an operand; operands were {:?}",
3970                ops.operands
3971            );
3972        }
3973
3974        // The vocabulary is not deduplicated — it is the concatenation
3975        // of the kind-keyed and the lexeme-keyed operator maps — so a
3976        // keyword appearing twice is the #453 shape: one keyword split
3977        // across the two maps and counted twice in `n1`. Keeping both
3978        // spellings in one map is what `CsharpCode::is_primitive`
3979        // listing the five bare kinds buys, and this is the only
3980        // assertion that can see it.
3981        //
3982        // It needs a fixture carrying *both* spellings of each keyword,
3983        // which `CSHARP_PARAMETER_MODIFIERS` does for `out` alone: its
3984        // `ref`, `in`, `scoped` and `readonly` are all in parameter
3985        // position, so for those the count can only ever be one and the
3986        // assertion is dead. That left four of the five bare kinds with
3987        // no input at all — dropping `Csharp::Ref`, `In`, `Readonly` or
3988        // `Scoped` from `is_primitive` failed no test in the workspace.
3989        assert_modifier_spellings_are_one_operator(
3990            CSHARP_MODIFIER_BOTH_SPELLINGS,
3991            &[
3992                ("ref", Csharp::Ref as u16),
3993                ("out", Csharp::Out as u16),
3994                ("in", Csharp::In as u16),
3995                ("scoped", Csharp::Scoped as u16),
3996                ("readonly", Csharp::Readonly as u16),
3997            ],
3998        );
3999    }
4000
4001    /// Asserts each keyword in `spellings` appears in `source` both as
4002    /// its own `kind` and as a childless aliased `modifier`, and that the
4003    /// two spellings render as exactly one entry in the operator
4004    /// vocabulary.
4005    ///
4006    /// The vocabulary is the concatenation of the kind-keyed and the
4007    /// lexeme-keyed operator maps, so a keyword listed twice is one
4008    /// keyword split across both maps and counted twice in `n1`.
4009    #[cfg(feature = "csharp")]
4010    #[track_caller]
4011    fn assert_modifier_spellings_are_one_operator(source: &str, spellings: &[(&str, u16)]) {
4012        let code = source.as_bytes();
4013        // (bare, aliased) occurrences per keyword.
4014        let mut seen = vec![(0_usize, 0_usize); spellings.len()];
4015
4016        for_each_node_with_chain::<CsharpCode>(code, |node, _| {
4017            let text = &code[node.start_byte()..node.end_byte()];
4018            let Some(index) = spellings.iter().position(|(kw, _)| kw.as_bytes() == text) else {
4019                return;
4020            };
4021            if node.kind_id() == Csharp::Modifier as u16 && node.child_count() == 0 {
4022                seen[index].1 += 1;
4023            } else if node.kind_id() == spellings[index].1 {
4024                seen[index].0 += 1;
4025            }
4026        });
4027
4028        // Without this the vocabulary count below is vacuous for any
4029        // keyword the fixture stopped spelling both ways — the decay
4030        // that once left four of #1418's bare kinds with no input.
4031        for ((keyword, _), (bare, aliased)) in spellings.iter().zip(&seen) {
4032            assert!(
4033                *bare > 0 && *aliased > 0,
4034                "fixture must spell `{keyword}` both ways; it has {bare} bare and {aliased} aliased"
4035            );
4036        }
4037
4038        let ops = ops_of::<CsharpParser>(source, "both.cs");
4039        for (keyword, _) in spellings {
4040            assert_eq!(
4041                ops.operators.iter().filter(|o| o == keyword).count(),
4042                1,
4043                "`{keyword}` must be one operator across both of its kind spellings; \
4044                 operators were {:?}",
4045                ops.operators
4046            );
4047        }
4048    }
4049
4050    /// Pins the grammar shape the #1418 arm gates on, in both
4051    /// directions.
4052    ///
4053    /// The arm reads `modifier` nodes with no children as aliased bare
4054    /// keywords and leaves the rest to their classified keyword leaf.
4055    /// That is a claim about two populations, and a grammar bump can
4056    /// break either: giving an alias a token child would double-count
4057    /// it, and flattening a declaration modifier to a childless node
4058    /// would bill `public` / `static` as an unknown keyword. Both are
4059    /// checked here, so neither can move quietly.
4060    ///
4061    /// Childlessness says "aliased", *not* "parameter modifier" —
4062    /// `_lambda_expression_init` and `anonymous_method_expression` alias
4063    /// a bare `static` / `async` onto the same kind, which the second
4064    /// walk below pins so the distinction cannot be forgotten. Both are
4065    /// operators, as their declaration spelling is (#1482).
4066    #[cfg(feature = "csharp")]
4067    #[test]
4068    fn csharp_aliased_modifier_is_childless() {
4069        let code = CSHARP_PARAMETER_MODIFIERS.as_bytes();
4070        let (mut aliased, mut declaration) = (0_usize, 0_usize);
4071
4072        for_each_node_with_chain::<CsharpCode>(code, |node, chain| {
4073            if node.kind_id() != Csharp::Modifier as u16 {
4074                return;
4075            }
4076            let role = CsharpCode::get_op_type_with_code(node, code, Ancestors::known(chain));
4077            let text = &code[node.start_byte()..node.end_byte()];
4078
4079            if node.child_count() == 0 {
4080                aliased += 1;
4081                // Spelled out rather than `this`-versus-everything-else,
4082                // so a text this fixture does not carry fails loudly;
4083                // the lambda spellings are owned by the second walk.
4084                let (correct, expected) = match text {
4085                    b"this" => (matches!(role, TokenRole::Operand), "an operand"),
4086                    b"scoped" | b"ref" | b"out" | b"in" | b"readonly" => {
4087                        (matches!(role, TokenRole::Operator), "an operator")
4088                    }
4089                    other => panic!(
4090                        "`{}` is not one of the six parameter spellings this fixture carries",
4091                        String::from_utf8_lossy(other)
4092                    ),
4093                };
4094                assert!(
4095                    correct,
4096                    "`{}` is an aliased parameter modifier and must be {expected}",
4097                    String::from_utf8_lossy(text)
4098                );
4099                // Grammar-dispatch section 5: nothing containing the
4100                // token is classified, so billing the token cannot
4101                // count the same source text twice. Every ancestor,
4102                // not just `parameter` — `parameter_list` and the
4103                // declaration above it contain it too.
4104                for (depth, ancestor) in chain.iter().enumerate() {
4105                    assert!(
4106                        matches!(
4107                            CsharpCode::get_op_type_with_code(
4108                                ancestor,
4109                                code,
4110                                Ancestors::known(&chain[..depth])
4111                            ),
4112                            TokenRole::Unknown
4113                        ),
4114                        "`{}` contains an aliased modifier and is itself classified, so the \
4115                         keyword now counts twice",
4116                        ancestor.kind()
4117                    );
4118                }
4119            } else {
4120                declaration += 1;
4121                assert_eq!(
4122                    node.child_count(),
4123                    1,
4124                    "a declaration `modifier` wraps exactly one keyword leaf"
4125                );
4126                assert!(
4127                    matches!(role, TokenRole::Unknown),
4128                    "the `{}` wrapper must stay unclassified; its leaf carries the operator",
4129                    String::from_utf8_lossy(text)
4130                );
4131                let leaf = node.child(0).expect("child_count is 1");
4132                let mut leaf_chain = chain.to_vec();
4133                leaf_chain.push(*node);
4134                assert!(
4135                    matches!(
4136                        CsharpCode::get_op_type_with_code(
4137                            &leaf,
4138                            code,
4139                            Ancestors::known(&leaf_chain)
4140                        ),
4141                        TokenRole::Operator
4142                    ),
4143                    "the leaf under a declaration `modifier` must carry the operator"
4144                );
4145            }
4146        });
4147
4148        // Eight of each: `this`, `scoped`, `ref` x3, `readonly`, `out`,
4149        // `in` in parameter position, and the `static` of the class and
4150        // of its seven methods. Asserted so a fixture that stopped
4151        // carrying one population cannot pass having checked nothing.
4152        assert_eq!(aliased, 8, "fixture lost an aliased parameter modifier");
4153        assert_eq!(declaration, 8, "fixture lost a declaration modifier");
4154
4155        // The second alias site. `static (int x) => …` and `async
4156        // delegate (int y) { … }` put a bare keyword under the same
4157        // childless `modifier`, so `csharp_is_aliased_modifier` holds
4158        // for a node that is not in a parameter at all — which is why
4159        // the getter dispatches on the text and not on the kind.
4160        let lambda = "static class L {
4161    static void M() {
4162        var a = static (int x) => x + 1;
4163        var b = async delegate (int y) { return y; };
4164    }
4165}"
4166        .as_bytes();
4167        let mut lambda_aliases = 0_usize;
4168
4169        for_each_node_with_chain::<CsharpCode>(lambda, |node, chain| {
4170            if node.kind_id() != Csharp::Modifier as u16 || node.child_count() != 0 {
4171                return;
4172            }
4173            lambda_aliases += 1;
4174            let text = &lambda[node.start_byte()..node.end_byte()];
4175            assert!(
4176                matches!(text, b"static" | b"async"),
4177                "`{}` is not a lambda modifier the grammar aliases",
4178                String::from_utf8_lossy(text)
4179            );
4180            assert!(
4181                matches!(
4182                    CsharpCode::get_op_type_with_code(node, lambda, Ancestors::known(chain)),
4183                    TokenRole::Operator
4184                ),
4185                "`{}` on a lambda must be an operator, as its declaration spelling is (#1482)",
4186                String::from_utf8_lossy(text)
4187            );
4188        });
4189
4190        assert_eq!(
4191            lambda_aliases, 2,
4192            "fixture lost a lambda / anonymous-method modifier alias"
4193        );
4194    }
4195
4196    /// `static` and `async` in both of their kind spellings: the keyword
4197    /// leaf under a method's `modifier` wrapper, and the childless
4198    /// `modifier` the grammar aliases a lambda's keyword to (#1482).
4199    #[cfg(feature = "csharp")]
4200    const CSHARP_LAMBDA_MODIFIER_BOTH_SPELLINGS: &str = "class L {
4201    static void N() { }
4202    async void M() { }
4203    void P() {
4204        var a = static (int x) => x;
4205        var b = async (int y) => y;
4206    }
4207}";
4208
4209    /// A C# lambda's bare parameter (`x => x`) is the operand its
4210    /// parenthesised twin (`(x) => x`) bills (#1544). Each row is one
4211    /// lambda in a method body; the twins differ only by the `()` the
4212    /// parenthesised spelling writes, so the operand halves must match
4213    /// and the operator halves differ by exactly that one `()`.
4214    #[cfg(feature = "csharp")]
4215    #[test]
4216    fn csharp_implicit_lambda_parameter_is_an_operand() {
4217        let wrap = |lambda: &str| {
4218            format!("class C {{\n    void M() {{\n        var f = {lambda};\n    }}\n}}\n")
4219        };
4220        // Operators: class, {} x2, void, () x1 (M's parameter list), =,
4221        // =>, ; — n1 = 7, N1 = 8 — plus the lambda's own `()` in the
4222        // parenthesised spelling. Operands: C, M, f, and the parameter
4223        // twice; `var` is an unclassified `implicit_type`. `_ => 0`
4224        // names `_` once and adds the literal `0`.
4225        //
4226        // Before #1544 the bare rows read (7, 8, 4, 4) for both: `x`
4227        // lost one occurrence, and `_` left the vocabulary entirely.
4228        for (lambda, expected) in [
4229            ("x => x", [7, 8, 4, 5]),
4230            ("(x) => x", [7, 9, 4, 5]),
4231            ("_ => 0", [7, 8, 5, 5]),
4232            ("(_) => 0", [7, 9, 5, 5]),
4233        ] {
4234            assert_halstead_counts::<CsharpParser>(&wrap(lambda), "foo.cs", expected, lambda);
4235        }
4236
4237        // The bare parameter is billed under its own text, not under
4238        // the kind name, and as an operand only.
4239        assert_keywords_are_operands_only::<CsharpParser>(&wrap("_ => 0"), "foo.cs", &["_"]);
4240    }
4241
4242    /// A lambda's `static` / `async` is the same operator as the
4243    /// declaration's, counted once in `n1` across both spellings
4244    /// (#1482).
4245    #[cfg(feature = "csharp")]
4246    #[test]
4247    fn csharp_lambda_modifiers_are_operators_1482() {
4248        // Operators (n1 = 10, N1 = 25): class 1; {} 4 (the class body
4249        // and three method bodies); static 2; async 2; void 3; () 5
4250        // (three method parameter lists and two lambda ones); int 2;
4251        // = 2; ; 2; => 2.
4252        //
4253        // Operands (n2 = 8, N2 = 10): L, N, M, P, a, b once each; x 2
4254        // and y 2 (the parameter and the lambda body). `var` is an
4255        // unclassified `implicit_type` wrapper.
4256        //
4257        // Before the fix this read (10, 23, 8, 10): the two lambda
4258        // keywords were billed nowhere. Billing them without listing
4259        // `Static` / `Async` in `CsharpCode::is_primitive` reads
4260        // (12, 25, 8, 10) — each keyword split across the kind-keyed
4261        // and the lexeme-keyed operator map.
4262        assert_halstead_counts::<CsharpParser>(
4263            CSHARP_LAMBDA_MODIFIER_BOTH_SPELLINGS,
4264            "foo.cs",
4265            [10, 25, 8, 10],
4266            "csharp lambda modifiers",
4267        );
4268
4269        assert_modifier_spellings_are_one_operator(
4270            CSHARP_LAMBDA_MODIFIER_BOTH_SPELLINGS,
4271            &[
4272                ("static", Csharp::Static as u16),
4273                ("async", Csharp::Async as u16),
4274            ],
4275        );
4276    }
4277
4278    #[cfg(feature = "go")]
4279    #[test]
4280    fn go_operators_and_operands() {
4281        check_metrics::<GoParser>(
4282            "package main
4283            func sum(a, b int) int {
4284                return a + b
4285            }",
4286            "foo.go",
4287            |metric| {
4288                insta::assert_json_snapshot!(
4289                    metric.halstead,
4290                    @r#"
4291                {
4292                  "unique_operators": 7,
4293                  "total_operators": 7,
4294                  "unique_operands": 5,
4295                  "total_operands": 8,
4296                  "length": 15,
4297                  "estimated_program_length": 31.26112492884004,
4298                  "purity_ratio": 2.0840749952560027,
4299                  "vocabulary": 12,
4300                  "volume": 53.77443751081734,
4301                  "difficulty": 5.6,
4302                  "level": 0.17857142857142858,
4303                  "effort": 301.1368500605771,
4304                  "time": 16.729825003365395,
4305                  "bugs": 0.014975730436275946
4306                }
4307                "#
4308                );
4309            },
4310        );
4311    }
4312
4313    #[cfg(feature = "perl")]
4314    #[test]
4315    fn perl_operators_and_operands() {
4316        check_metrics::<PerlParser>(
4317            "sub sum {
4318                my ($a, $b) = @_;
4319                return $a + $b;
4320            }",
4321            "foo.pl",
4322            |metric| {
4323                insta::assert_json_snapshot!(
4324                    metric.halstead,
4325                    @r#"
4326                {
4327                  "unique_operators": 10,
4328                  "total_operators": 14,
4329                  "unique_operands": 4,
4330                  "total_operands": 6,
4331                  "length": 20,
4332                  "estimated_program_length": 41.219280948873624,
4333                  "purity_ratio": 2.0609640474436812,
4334                  "vocabulary": 14,
4335                  "volume": 76.14709844115208,
4336                  "difficulty": 7.5,
4337                  "level": 0.13333333333333333,
4338                  "effort": 571.1032383086406,
4339                  "time": 31.727957683813365,
4340                  "bugs": 0.02294502281013948
4341                }
4342                "#
4343                );
4344            },
4345        );
4346    }
4347
4348    #[cfg(feature = "perl")]
4349    #[test]
4350    fn perl_interpolated_string_no_double_count() {
4351        // Regression: issue #199. A `string_double_quoted` (and
4352        // `string_qq_quoted` / `backtick_quoted` / `command_qx_quoted`)
4353        // wrapping an `interpolation` child used to be counted as a
4354        // Halstead operand while the inner scalar/array/hash variable
4355        // was also walked and counted — double-counting the inner
4356        // variable's contribution to `N2`. Mirrors #180 (Bash/Elixir),
4357        // #183 (C#), #184 (PHP), #191 (Kotlin).
4358        //
4359        // expected: for
4360        //   sub greet { my $name = shift; my $msg = "Hi $name"; return $msg; }
4361        // — operands are `greet`, `$name`, `shift`, `$msg`. With the
4362        // fix the wrapping `"Hi $name"` is skipped (has `Interpolation`
4363        // child), so u_operands = 4 and N2 = 6 (`$name` x2 from the
4364        // `my` binding and the interpolation; `$msg` x2 from the `my`
4365        // binding and `return`; `greet`, `shift` once each). Without
4366        // the fix the wrapping literal would also be counted, lifting
4367        // u_operands to 5 and N2 to 7.
4368        check_metrics::<PerlParser>(
4369            "sub greet { my $name = shift; my $msg = \"Hi $name\"; return $msg; }",
4370            "foo.pl",
4371            |metric| {
4372                assert_eq!(metric.halstead.unique_operands(), 4);
4373                assert_eq!(metric.halstead.total_operands(), 6);
4374                insta::assert_json_snapshot!(metric.halstead);
4375            },
4376        );
4377    }
4378
4379    #[cfg(feature = "perl")]
4380    #[test]
4381    fn perl_plain_string_still_operand() {
4382        // The fix for #199 only skips wrapping literals that carry an
4383        // `Interpolation` child; a plain `"hello"` (no `$…` inside)
4384        // must still contribute exactly one operand. expected: operands
4385        // `greet`, `$msg`, `"hello"` → u_operands = 3, N2 = 4 (`$msg`
4386        // appears in the `my` binding and the `return`).
4387        check_metrics::<PerlParser>(
4388            "sub greet { my $msg = \"hello\"; return $msg; }",
4389            "foo.pl",
4390            |metric| {
4391                assert_eq!(metric.halstead.unique_operands(), 3);
4392                assert_eq!(metric.halstead.total_operands(), 4);
4393            },
4394        );
4395    }
4396
4397    #[cfg(feature = "perl")]
4398    #[test]
4399    fn perl_single_quoted_string_never_interpolates() {
4400        // Single-quoted (`'…'`) and `q{…}` literals are not subject to
4401        // interpolation in Perl, so even when their text contains a
4402        // `$name`-shaped sequence the wrapper is still counted as one
4403        // operand and the inner text is not parsed as a variable.
4404        // expected: operands `greet`, `$msg`, `'Hi $name'` →
4405        // u_operands = 3, N2 = 4 (`$msg` x2).
4406        check_metrics::<PerlParser>(
4407            "sub greet { my $msg = 'Hi $name'; return $msg; }",
4408            "foo.pl",
4409            |metric| {
4410                assert_eq!(metric.halstead.unique_operands(), 3);
4411                assert_eq!(metric.halstead.total_operands(), 4);
4412            },
4413        );
4414    }
4415
4416    #[cfg(feature = "perl")]
4417    #[test]
4418    fn perl_plain_heredoc_counts_as_one_operand() {
4419        // Regression: issue #287. A plain (non-interpolating) Perl
4420        // heredoc body used to be classified `TokenRole::Unknown`,
4421        // so its visible `HeredocBodyStatement` node contributed
4422        // nothing to N2 even though it is a string literal. The fix
4423        // adds `HeredocBodyStatement` to the interpolation-aware
4424        // operand arm, so an inert heredoc counts as one operand.
4425        //
4426        // Source (heredoc body lives at the source_file level, not
4427        // inside any sub):
4428        //   my $msg = <<END;
4429        //   hello world
4430        //   END
4431        //
4432        // Operands traversed:
4433        //   * `$msg` (`scalar_variable`)                    × 1
4434        //   * heredoc body (`heredoc_body_statement`)       × 1
4435        // expected: u_operands = 2, N2 = 2.
4436        check_metrics::<PerlParser>("my $msg = <<END;\nhello world\nEND\n", "foo.pl", |metric| {
4437            assert_eq!(metric.halstead.unique_operands(), 2);
4438            assert_eq!(metric.halstead.total_operands(), 2);
4439        });
4440    }
4441
4442    #[cfg(feature = "perl")]
4443    #[test]
4444    fn perl_interpolated_heredoc_no_double_count() {
4445        // Regression: issue #287. An interpolating Perl heredoc
4446        // (`<<"TAG"` or bare `<<TAG`) carries an `Interpolation` child
4447        // when its body contains a `$var`. The wrapper must drop to
4448        // `Unknown` so the inner scalar variable carries the operand
4449        // count — same dispatch as the existing double-quoted /
4450        // backtick / qx wrappers (issue #199) and the PHP heredoc fix
4451        // (issue #184).
4452        //
4453        // Source:
4454        //   my $name = "x";
4455        //   my $msg = <<"END";
4456        //   hi $name
4457        //   END
4458        //
4459        // Operands by text key:
4460        //   * `$name` × 2 (my-binding + interpolation inside heredoc)
4461        //   * `"x"`  × 1 (inert double-quoted string)
4462        //   * `$msg` × 1
4463        // expected: u_operands = 3, N2 = 4. Without the
4464        // interpolation-aware drop the wrapping heredoc body would
4465        // also count, lifting u_operands to 4 and N2 to 5.
4466        check_metrics::<PerlParser>(
4467            "my $name = \"x\";\nmy $msg = <<\"END\";\nhi $name\nEND\n",
4468            "foo.pl",
4469            |metric| {
4470                assert_eq!(metric.halstead.unique_operands(), 3);
4471                assert_eq!(metric.halstead.total_operands(), 4);
4472            },
4473        );
4474    }
4475
4476    #[cfg(feature = "perl")]
4477    #[test]
4478    fn perl_bare_pattern_delimiters_are_not_operators() {
4479        // Regression: issue #1312, the Perl sibling of Elixir #1256.
4480        // The bare match form is the only one of Perl's regex literals
4481        // whose delimiters are spelled with an operator token kind —
4482        // `bca dump` shows `/abc/` emitting two `SLASH` under
4483        // `PatternMatcher` — so `$s =~ /abc/;` reported a `/` operator
4484        // with no division in the source.
4485        //
4486        // expected: operators `$` (the `scalar_variable` sigil), `=~`
4487        // and `;` → n1 = N1 = 3. Operands `$s` and the `/abc/` pattern
4488        // → n2 = N2 = 2. Before the guard the two delimiters added `/`
4489        // → n1 = 4, N1 = 5; the pattern operand arrived with #1314,
4490        // which promoted all three pattern spellings together (see
4491        // `perl_every_pattern_value_spelling_scores_alike`).
4492        check_metrics::<PerlParser>("$s =~ /abc/;\n", "foo.pl", |metric| {
4493            assert_eq!(metric.halstead.unique_operators(), 3);
4494            assert_eq!(metric.halstead.total_operators(), 3);
4495            assert_eq!(metric.halstead.unique_operands(), 2);
4496            assert_eq!(metric.halstead.total_operands(), 2);
4497        });
4498    }
4499
4500    #[cfg(feature = "perl")]
4501    #[test]
4502    fn perl_every_pattern_value_spelling_scores_alike() {
4503        // Companion to the test above (#1312, extended by #1314).
4504        // `m/abc/` is exactly `/abc/` in Perl and `qr/abc/` is the same
4505        // pattern as a value, so the three spellings of a pattern
4506        // *value* must score identically whatever delimiters they use.
4507        //
4508        // Until #1314 they scored alike at *zero*: no Perl pattern
4509        // wrapper was in the operand arm, so the literal never counted,
4510        // unlike Ruby's `Regex` and Elixir's `Sigil`. #1312 declined to
4511        // promote the bare form on its own precisely because that would
4512        // have scored `/abc/` at one operand and its synonyms at zero,
4513        // reintroducing the spelling sensitivity this test pins.
4514        // Promoting all three together closes the gap and keeps the
4515        // equality, which is what this test now asserts.
4516        //
4517        // `s///` and `tr///` are deliberately *not* rows here any more.
4518        // They are operations applied to a target rather than pattern
4519        // values, so #1314 made them operators; their own equality is
4520        // pinned by `perl_every_pattern_operation_spelling_scores_alike`
4521        // below. Splitting the one loop in two is the substantive
4522        // disagreement #1314 had with the reasoning recorded here: this
4523        // test governs *synonyms*, and `s///` is not a synonym of
4524        // `/abc/`.
4525        //
4526        // The fixture matches twice against one variable so that no two
4527        // of `[n1, N1, n2, N2]` are equal. A square tuple would leave
4528        // `assert_halstead_counts`' unique-vs-total axes unpinned —
4529        // transposing n1 with N1 inside the helper failed no test while
4530        // all three of its callers expected a square tuple.
4531        //
4532        // expected per variant: operators `$` × 2 (one per
4533        // `scalar_variable`), `=~` × 2, `and`, `;` → n1 = 4, N1 = 6.
4534        // The pattern contributes no *operator* — that is #1312's half
4535        // — and one operand, so with `$s` twice and the pattern twice
4536        // → n2 = 2, N2 = 4.
4537        for pattern in ["/abc/", "m/abc/", "m{abc}", "qr/abc/"] {
4538            assert_halstead_counts::<PerlParser>(
4539                &format!("$s =~ {pattern} and $s =~ {pattern};\n"),
4540                "foo.pl",
4541                [4, 6, 2, 4],
4542                &format!("pattern {pattern}"),
4543            );
4544        }
4545    }
4546
4547    #[cfg(feature = "perl")]
4548    #[test]
4549    fn perl_comment_leader_is_not_an_operator_1549() {
4550        // tree-sitter-perl emits a comment's leading `#` as a `#` token
4551        // under `comments`, and the getter listed it as an operator, so
4552        // every comment billed one `#` (#1549). The oracle is the same
4553        // source with its comments deleted, which must score identically.
4554        // `$#a` and `qw#…#` are kept to pin that dropping `#` leaves the
4555        // distinct `$#` operator and the `qw` delimiters alone.
4556        //
4557        // expected: operators `sub`, `{}`, `my`, `=`, `;` × 2, `return`,
4558        // `$#` × 2, `+` → n1 = 8, N1 = 10. Operands `f`, `@a`, `x`, `y`,
4559        // `a` × 2 → n2 = 5, N2 = 6. Before the fix the commented row
4560        // added `#` → n1 = 9, N1 = 13. Once excluded, a comment feeds no
4561        // axis to anchor on, so only the revert run guards the fixture:
4562        // keep its comments.
4563        for (source, label) in [
4564            (
4565                "sub f {\n  # leading\n  my @a = qw#x y#; # tail\n  return $#a + $#a; # tail\n}\n",
4566                "commented",
4567            ),
4568            (
4569                "sub f {\n  my @a = qw#x y#;\n  return $#a + $#a;\n}\n",
4570                "comment-free twin",
4571            ),
4572        ] {
4573            assert_halstead_counts::<PerlParser>(source, "foo.pl", [8, 10, 5, 6], label);
4574        }
4575    }
4576
4577    #[cfg(feature = "perl")]
4578    #[test]
4579    fn perl_bare_pattern_text_bills_no_operator_1549() {
4580        // The bare `/…/` form keeps its pattern text in a
4581        // `regex_pattern_content` that spells `#`, `[`, `]` and `\` as
4582        // anonymous tokens, three of which an operator arm listed, so
4583        // `/a#b[c]\#/` billed `#`, `[]` and `\` operators. `m//` and
4584        // `qr//` emit no node for the same text, so they are the oracle:
4585        // all three spellings must score as `/abc/` does in
4586        // `perl_every_pattern_value_spelling_scores_alike`.
4587        //
4588        // expected per variant: operators `$` × 2, `=~` × 2, `and`, `;`
4589        // → n1 = 4, N1 = 6; operands `$s` × 2 and the pattern × 2 →
4590        // n2 = 2, N2 = 4. Before the fix the bare row scored n1 = 7,
4591        // N1 = 14. The `#`, `[c]` and `\#` feed no axis once excluded,
4592        // so nothing anchors them and only the revert run guards the
4593        // fixture: keep them in the bare row.
4594        for pattern in [r"/a#b[c]\#/", r"m/a#b[c]\#/", r"qr/a#b[c]\#/"] {
4595            assert_halstead_counts::<PerlParser>(
4596                &format!("$s =~ {pattern} and $s =~ {pattern};\n"),
4597                "foo.pl",
4598                [4, 6, 2, 4],
4599                &format!("pattern {pattern}"),
4600            );
4601        }
4602    }
4603
4604    #[cfg(feature = "perl")]
4605    #[test]
4606    fn perl_every_pattern_operation_spelling_scores_alike() {
4607        // The other half of the split (#1314). Substitution and
4608        // transliteration are operations applied to a target, so they
4609        // are operators — and like the value spellings, their delimiter
4610        // choice must not move the count. `y///` is a synonym of
4611        // `tr///` and shares `TransliterationTrOrY`, so the two fold to
4612        // one operator entry, which is why all four rows agree on n1.
4613        //
4614        // expected per variant: operators `$` × 2, `=~` × 2, `and`,
4615        // `;`, and the operation itself × 2 → n1 = 5, N1 = 8. The
4616        // pattern and replacement text is invisible to this grammar —
4617        // `substitution_pattern_s` emits only its keyword and
4618        // delimiters, no content node — so the sole operand is `$s`,
4619        // twice → n2 = 1, N2 = 2.
4620        for pattern in ["s/a/b/", "s{a}{b}", "tr/a/b/", "y/a/b/"] {
4621            assert_halstead_counts::<PerlParser>(
4622                &format!("$s =~ {pattern} and $s =~ {pattern};\n"),
4623                "foo.pl",
4624                [5, 8, 1, 2],
4625                &format!("pattern {pattern}"),
4626            );
4627        }
4628    }
4629
4630    #[cfg(feature = "perl")]
4631    #[test]
4632    fn perl_interpolated_pattern_operands_agree_but_operators_do_not() {
4633        // Two things at once (#1314), because they are the same
4634        // measurement: why the three pattern-value spellings route
4635        // through `string_operand_type` rather than a plain operand
4636        // arm, and what that routing does *not* fix.
4637        //
4638        // `m/$x/` and `qr/$x/` emit a real `Interpolation` wrapping a
4639        // `scalar_variable`, while the bare form keeps its `$x` inside
4640        // an unclassified `regex_pattern_content`. So:
4641        //
4642        // * Operands agree at n2 = N2 = 2 (`$s` plus one contribution
4643        //   from the pattern) only because of the interpolation guard.
4644        //   A plain operand arm would count the wrapper *and* the inner
4645        //   `$x` for the suffixed forms — n2 = 3 — reintroducing
4646        //   through the back door the divergence
4647        //   `perl_every_pattern_value_spelling_scores_alike` exists to
4648        //   prevent. Which node carries the one operand still differs
4649        //   by spelling: the wrapper for the bare form, the inner `$x`
4650        //   for the other two.
4651        // * Operators do *not* agree: the exposed `scalar_variable`
4652        //   brings a `$` sigil, an operator here, that the bare form
4653        //   has no node for. Over two matches N1 is 6 for `/$x/` and
4654        //   8 for the other two.
4655        //
4656        // The operator asymmetry is a grammar gap this classifier
4657        // cannot repair — there is nothing to classify in the bare
4658        // form — so it is pinned rather than papered over, the same
4659        // treatment `perl_division_emits_no_slash_token` gives the
4660        // missing division token. A bump that starts exposing the bare
4661        // form's interpolation turns this red, at which point the
4662        // expectations above need re-deriving.
4663        // Each fixture matches twice, so `N1 > n1` and `N2 > n2` and no
4664        // row is a square tuple that a transposition inside
4665        // `assert_halstead_counts` could pass (#1312).
4666        assert_halstead_counts::<PerlParser>(
4667            "$s =~ /$x/ and $s =~ /$x/;\n",
4668            "foo.pl",
4669            [4, 6, 2, 4],
4670            "bare /$x/",
4671        );
4672        for pattern in ["m/$x/", "qr/$x/"] {
4673            assert_halstead_counts::<PerlParser>(
4674                &format!("$s =~ {pattern} and $s =~ {pattern};\n"),
4675                "foo.pl",
4676                [4, 8, 2, 4],
4677                &format!("suffixed {pattern}"),
4678            );
4679        }
4680    }
4681
4682    #[cfg(feature = "perl")]
4683    #[test]
4684    fn perl_division_emits_no_slash_token() {
4685        // Drift marker, not an endorsement. Ruby's counterpart
4686        // (`ruby_division_survives_the_regex_guard`) proves #1312's
4687        // guard cannot swallow a real division; Perl has no such
4688        // fixture to write, because at the pinned grammar `$a / $b`
4689        // emits *no* `SLASH` token at all — `binary_expression`'s
4690        // children skip straight from one `scalar_variable` to the
4691        // other. Perl division therefore counts zero operators today,
4692        // a pre-existing grammar gap this fix neither causes nor
4693        // repairs.
4694        //
4695        // Pinning it keeps the gap in CI: a bump that starts emitting
4696        // the token turns this red, at which point the division would
4697        // begin counting (its parent is `BinaryExpression`, not
4698        // `PatternMatcher`, so the guard leaves it alone) and the
4699        // expectations above need re-deriving.
4700        //
4701        // The same gap is why no Perl test can distinguish the
4702        // parent-scoped guard from an ancestor-scoped one: with no
4703        // `SLASH` reachable below a `PatternMatcher`, that mutant is
4704        // unobservable here — measured, not assumed. Perl's guard is
4705        // parent-scoped for correctness by construction and for
4706        // symmetry with Ruby's, where the distinction *is* observable
4707        // and is pinned by
4708        // `ruby_regex_guard_is_parent_scoped_not_ancestor_scoped`.
4709        let path = PathBuf::from("foo.pl");
4710        let source = "my $z = $a / $b;\n";
4711        let parser = PerlParser::new(source.as_bytes().to_vec(), &path, None);
4712        assert!(
4713            !ast_has_kind_id(&parser, Perl::SLASH as u16),
4714            "tree-sitter-perl still emits no SLASH for `{source}`"
4715        );
4716        // Anchor the negative assertion to *this* fixture. Without it
4717        // the test stays green when `source` is edited to something
4718        // containing no division at all — measured: swapping in
4719        // `my $z = 1;` failed nothing.
4720        //
4721        // expected: operators `my`, `=`, `$` × 3 (one per
4722        // `scalar_variable`), `;` → n1 = 4, N1 = 6, with no `/` among
4723        // them. Operands `$z`, `$a`, `$b` → n2 = N2 = 3.
4724        check_metrics::<PerlParser>(source, "foo.pl", |metric| {
4725            assert_eq!(metric.halstead.unique_operators(), 4);
4726            assert_eq!(metric.halstead.total_operators(), 6);
4727            assert_eq!(metric.halstead.unique_operands(), 3);
4728            assert_eq!(metric.halstead.total_operands(), 3);
4729        });
4730        // Positive control: the same kind *is* reachable in this
4731        // grammar, so the assertion above is about division and not
4732        // about `Perl::SLASH` being enum-only dead weight.
4733        let matcher = PerlParser::new(b"$s =~ /abc/;\n".to_vec(), &path, None);
4734        assert!(
4735            ast_has_kind_id(&matcher, Perl::SLASH as u16),
4736            "Perl::SLASH must be the bare pattern delimiter kind"
4737        );
4738    }
4739
4740    #[cfg(feature = "perl")]
4741    #[test]
4742    fn perl_readline_closer_alias_never_reaches_kind_id() {
4743        // Drift marker for `Perl::GT2` (#1395). The grammar closes
4744        // `<FH>` / `<$fh>` with `token.immediate('>')`, which the
4745        // generated parser gives its own symbol — hence the `GT2`
4746        // variant sitting next to `FileHandle` in the enum — but whose
4747        // `public_symbol_map` entry collapses it onto `GT` before
4748        // `kind_id()`, exactly like `LPAREN2` in #768 and Ruby's
4749        // `SLASH2` in #1312. Nothing in `getter/perl.rs` names it, and
4750        // nothing should: `compute_halstead` keys a non-primitive
4751        // operator on `kind_id()`, so two live kinds both rendering
4752        // `">"` would split one `>` across two `n1` entries. If a bump
4753        // ever starts emitting it, the repair is to route `>` through
4754        // the lexeme-keyed map, not to list both — and this test is
4755        // what says so.
4756        //
4757        // The C++ grammars' template-list closer collapses the same way;
4758        // `cpp_template_closer_alias_never_reaches_kind_id` pins that.
4759        let path = PathBuf::from("foo.pl");
4760        for source in ["my $l = <FH>;\n", "my $l = <$fh>;\n", "my $b = $x > $y;\n"] {
4761            let parser = PerlParser::new(source.as_bytes().to_vec(), &path, None);
4762            assert!(
4763                !ast_has_kind_id(&parser, Perl::GT2 as u16),
4764                "Perl::GT2 must stay collapsed to Perl::GT for `{source}`"
4765            );
4766            // Positive control: the id the operator arm fires on is
4767            // present, so the assertion above cannot pass merely
4768            // because the fixture grew no `>` at all.
4769            assert!(
4770                ast_has_kind_id(&parser, Perl::GT as u16),
4771                "Perl::GT must be the `>` kind for `{source}`"
4772            );
4773        }
4774    }
4775
4776    /// Drift marker for `Cpp::GT2` / `Mozcpp::GT2`. Both grammars close
4777    /// a template parameter or argument list with
4778    /// `alias(token(prec(1, '>')), '>')`, which the generated parser
4779    /// numbers separately, but `ts_symbol_map` folds that symbol onto
4780    /// `GT` before `kind_id()`, as with Perl's readline closer above. The
4781    /// `GT2` arms in `getter/cpp.rs` and `getter/mozcpp.rs` are therefore
4782    /// defensive; this fails if a grammar bump makes them live, at which
4783    /// point the two kinds would split one `>` across two `n1` entries.
4784    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
4785    fn assert_template_closer_folds_to_gt<P: crate::ParserTrait>(gt: u16, gt2: u16, label: &str) {
4786        // One parameter list (`S<T>`'s declaration) and two argument
4787        // lists, the inner one closed by the first half of a `>>`.
4788        let source = "template <typename T> struct S {};\n\
4789                      std::vector<std::vector<int>> v;\n\
4790                      bool b = x > y;\n";
4791        let parser = P::new(source.as_bytes().to_vec(), &PathBuf::from("foo.cpp"), None);
4792        assert!(
4793            !ast_has_kind_id(&parser, gt2),
4794            "{label}::GT2 must stay collapsed to {label}::GT",
4795        );
4796        // Positive control: every template-list closer is present and
4797        // carries `GT`, so the assertion above cannot pass merely
4798        // because the fixture stopped producing template lists.
4799        let closers = parser
4800            .root()
4801            .preorder()
4802            .filter(|n| {
4803                n.kind_id() == gt
4804                    && n.parent().is_some_and(|p| {
4805                        matches!(
4806                            p.kind(),
4807                            "template_parameter_list" | "template_argument_list"
4808                        )
4809                    })
4810            })
4811            .count();
4812        assert_eq!(
4813            closers, 3,
4814            "{label}: each template-list closer must be a `GT`"
4815        );
4816    }
4817
4818    #[cfg(feature = "cpp")]
4819    #[test]
4820    fn cpp_template_closer_alias_never_reaches_kind_id() {
4821        assert_template_closer_folds_to_gt::<CppParser>(Cpp::GT as u16, Cpp::GT2 as u16, "Cpp");
4822    }
4823
4824    #[cfg(feature = "mozcpp")]
4825    #[test]
4826    fn mozcpp_template_closer_alias_never_reaches_kind_id() {
4827        assert_template_closer_folds_to_gt::<MozcppParser>(
4828            Mozcpp::GT as u16,
4829            Mozcpp::GT2 as u16,
4830            "Mozcpp",
4831        );
4832    }
4833
4834    /// Asserts a grammar's wrapped generic-list opener, `LT2`, never
4835    /// reaches a node, so the `LT` its getter pairs as `<>` is the only
4836    /// `<` a list opens with (#1559, grammar-dispatch §2). `lists` are the
4837    /// lists the wrapped token opens; the positive control counts their
4838    /// `LT` openers, so the assertion cannot pass on a fixture that
4839    /// stopped producing those lists.
4840    #[cfg(any(feature = "groovy", feature = "objc", feature = "rust"))]
4841    #[track_caller]
4842    fn assert_generic_opener_folds_to_lt<P: crate::ParserTrait>(
4843        source: &str,
4844        [lt, lt2]: [u16; 2],
4845        lists: &[&str],
4846        openers: usize,
4847        label: &str,
4848    ) {
4849        let parser = P::new(source.as_bytes().to_vec(), &PathBuf::from("foo"), None);
4850        assert!(
4851            !ast_has_kind_id(&parser, lt2),
4852            "{label}::LT2 must stay collapsed to {label}::LT",
4853        );
4854        let found = parser
4855            .root()
4856            .preorder()
4857            .filter(|n| n.kind_id() == lt && n.parent().is_some_and(|p| lists.contains(&p.kind())))
4858            .count();
4859        assert_eq!(
4860            found, openers,
4861            "{label}: each generic-list opener must be an `LT`"
4862        );
4863    }
4864
4865    #[cfg(feature = "rust")]
4866    #[test]
4867    fn rust_generic_opener_alias_never_reaches_kind_id() {
4868        assert_generic_opener_folds_to_lt::<RustParser>(
4869            "fn u<T>(x: Vec<T>) -> impl Sized + use<T> { x }\n",
4870            [Rust::LT as u16, Rust::LT2 as u16],
4871            &["type_arguments", "use_bounds"],
4872            2,
4873            "Rust",
4874        );
4875    }
4876
4877    #[cfg(feature = "groovy")]
4878    #[test]
4879    fn groovy_generic_opener_alias_never_reaches_kind_id() {
4880        assert_generic_opener_folds_to_lt::<GroovyParser>(
4881            "class A<T> { List<T> v }\n",
4882            [Groovy::LT as u16, Groovy::LT2 as u16],
4883            &["type_parameters", "type_arguments"],
4884            2,
4885            "Groovy",
4886        );
4887    }
4888
4889    #[cfg(feature = "objc")]
4890    #[test]
4891    fn objc_generic_opener_alias_never_reaches_kind_id() {
4892        assert_generic_opener_folds_to_lt::<ObjcParser>(
4893            "int f(void) { return g(NSArray<NSString *>); }\n",
4894            [Objc::LT as u16, Objc::LT2 as u16],
4895            &["argument_list"],
4896            1,
4897            "Objc",
4898        );
4899    }
4900
4901    #[cfg(feature = "perl")]
4902    #[test]
4903    fn perl_readline_brackets_are_operators() {
4904        // #1395: a readline's `<` / `>` delimit syntax, not a literal,
4905        // so under the policy on `Getter::get_op_type` they are
4906        // ordinary operators — even though ABC's `conditions` excludes
4907        // them (#1297). `<STDIN>` is the one spelling that cannot show
4908        // this: the grammar lexes it as a single `standard_input`
4909        // token, so there are no brackets to classify.
4910        //
4911        // expected: operators `sub`, `{}`, `my`, `$` × 2 (one per
4912        // `scalar_variable`), `=`, `<`, `>`, `;` × 2, `return` →
4913        // n1 = 9, N1 = 11. Operands `r`, `$l` × 2, `FH` →
4914        // n2 = 3, N2 = 4.
4915        check_metrics::<PerlParser>("sub r { my $l = <FH>; return $l; }\n", "foo.pl", |metric| {
4916            assert_eq!(metric.halstead.unique_operators(), 9);
4917            assert_eq!(metric.halstead.total_operators(), 11);
4918            assert_eq!(metric.halstead.unique_operands(), 3);
4919            assert_eq!(metric.halstead.total_operands(), 4);
4920        });
4921    }
4922
4923    /// Every (name wrapper, contained operand) pairing tree-sitter-perl's
4924    /// node-types.json admits, and the single source of truth for both
4925    /// halves of #1355's guard: its parent set is the distinct first
4926    /// components, the kinds it subsumes the distinct second ones.
4927    /// `perl_name_wrappers_bill_the_name_once_1355` witnesses every row
4928    /// and fails on an eleventh pairing.
4929    #[cfg(feature = "perl")]
4930    const PERL_NAME_WRAPPER_PAIRINGS: [(Perl, Perl); 10] = [
4931        (Perl::PackageName, Perl::Identifier),
4932        (Perl::PackageName, Perl::ScalarVariable),
4933        (Perl::PackageName, Perl::ArrayVariable),
4934        (Perl::PackageName, Perl::HashVariable),
4935        (Perl::PackageName, Perl::SpecialScalarVariable),
4936        (Perl::PackageName, Perl::Typeglob),
4937        (Perl::PackageName, Perl::PackageVariable),
4938        (Perl::PackageVariable, Perl::PackageName),
4939        (Perl::PackageVariable, Perl::ScalarVariable),
4940        (Perl::Typeglob, Perl::Identifier),
4941    ];
4942
4943    /// The anonymous tokens those wrappers also hold. `::` and `*` are
4944    /// operators (matched above the guard, so they keep that reading);
4945    /// `{` folds into the `{}` glyph and `}` has never been classified
4946    /// at all. Listing them is what lets
4947    /// `perl_name_wrappers_bill_the_name_once_1355` police *every*
4948    /// child rather than only the named ones — the operand arm carries
4949    /// token-shaped kinds too (`True`, `FILE`, `SUB`, …), and a bump
4950    /// that let one of those inside a wrapper would otherwise be
4951    /// silenced with nothing failing.
4952    #[cfg(feature = "perl")]
4953    const PERL_NAME_WRAPPER_TOKENS: [Perl; 4] =
4954        [Perl::COLONCOLON, Perl::STAR, Perl::LBRACE, Perl::RBRACE];
4955
4956    /// The occurrences #1355's guard suppresses in `source`, paired
4957    /// with the (wrapper kind, child kind) pairings they witness.
4958    ///
4959    /// Walks with `for_each_node_with_chain`, which maintains the
4960    /// ancestor chain exactly as `spaces::compute` does, so "parent"
4961    /// here means what `Ancestors::parent` means inside the guard
4962    /// rather than what a differently-built chain would say.
4963    #[cfg(feature = "perl")]
4964    fn perl_subsumed_operands(source: &str) -> (Vec<String>, HashSet<(u16, u16)>) {
4965        let wrappers: HashSet<u16> = PERL_NAME_WRAPPER_PAIRINGS
4966            .map(|(wrapper, _)| wrapper as u16)
4967            .into();
4968        let subsumed: HashSet<u16> = PERL_NAME_WRAPPER_PAIRINGS
4969            .map(|(_, child)| child as u16)
4970            .into();
4971        let tokens: HashSet<u16> = PERL_NAME_WRAPPER_TOKENS.map(|kind| kind as u16).into();
4972        let code = source.as_bytes();
4973        let mut hidden = Vec::new();
4974        let mut pairings = HashSet::new();
4975        for_each_node_with_chain::<PerlCode>(code, |node, chain| {
4976            let Some(parent) = chain.last() else { return };
4977            if !wrappers.contains(&parent.kind_id()) {
4978                return;
4979            }
4980            assert!(
4981                subsumed.contains(&node.kind_id()) || tokens.contains(&node.kind_id()),
4982                "`{source}`: a `{}` inside a `{}` is a child this guard was not \
4983                 derived against; re-read node-types.json before trusting it",
4984                node.kind(),
4985                parent.kind(),
4986            );
4987            if subsumed.contains(&node.kind_id()) {
4988                pairings.insert((parent.kind_id(), node.kind_id()));
4989                hidden.push(
4990                    node.utf8_text(code)
4991                        .expect("fixture is valid UTF-8")
4992                        .to_owned(),
4993                );
4994            }
4995        });
4996        (hidden, pairings)
4997    }
4998
4999    /// One row of `perl_name_wrappers_bill_the_name_once_1355`'s
5000    /// table: a fixture, what it must measure now, what it measured
5001    /// before #1355, and the operand text behind the counts.
5002    struct PerlNameWrapperCase {
5003        source: &'static str,
5004        /// `[n1, N1, n2, N2]` with the guard in place.
5005        counts: [u64; 4],
5006        /// `[n2, N2]` without it. Re-derived by the loop rather than
5007        /// trusted, so a stale row fails instead of misinforming.
5008        before: [u64; 2],
5009        operands: &'static [&'static str],
5010    }
5011
5012    /// Regression for #1355. `package_name`, `package_variable` and
5013    /// `typeglob` are operands spanning a whole name, so every
5014    /// operand-classified node *inside* one was billed a second time:
5015    /// `use strict;` scored `N2` 2 for one name, `our $Foo::count = 3;`
5016    /// n2 5 / N2 6 for two, and the vocabulary grew a bare `::` entry
5017    /// because a `package_variable`'s qualifier slot is itself a
5018    /// childless `package_name`.
5019    ///
5020    /// Each row's `before` column is what it measured without the
5021    /// guard, and the loop re-derives both halves from the current
5022    /// parse rather than trusting the column — the guard is the only
5023    /// difference between the two classifications, so the occurrences
5024    /// it removes are exactly the subsumed-kind children of a wrapper:
5025    ///
5026    /// - `N2` before minus `N2` after must equal how many of those
5027    ///   there are. That identity *is* the defect: one spurious operand
5028    ///   per contained name part.
5029    /// - `n2` before is the post-fix vocabulary unioned with their
5030    ///   texts. It exceeds `n2` after wherever a part's spelling is not
5031    ///   already an operand on its own (`Data`, `::`, `count`).
5032    ///
5033    /// The walk doubles as the grammar-dispatch §1 / §2 drift marker.
5034    /// It asserts that every *named* child of a wrapper is one of the
5035    /// eight subsumed kinds — which is what makes keying the arm on the
5036    /// parent alone safe — and that all ten pairings node-types.json
5037    /// admits are exercised here, so a bump that renumbers or re-parents
5038    /// one fails loudly instead of leaving the counts below measuring a
5039    /// construct the arm no longer reaches.
5040    ///
5041    /// Two mutants this does *not* catch, measured rather than assumed.
5042    /// Widening the guard from parent- to ancestor-scoped fails nothing,
5043    /// for the reason `perl_division_emits_no_slash_token` already
5044    /// records about the other guard in this getter: every operand-kinded
5045    /// descendant of a wrapper is also a direct child of one, and the
5046    /// intervening sigil tokens are matched by the operator arm above
5047    /// before the guard is reached. Parent-scoping stands on
5048    /// grammar-dispatch §5 and on symmetry with that guard, not on a
5049    /// test. What *is* pinned is the arm's position: moving it above the
5050    /// operator arm swallows `::`, `*` and the typeglob's opening brace,
5051    /// and the operator columns below fail.
5052    #[cfg(feature = "perl")]
5053    #[test]
5054    fn perl_name_wrappers_bill_the_name_once_1355() {
5055        let cases: [PerlNameWrapperCase; 12] = [
5056            // identifier under package_name, the single-segment form.
5057            PerlNameWrapperCase {
5058                source: "use strict;\n",
5059                counts: [2, 2, 1, 1],
5060                before: [1, 2],
5061                operands: &["strict"],
5062            },
5063            // …and the multi-segment one, twice over.
5064            PerlNameWrapperCase {
5065                source: "require Data::Dumper;\n",
5066                counts: [3, 3, 1, 1],
5067                before: [3, 3],
5068                operands: &["Data::Dumper"],
5069            },
5070            PerlNameWrapperCase {
5071                source: "package Foo::Bar;\n",
5072                counts: [3, 3, 1, 1],
5073                before: [3, 3],
5074                operands: &["Foo::Bar"],
5075            },
5076            // The `bar` of a qualified call is a *sibling* of the
5077            // `package_name`, not a child, so it still counts while the
5078            // `Foo` inside the wrapper does not. That is the row saying
5079            // the guard reads position and not kind: keying it on the
5080            // child kinds instead fails 14 tests here, this one among
5081            // them.
5082            PerlNameWrapperCase {
5083                source: "Foo::bar();\n",
5084                counts: [3, 3, 2, 2],
5085                before: [2, 3],
5086                operands: &["Foo", "bar"],
5087            },
5088            // identifier under typeglob, bare and brace-delimited.
5089            PerlNameWrapperCase {
5090                source: "my $g = *STDOUT;\n",
5091                counts: [5, 5, 2, 2],
5092                before: [3, 3],
5093                operands: &["$g", "*STDOUT"],
5094            },
5095            PerlNameWrapperCase {
5096                source: "my $t = *{Foo};\n",
5097                counts: [6, 6, 2, 2],
5098                before: [3, 3],
5099                operands: &["$t", "*{Foo}"],
5100            },
5101            // package_name and scalar_variable under package_variable,
5102            // and scalar_variable under package_name — the reported
5103            // fixture, where `$Foo` was billed twice and `::` once.
5104            PerlNameWrapperCase {
5105                source: "our $Foo::count = 3;\n",
5106                counts: [4, 4, 2, 2],
5107                before: [5, 6],
5108                operands: &["$Foo::count", "3"],
5109            },
5110            // array_variable / hash_variable / special_scalar_variable
5111            // under package_name: the same shape with the other sigils.
5112            PerlNameWrapperCase {
5113                source: "my @l = @Foo::list;\n",
5114                counts: [3, 3, 2, 2],
5115                before: [5, 6],
5116                operands: &["@l", "@Foo::list"],
5117            },
5118            PerlNameWrapperCase {
5119                source: "my %h = %Foo::hash;\n",
5120                counts: [3, 3, 2, 2],
5121                before: [5, 6],
5122                operands: &["%h", "%Foo::hash"],
5123            },
5124            PerlNameWrapperCase {
5125                source: "my $z = $_::x;\n",
5126                counts: [4, 5, 2, 2],
5127                before: [5, 6],
5128                operands: &["$z", "$_::x"],
5129            },
5130            // typeglob under package_name.
5131            PerlNameWrapperCase {
5132                source: "*Foo::glob = 1;\n",
5133                counts: [3, 3, 2, 2],
5134                before: [6, 7],
5135                operands: &["*Foo::glob", "1"],
5136            },
5137            // package_variable under package_name: the nesting that
5138            // makes qualifier depth unbounded. One variable reference
5139            // used to spell seven vocabulary entries.
5140            PerlNameWrapperCase {
5141                source: "my $x = $Foo::Bar::baz;\n",
5142                counts: [4, 5, 2, 2],
5143                before: [7, 10],
5144                operands: &["$x", "$Foo::Bar::baz"],
5145            },
5146        ];
5147
5148        let mut witnessed: HashSet<(u16, u16)> = HashSet::new();
5149        for PerlNameWrapperCase {
5150            source,
5151            counts,
5152            before: [n2_before, total_before],
5153            operands,
5154        } in cases
5155        {
5156            let (hidden, pairings) = perl_subsumed_operands(source);
5157            assert!(
5158                !hidden.is_empty(),
5159                "row {source:?} contains no name-wrapper child, so it witnesses nothing",
5160            );
5161            witnessed.extend(pairings);
5162
5163            // Phrased as an addition rather than a subtraction so a
5164            // future edit that inverts the two underflows nothing and
5165            // fails with the message below.
5166            assert_eq!(
5167                total_before,
5168                counts[3] + hidden.len() as u64,
5169                "row {source:?} must shed exactly one operand per contained \
5170                 name part; recorded N2_before {total_before}, parts {hidden:?}",
5171            );
5172            let mut vocabulary: HashSet<&str> = operands.iter().copied().collect();
5173            vocabulary.extend(hidden.iter().map(String::as_str));
5174            assert_eq!(
5175                vocabulary.len() as u64,
5176                n2_before,
5177                "row {source:?}: n2 before the fix is the post-fix vocabulary \
5178                 plus the contained name parts; got {vocabulary:?}",
5179            );
5180
5181            assert_halstead_counts::<PerlParser>(source, "foo.pl", counts, source);
5182            assert_ops_operands::<PerlParser>(source, "foo.pl", operands.len(), operands.to_vec());
5183        }
5184
5185        let mut got: Vec<(u16, u16)> = witnessed.into_iter().collect();
5186        got.sort_unstable();
5187        let mut expected_pairings: Vec<(u16, u16)> = PERL_NAME_WRAPPER_PAIRINGS
5188            .map(|(wrapper, child)| (wrapper as u16, child as u16))
5189            .into();
5190        expected_pairings.sort_unstable();
5191        assert_eq!(
5192            got, expected_pairings,
5193            "the table must exercise every (wrapper, child) pairing \
5194             node-types.json admits, and no other",
5195        );
5196    }
5197
5198    /// The over-suppression half of #1355 (grammar-dispatch §6 and §11).
5199    /// The guard is keyed on the parent, so the same kinds it silences
5200    /// inside a name wrapper have to keep counting everywhere else —
5201    /// otherwise "one operand per name" would have been bought by
5202    /// zeroing ordinary variables and calls.
5203    ///
5204    /// `module_name` rides along because the issue asserted it shares
5205    /// the `identifier` leaf and would be collateral damage. It does
5206    /// not: `use 'Foo.pm'` parses to a leaf holding only its two quote
5207    /// tokens, so it wraps nothing and is untouched either way.
5208    #[cfg(feature = "perl")]
5209    #[test]
5210    fn perl_qw_list_bills_one_operand_per_element() {
5211        // `qw(a b c)` was invisible to Halstead — neither the elements,
5212        // the wrapper nor the `qw` keyword had an arm — so it billed
5213        // nothing where its synonym `("a", "b", "c")` billed three
5214        // operands. Each `list_item` is now one operand and the
5215        // `word_list_qw` wrapper is gated on holding one, the #1353
5216        // Ruby `%w[]` rule: one operand per element, or one for the
5217        // empty literal.
5218        assert_ops_operands::<PerlParser>(
5219            "my @a = qw(a b c);\n",
5220            "foo.pl",
5221            4,
5222            vec!["@a", "a", "b", "c"],
5223        );
5224        assert_ops_operands::<PerlParser>("my @a = qw();\n", "foo.pl", 2, vec!["@a", "qw()"]);
5225        assert_ops_operands::<PerlParser>(
5226            "use POSIX qw(floor ceil);\n",
5227            "foo.pl",
5228            3,
5229            vec!["POSIX", "floor", "ceil"],
5230        );
5231        // expected: [n1, N1, n2, N2] = [3, 3, 4, 4] for every delimiter —
5232        // operators `my`, `=`, `;`; the `qw` keyword and its delimiters
5233        // are unclassified, as Ruby's `%w[` is, so the choice of
5234        // delimiter cannot move the score (#1312).
5235        for spelling in [
5236            "qw(a b c)",
5237            "qw/a b c/",
5238            "qw{a b c}",
5239            "qw[a b c]",
5240            "qw<a b c>",
5241        ] {
5242            assert_halstead_counts::<PerlParser>(
5243                &format!("my @a = {spelling};\n"),
5244                "foo.pl",
5245                [3, 3, 4, 4],
5246                spelling,
5247            );
5248        }
5249        // The synonym: the same four operands, plus the `()` and `,`
5250        // operators the list spelling carries.
5251        assert_halstead_counts::<PerlParser>(
5252            "my @a = (\"a\", \"b\", \"c\");\n",
5253            "foo.pl",
5254            [5, 6, 4, 4],
5255            "list literal",
5256        );
5257    }
5258
5259    #[cfg(feature = "perl")]
5260    #[test]
5261    fn perl_qualified_name_leaves_still_count_elsewhere_1355() {
5262        // expected: operators `my` × 4, `$` × 3 (one per `$`-sigilled
5263        // variable), `=` × 4, `;` × 5, `()` × 3 and the fat comma
5264        // → n1 = 6, N1 = 20. Operands are the four declared variables,
5265        // the three integers, the hash key, `$_` and the call target
5266        // → n2 = N2 = 10. Five of the eight kinds the guard silences
5267        // under a name wrapper appear among them — `scalar_variable`,
5268        // `array_variable`, `hash_variable`, `special_scalar_variable`
5269        // and `identifier` — and all five still count here.
5270        let bare = "my $x = 1;\nmy @a = (2);\nmy %h = (k => 3);\nmy $u = $_;\nfoo();\n";
5271        assert_halstead_counts::<PerlParser>(bare, "foo.pl", [6, 20, 10, 10], bare);
5272        assert_ops_operands::<PerlParser>(
5273            bare,
5274            "foo.pl",
5275            10,
5276            vec!["$x", "1", "@a", "2", "%h", "k", "3", "$u", "$_", "foo"],
5277        );
5278        let (hidden, _) = perl_subsumed_operands(bare);
5279        assert!(
5280            hidden.is_empty(),
5281            "no name wrapper appears here, so the guard must be inert; got {hidden:?}",
5282        );
5283
5284        // expected: operators `use`, `;` → n1 = N1 = 2; the quoted
5285        // module name is the sole operand → n2 = N2 = 1.
5286        let quoted = "use 'Some.pm';\n";
5287        assert_halstead_counts::<PerlParser>(quoted, "foo.pl", [2, 2, 1, 1], quoted);
5288        assert_ops_operands::<PerlParser>(quoted, "foo.pl", 1, vec!["'Some.pm'"]);
5289        assert!(
5290            ast_has_kind_id(
5291                &PerlParser::new(quoted.as_bytes().to_vec(), &PathBuf::from("foo.pl"), None),
5292                Perl::ModuleName as u16,
5293            ),
5294            "the quoted `use` form no longer parses to `module_name`, so this \
5295             row no longer says anything about it",
5296        );
5297    }
5298
5299    #[cfg(feature = "lua")]
5300    #[test]
5301    fn lua_operators_and_operands() {
5302        check_metrics::<LuaParser>(
5303            "local function add(a, b)
5304  local result = a + b
5305  if result > 0 then
5306    return result
5307  end
5308  return 0
5309end",
5310            "foo.lua",
5311            |metric| {
5312                // n1=11: local,function,(,,,=,+,if,>,then,return,end
5313                // (after #695 the `)` closer no longer counts — only the
5314                // folded `(` opener does; was n1=12).
5315                // n2=5: add,a,b,result,0
5316                insta::assert_json_snapshot!(metric.halstead, @r#"
5317                {
5318                  "unique_operators": 11,
5319                  "total_operators": 14,
5320                  "unique_operands": 5,
5321                  "total_operands": 10,
5322                  "length": 24,
5323                  "estimated_program_length": 49.66338827944708,
5324                  "purity_ratio": 2.0693078449769615,
5325                  "vocabulary": 16,
5326                  "volume": 96.0,
5327                  "difficulty": 11.0,
5328                  "level": 0.09090909090909091,
5329                  "effort": 1056.0,
5330                  "time": 58.666666666666664,
5331                  "bugs": 0.03456644293839657
5332                }
5333                "#);
5334            },
5335        );
5336    }
5337
5338    /// Regression for #695. Lua/Bash/Tcl/iRules/PHP/Ruby/Elixir used to
5339    /// classify the *closing* delimiter (`)`/`]`/`}`) as a separate
5340    /// operator, while the C-family majority folds each balanced pair to a
5341    /// single glyph via `get_operator_id_as_str` and counts only the
5342    /// opener. A balanced `(1)` therefore double-counted as `()` + `)`,
5343    /// inflating n1 and N1. With the fix only the folded `(` opener counts:
5344    /// `local x = (1)` yields operators `local`, `=`, `()` — n1 = N1 = 3,
5345    /// with no standalone `)`.
5346    #[cfg(feature = "lua")]
5347    #[test]
5348    fn lua_balanced_paren_counts_opener_only() {
5349        let source = "local x = (1)\n";
5350        let path = PathBuf::from("foo.lua");
5351        let parser = LuaParser::new(source.as_bytes().to_vec(), &path, None);
5352        let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
5353        let paren = ops.operators.iter().filter(|o| o.as_str() == "()").count();
5354        assert_eq!(
5355            paren, 1,
5356            "balanced `(1)` must be one `()` operator; operators were {:?}",
5357            ops.operators
5358        );
5359        assert!(
5360            !ops.operators.iter().any(|o| o.as_str() == ")"),
5361            "the closing `)` must not be a separate operator; operators were {:?}",
5362            ops.operators
5363        );
5364    }
5365
5366    /// Guard for #768. Several `get_op_type` impls (Cpp/C/Objc/Mozcpp/
5367    /// Tcl/iRules/Php/Elixir/Ruby) classify a grammar's *second-alias*
5368    /// opener — `LPAREN2`, and for Elixir/Ruby `LBRACK2`/`LBRACK3` — as a
5369    /// Halstead operator alongside the base `LPAREN`/`LBRACK`. #768 worried
5370    /// that an alias opener would reach `compute_halstead` with a kind_id
5371    /// distinct from the base, inflating n1 (a second `()` entry) and
5372    /// rendering a bare `"("` instead of the folded `"()"`.
5373    ///
5374    /// That cannot happen: tree-sitter's runtime collapses each alias to
5375    /// its base via the grammar's `public_symbol_map` *before*
5376    /// `Node::kind_id()` (`ts_node_symbol`) ever returns. So the alias
5377    /// kind_id is unobservable to the metric layer and the alias match arms
5378    /// are defensive — they only fire if a future grammar bump drops that
5379    /// collapse. This test pins the invariant: parsing the exact
5380    /// constructs each grammar produces the alias for internally
5381    /// (pp-conditional `defined(...)` for Cpp; call arg-list / subscript /
5382    /// constant-array-pattern for Ruby) must yield **no** node carrying the
5383    /// alias kind_id, and the balanced opener must count once and render as
5384    /// the pair glyph. If a grammar bump makes an alias id observable, this
5385    /// goes red and signals that the alias arms must additionally fold to
5386    /// the base in `get_operator_id_as_str` (the fix #768 proposed).
5387    #[cfg(all(feature = "c", feature = "cpp", feature = "elixir", feature = "ruby"))]
5388    #[test]
5389    fn second_alias_opener_collapses_to_base_kind_id() {
5390        #[cfg(any(feature = "c", feature = "cpp", feature = "elixir", feature = "ruby"))]
5391        fn assert_no_alias<T: crate::ParserTrait>(
5392            source: &str,
5393            file: &str,
5394            alias_id: u16,
5395            alias_name: &str,
5396        ) {
5397            let path = PathBuf::from(file);
5398            let parser = T::new(source.as_bytes().to_vec(), &path, None);
5399            let mut stack = vec![parser.root()];
5400            while let Some(node) = stack.pop() {
5401                assert_ne!(
5402                    node.kind_id(),
5403                    alias_id,
5404                    "{alias_name} (kind_id {alias_id}) must never reach kind_id() \
5405                     for `{source}`; the runtime public_symbol_map should have \
5406                     collapsed it to the base opener. If this fires after a \
5407                     grammar bump, fold {alias_name} to its pair glyph in \
5408                     get_operator_id_as_str (issue #768)."
5409                );
5410                for child in node.children() {
5411                    stack.push(child);
5412                }
5413            }
5414        }
5415
5416        // Balanced openers must count once and render folded (no bare
5417        // `(`/`[`, no n1 inflation) — the property #768 feared was broken.
5418        #[cfg(any(feature = "c", feature = "cpp", feature = "elixir", feature = "ruby"))]
5419        fn assert_folded_openers<T: crate::MetricSuite>(source: &str, file: &str) {
5420            let path = PathBuf::from(file);
5421            let parser = T::new(source.as_bytes().to_vec(), &path, None);
5422            let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
5423            assert!(
5424                !ops.operators.iter().any(|o| o.as_str() == "("),
5425                "no bare `(` operator (must fold to `()`); operators were {:?}",
5426                ops.operators
5427            );
5428            assert!(
5429                !ops.operators.iter().any(|o| o.as_str() == "["),
5430                "no bare `[` operator (must fold to `[]`); operators were {:?}",
5431                ops.operators
5432            );
5433            // Each pair glyph appears at most once — the alias does not add
5434            // a second `()`/`[]` entry to n1.
5435            assert!(
5436                ops.operators.iter().filter(|o| o.as_str() == "()").count() <= 1,
5437                "`()` must be a single n1 entry; operators were {:?}",
5438                ops.operators
5439            );
5440            assert!(
5441                ops.operators.iter().filter(|o| o.as_str() == "[]").count() <= 1,
5442                "`[]` must be a single n1 entry; operators were {:?}",
5443                ops.operators
5444            );
5445        }
5446
5447        // Cpp/C/Mozcpp: LPAREN2 = 20. The grammar emits it internally only
5448        // inside preprocessor-conditional expressions (`#if defined(FOO)`).
5449        assert_no_alias::<crate::CppParser>(
5450            "#if defined(FOO)\n#endif\n",
5451            "a.cpp",
5452            20,
5453            "Cpp::LPAREN2",
5454        );
5455        assert_no_alias::<crate::CParser>("#if defined(FOO)\n#endif\n", "a.c", 20, "C::LPAREN2");
5456
5457        // Ruby: LPAREN2 = 47 (call arg-list), LBRACK3 = 155 (element-
5458        // reference subscript), LBRACK2 = 46 (constant array pattern).
5459        assert_no_alias::<crate::RubyParser>("f(1)\n", "a.rb", 47, "Ruby::LPAREN2");
5460        assert_no_alias::<crate::RubyParser>("a[0]\n", "a.rb", 155, "Ruby::LBRACK3");
5461        assert_no_alias::<crate::RubyParser>(
5462            "case p\nin Point[1, 2] then 1\nend\n",
5463            "a.rb",
5464            46,
5465            "Ruby::LBRACK2",
5466        );
5467
5468        // Elixir: LPAREN2 = 95 (immediate call paren), LBRACK2 = 96
5469        // (access / subscript).
5470        assert_no_alias::<crate::ElixirParser>("f(1)\n", "a.ex", 95, "Elixir::LPAREN2");
5471        assert_no_alias::<crate::ElixirParser>("x[0]\n", "a.ex", 96, "Elixir::LBRACK2");
5472
5473        assert_folded_openers::<crate::CppParser>("int main(){ int a[3]; return a[0]; }", "b.cpp");
5474        assert_folded_openers::<crate::RubyParser>("f(1)\nb = [1]\nb[0]\n", "b.rb");
5475    }
5476
5477    #[cfg(feature = "kotlin")]
5478    #[test]
5479    fn kotlin_halstead_basic() {
5480        check_metrics::<KotlinParser>(
5481            "fun add(a: Int, b: Int): Int {
5482                val result = a + b
5483                return result
5484            }",
5485            "foo.kt",
5486            |metric| {
5487                insta::assert_json_snapshot!(
5488                    metric.halstead,
5489                    @r#"
5490                {
5491                  "unique_operators": 9,
5492                  "total_operators": 11,
5493                  "unique_operands": 5,
5494                  "total_operands": 10,
5495                  "length": 21,
5496                  "estimated_program_length": 40.13896548741762,
5497                  "purity_ratio": 1.9113793089246487,
5498                  "vocabulary": 14,
5499                  "volume": 79.9544533632097,
5500                  "difficulty": 9.0,
5501                  "level": 0.1111111111111111,
5502                  "effort": 719.5900802688873,
5503                  "time": 39.97722668160485,
5504                  "bugs": 0.026767153565498338
5505                }
5506                "#
5507                );
5508            },
5509        );
5510    }
5511
5512    #[cfg(feature = "kotlin")]
5513    #[test]
5514    fn kotlin_string_template_no_double_count() {
5515        // Re-anchored for issue #454. The pre-#454 comment claimed
5516        // kotlin-ng emits an `identifier` node for the short `$name`
5517        // form whose bytes include the leading `$`. That is factually
5518        // false: AST dump shows the short form produces bare
5519        // `string_content` tokens (`$`, then `name`) with **no**
5520        // structured node. The old assertion (u_operands = 4, N2 = 5)
5521        // passed for the wrong reason (lesson 6): the wrapping literal
5522        // was counted (+1) and the inner `name` was dropped (-1), and
5523        // the two errors cancelled. The `$name!` it used also defeats
5524        // recovery because the grammar glues the trailing `!` onto the
5525        // name token.
5526        //
5527        // Correct mechanism (clean end-of-segment short form):
5528        // `fun greet(name: String): String {\n    return "Hi $name"\n}\n`
5529        //   operators: fun, (, ), :, {}, return → as classified.
5530        //   operands by token text:
5531        //     `greet` × 1, `name` × 2 (param + recovered short-interp),
5532        //     `String` × 2 (param type + return type).
5533        //   The wrapping `"Hi $name"` literal is suppressed and the
5534        //   inner `name` recovered → u_operands = 3 (`greet`, `name`,
5535        //   `String`), N2 = 5. Pre-#454: wrapper counted, inner dropped
5536        //   → u_operands = 4, N2 = 6.
5537        check_metrics::<KotlinParser>(
5538            "fun greet(name: String): String {\n    return \"Hi $name\"\n}\n",
5539            "foo.kt",
5540            |metric| {
5541                assert_eq!(metric.halstead.unique_operands(), 3);
5542                assert_eq!(metric.halstead.total_operands(), 5);
5543            },
5544        );
5545        // Lesson 4: the ops store agrees on n2 and the exact operand set
5546        // (inner `name` present, wrapper absent).
5547        assert_ops_operands::<KotlinParser>(
5548            "fun greet(name: String): String {\n    return \"Hi $name\"\n}\n",
5549            "foo.kt",
5550            3,
5551            vec!["greet", "name", "String"],
5552        );
5553    }
5554
5555    #[cfg(feature = "kotlin")]
5556    #[test]
5557    fn kotlin_short_interpolation_counts_inner_not_wrapper() {
5558        // Issue #454: the short `$name` template — distinct from the
5559        // long `${expr}` form, which the kotlin-ng grammar gives a
5560        // structured `interpolation` node (see
5561        // `kotlin_string_template_long_form_no_double_count`). The short
5562        // form has no such node; the variable arrives as a bare
5563        // `string_content` token preceded by a `$` `string_content`.
5564        // The fix recovers the clean-identifier variable as an operand
5565        // and suppresses the opaque wrapper.
5566        //
5567        // `fun f() { val x = 1; println("v=$x") }\n`
5568        //   operands by token text: `f`, `x` × 2 (decl + recovered),
5569        //   `println`, `1`. The wrapping `"v=$x"` is suppressed →
5570        //   u_operands = 4 (`f`, `x`, `println`, `1`), N2 = 5.
5571        // Pre-#454 the wrapper `"v=$x"` counted and the inner `x` was
5572        // dropped → u_operands = 4 but the wrapper, not `x`, was the
5573        // fourth operand, and N2 = 5 with the wrong member — the ops
5574        // assertion below pins the exact set so the cancellation cannot
5575        // hide it.
5576        let src = "fun f() { val x = 1; println(\"v=$x\") }\n";
5577        check_metrics::<KotlinParser>(src, "foo.kt", |metric| {
5578            assert_eq!(metric.halstead.unique_operands(), 4);
5579            assert_eq!(metric.halstead.total_operands(), 5);
5580        });
5581        assert_ops_operands::<KotlinParser>(src, "foo.kt", 4, vec!["f", "x", "println", "1"]);
5582    }
5583
5584    #[cfg(feature = "kotlin")]
5585    #[test]
5586    fn kotlin_short_interpolation_space_separated() {
5587        // Issue #454 follow-up: tree-sitter-kotlin-ng splits the literal
5588        // only at each `$`, so a `$name` segment's name token absorbs any
5589        // trailing inter-segment text into its byte range. For `"$a $b"`
5590        // the token after the first `$` is `"a "` (with the trailing
5591        // space). Pre-fix `kotlin_is_identifier("a ")` returned false and
5592        // the leading variable `a` was silently dropped, yielding
5593        // operands `{b, f, s}` (verified: `a` missing) — breaking parity
5594        // with the long form `"${a} ${b}"`, which recovers `{a, b, f, s}`.
5595        //
5596        // The fix takes the maximal leading-identifier prefix of the name
5597        // token, recovering `a` and keying it as the bare `"a"` (not
5598        // `"a "`). Short and long forms must now agree exactly.
5599        //
5600        // `fun f() { val s = "$a $b" }\n`
5601        //   operands by token text: `f`, `s`, `a` (recovered), `b`
5602        //   (recovered). Wrapper suppressed → u_operands = 4, N2 = 4.
5603        let short = "fun f() { val s = \"$a $b\" }\n";
5604        let long = "fun f() { val s = \"${a} ${b}\" }\n";
5605        check_metrics::<KotlinParser>(short, "foo.kt", |metric| {
5606            assert_eq!(metric.halstead.unique_operands(), 4);
5607            assert_eq!(metric.halstead.total_operands(), 4);
5608        });
5609        // Both `a` and `b` present, wrapper absent, n2 == dedupe(operands).
5610        assert_ops_operands::<KotlinParser>(short, "foo.kt", 4, vec!["f", "s", "a", "b"]);
5611        // Exact parity with the long `${a} ${b}` form.
5612        assert_ops_operands::<KotlinParser>(long, "foo.kt", 4, vec!["f", "s", "a", "b"]);
5613
5614        // Comma after the name (`"$a, $b"`): the first name token is
5615        // `"a, "`; its leading identifier prefix is `a`.
5616        let comma = "fun f() { val s = \"$a, $b\" }\n";
5617        assert_ops_operands::<KotlinParser>(comma, "foo.kt", 4, vec!["f", "s", "a", "b"]);
5618
5619        // Name preceded by literal text and at end-of-segment (`"x=$a"`):
5620        // the `a` token has no trailing text, so recovery is unchanged.
5621        let prefixed = "fun f() { val s = \"x=$a\" }\n";
5622        assert_ops_operands::<KotlinParser>(prefixed, "foo.kt", 3, vec!["f", "s", "a"]);
5623
5624        // Mid-prose `"$x is "`: the name token is `"x is "`. The leading
5625        // identifier prefix is `x`, matching the long form `"${x} is "`,
5626        // which also recovers `x` and treats `" is "` as literal text.
5627        let prose_short = "fun f() { val s = \"$x is \" }\n";
5628        let prose_long = "fun f() { val s = \"${x} is \" }\n";
5629        assert_ops_operands::<KotlinParser>(prose_short, "foo.kt", 3, vec!["f", "s", "x"]);
5630        assert_ops_operands::<KotlinParser>(prose_long, "foo.kt", 3, vec!["f", "s", "x"]);
5631    }
5632
5633    #[cfg(feature = "kotlin")]
5634    #[test]
5635    fn kotlin_dollar_non_identifier_stays_literal() {
5636        // Issue #454 boundary: a `$` not followed by a clean identifier
5637        // is literal text, not an interpolation. `"price: $5"` (digit
5638        // after `$`) must keep the wrapping literal as a single operand
5639        // and recover nothing.
5640        //
5641        // `fun f() { val a = "price: $5" }\n`
5642        //   operands: `f`, `a`, `"price: $5"` → u_operands = 3, N2 = 3.
5643        let src = "fun f() { val a = \"price: $5\" }\n";
5644        check_metrics::<KotlinParser>(src, "foo.kt", |metric| {
5645            assert_eq!(metric.halstead.unique_operands(), 3);
5646            assert_eq!(metric.halstead.total_operands(), 3);
5647        });
5648        assert_ops_operands::<KotlinParser>(src, "foo.kt", 3, vec!["f", "a", "\"price: $5\""]);
5649    }
5650
5651    #[cfg(feature = "kotlin")]
5652    #[test]
5653    fn kotlin_string_template_long_form_no_double_count() {
5654        // The `${expr}` long form of a Kotlin string template also
5655        // produces an `Interpolation` child. The fix must apply to it
5656        // identically.
5657        //
5658        // Source: `fun f(x: Int): String { return "v=${x}" }\n`
5659        // Operands by source-byte key:
5660        //   `f` × 1, `x` × 2 (param + inside `${x}`),
5661        //   `Int` × 1, `String` × 1.
5662        // With the fix u_operands = 4 (`f`, `x`, `Int`, `String`),
5663        // N2 = 5. Without the fix the wrapping `"v=${x}"` would also
5664        // count → u_operands = 5, N2 = 6.
5665        check_metrics::<KotlinParser>(
5666            "fun f(x: Int): String { return \"v=${x}\" }\n",
5667            "foo.kt",
5668            |metric| {
5669                assert_eq!(metric.halstead.unique_operands(), 4);
5670                assert_eq!(metric.halstead.total_operands(), 5);
5671            },
5672        );
5673    }
5674
5675    #[cfg(feature = "kotlin")]
5676    #[test]
5677    fn kotlin_plain_string_still_operand() {
5678        // The fix for #191 only skips wrapping templates that contain
5679        // an `Interpolation` child; a plain `"hello"` (no `$` interp)
5680        // must still contribute exactly one operand.
5681        //
5682        // Source: `fun f(): String { return "hello" }\n`
5683        // Operands: `f` × 1, `String` × 1, `"hello"` × 1 →
5684        // u_operands = 3, N2 = 3.
5685        check_metrics::<KotlinParser>(
5686            "fun f(): String { return \"hello\" }\n",
5687            "foo.kt",
5688            |metric| {
5689                assert_eq!(metric.halstead.unique_operands(), 3);
5690                assert_eq!(metric.halstead.total_operands(), 3);
5691            },
5692        );
5693    }
5694
5695    // Kotlin half of #1380 — see
5696    // `java_self_and_super_references_are_operands` for the structural
5697    // argument.
5698    #[cfg(feature = "kotlin")]
5699    #[test]
5700    fn kotlin_self_and_super_references_are_operands() {
5701        let source = "class T {\n    fun f() = this.x + super.y\n}";
5702        // Operators (n1=7, N1=8): class, {}, fun, (), =, . x2, +
5703        // Operands (n2=6, N2=6): T, f, this, x, super, y
5704        // Before the fix this read (9, 10, 4, 4).
5705        assert_halstead_counts::<KotlinParser>(
5706            source,
5707            "foo.kt",
5708            [7, 8, 6, 6],
5709            "kotlin self/super references",
5710        );
5711        assert_keywords_are_operands_only::<KotlinParser>(source, "foo.kt", &["this", "super"]);
5712    }
5713
5714    // The label-qualified spellings are their own leaf kinds — `this@`
5715    // (106) and `super@` (107) — carrying no `this` / `super` leaf, so
5716    // listing only the two bare kinds would score every `this@Outer`
5717    // reference zero (grammar-dispatch section 1). The labels `Outer`
5718    // and `Inner` stay separate operands: the `this_expression` wrapper
5719    // whose span would have swallowed them is deliberately unclassified
5720    // (section 5).
5721    #[cfg(feature = "kotlin")]
5722    #[test]
5723    fn kotlin_labelled_self_and_super_references_are_operands() {
5724        let source = "class Outer {\n    inner class Inner : A() {\n        \
5725                      fun f() = this@Outer.x + super@Inner.y\n    }\n}";
5726        assert_keywords_are_operands_only::<KotlinParser>(source, "foo.kt", &["this@", "super@"]);
5727        assert_ops_operands::<KotlinParser>(
5728            source,
5729            "foo.kt",
5730            8,
5731            vec!["A", "Inner", "Outer", "f", "super@", "this@", "x", "y"],
5732        );
5733        // The metrics store is a second, independent walk — it keys
5734        // operands by `get_operand_id` where `ops_inner` keys by text —
5735        // so `assert_ops_operands` on its own pins the ops store against
5736        // a literal rather than against the metric (lesson 4).
5737        //
5738        // `N2` is what makes the "the labels stay separate operands"
5739        // claim above testable. Both labels duplicate an enclosing class
5740        // name, so dropping their contribution entirely leaves `n2` at 8
5741        // with the same eight strings; only `N2` falls, 10 to 8.
5742        check_metrics::<KotlinParser>(source, "foo.kt", |metric| {
5743            assert_eq!(metric.halstead.unique_operands(), 8);
5744            assert_eq!(metric.halstead.total_operands(), 10);
5745        });
5746    }
5747
5748    // The keeper question of grammar-dispatch section 6: a
5749    // `constructor_delegation_call` emits a bare `this` / `super` leaf
5750    // with no `this_expression` / `super_expression` wrapper around it,
5751    // so classifying the wrapper instead would score constructor
5752    // delegation zero. This is the only spelling that can tell the two
5753    // choices apart — every other `this` carries both nodes — so it is
5754    // the independent path grammar-dispatch section 11 asks for.
5755    #[cfg(feature = "kotlin")]
5756    #[test]
5757    fn kotlin_constructor_delegation_self_reference_is_an_operand() {
5758        let source = "class C(val n: Int) {\n    constructor() : this(0)\n}";
5759        assert_keywords_are_operands_only::<KotlinParser>(source, "foo.kt", &["this"]);
5760        assert_ops_operands::<KotlinParser>(
5761            source,
5762            "foo.kt",
5763            5,
5764            vec!["0", "C", "Int", "n", "this"],
5765        );
5766        check_metrics::<KotlinParser>(source, "foo.kt", |metric| {
5767            assert_eq!(metric.halstead.unique_operands(), 5);
5768            assert_eq!(metric.halstead.total_operands(), 5);
5769        });
5770    }
5771
5772    /// One `this` / `super` per container the pinned
5773    /// `tree-sitter-kotlin-ng` can put them in: a bare expression, a
5774    /// call argument, a navigation receiver, a type-argument-qualified
5775    /// `super<P>`, both label-qualified forms, and the
5776    /// `constructor_delegation_call` that carries no wrapper.
5777    #[cfg(feature = "kotlin")]
5778    const KOTLIN_SELF_POSITIONS: &str = "class P {
5779        fun h() = 1
5780    }
5781    class Outer : P() {
5782        val x = 1
5783        fun c() = super.h()
5784        fun d() = this
5785        fun e(o: Any): Any = e(this)
5786        inner class Inner : P() {
5787            fun a() = this@Outer.x
5788            fun b() = this.hashCode()
5789            fun f() = super@Inner.h()
5790            fun g() = super<P>.h()
5791        }
5792    }
5793    class Del(val n: Int) {
5794        constructor() : this(0)
5795    }
5796";
5797
5798    #[cfg(feature = "kotlin")]
5799    #[test]
5800    fn kotlin_self_and_super_leaves_are_unaliased_and_unconditional() {
5801        assert_self_reference_leaves::<KotlinCode>(
5802            KOTLIN_SELF_POSITIONS,
5803            &[
5804                ("this", Kotlin::This as u16, 4),
5805                ("super", Kotlin::Super as u16, 2),
5806                ("this@", Kotlin::ThisAT as u16, 1),
5807                ("super@", Kotlin::SuperAT as u16, 1),
5808            ],
5809            "kotlin",
5810        );
5811    }
5812
5813    #[cfg(feature = "python")]
5814    #[test]
5815    fn python_fstring_no_double_count() {
5816        // Regression: issue #191. A Python f-string (`f"Hi {name}!"`)
5817        // wraps an `Interpolation` child whose inner identifier
5818        // `name` is walked and counted as its own operand. Without
5819        // the `is_child(Interpolation)` guard the wrapping `String`
5820        // would also count, double-counting `name`'s contribution to
5821        // `N2`. Same pattern as #180 (Bash/Elixir) and #184 (PHP).
5822        //
5823        // Source: `def greet(name):\n    return f"Hi {name}!"\n`
5824        // Operands by source-byte key:
5825        //   `greet` × 1, `name` × 2 (param + inside `{name}`).
5826        // With the fix the wrapping `f"Hi {name}!"` is skipped →
5827        // u_operands = 2 (`greet`, `name`), N2 = 3. Without the fix
5828        // the wrapping literal would also count → u_operands = 3,
5829        // N2 = 4.
5830        check_metrics::<PythonParser>(
5831            "def greet(name):\n    return f\"Hi {name}!\"\n",
5832            "foo.py",
5833            |metric| {
5834                assert_eq!(metric.halstead.unique_operands(), 2);
5835                assert_eq!(metric.halstead.total_operands(), 3);
5836            },
5837        );
5838    }
5839
5840    #[cfg(feature = "python")]
5841    #[test]
5842    fn python_plain_string_still_operand() {
5843        // The fix for #191 only skips wrapping `String` nodes that
5844        // contain an `Interpolation` child; a plain `"hi"` must still
5845        // contribute exactly one operand.
5846        //
5847        // Source: `def f():\n    return "hi"\n`
5848        // Operands: `f` × 1, `"hi"` × 1 → u_operands = 2, N2 = 2.
5849        // (The previous documentation-string filter is preserved:
5850        // a bare `"hi"` as a top-level `expression_statement` would
5851        // be skipped, but here it appears as `return "hi"`.)
5852        check_metrics::<PythonParser>("def f():\n    return \"hi\"\n", "foo.py", |metric| {
5853            assert_eq!(metric.halstead.unique_operands(), 2);
5854            assert_eq!(metric.halstead.total_operands(), 2);
5855        });
5856    }
5857
5858    #[cfg(feature = "python")]
5859    #[test]
5860    fn python_concatenated_docstring_suppressed() {
5861        // Regression for #695. An implicit-concatenation docstring
5862        // (`"""doc""" "more"`) parses as `expression_statement >
5863        // concatenated_string > [string, string]`. The single-literal
5864        // docstring guard (`parent == expression_statement &&
5865        // child_count == 1`) never fired here, so each fragment counted
5866        // as a separate operand and the docstring's N2 contribution
5867        // depended on how many literals it was split into. With the fix,
5868        // every fragment of such a docstring is suppressed.
5869        //
5870        // Source: `def f():\n    """doc""" "more"\n    return 1\n`
5871        // Operands: `f`, `1` only — both docstring fragments suppressed →
5872        // u_operands = 2, N2 = 2.
5873        check_metrics::<PythonParser>(
5874            "def f():\n    \"\"\"doc\"\"\" \"more\"\n    return 1\n",
5875            "foo.py",
5876            |metric| {
5877                assert_eq!(metric.halstead.unique_operands(), 2);
5878                assert_eq!(metric.halstead.total_operands(), 2);
5879            },
5880        );
5881    }
5882
5883    #[cfg(feature = "python")]
5884    #[test]
5885    fn python_concatenated_non_docstring_still_counts() {
5886        // The #695 fix must only suppress concatenated literals in the
5887        // *docstring* position (sole child of an `expression_statement`).
5888        // A concatenated string used as a value (`x = "a" "b"`) is not a
5889        // docstring — its `concatenated_string` parent's grandparent is
5890        // an assignment, not a single-child statement — so both fragments
5891        // must still be operands.
5892        //
5893        // Source: `def f():\n    x = "a" "b"\n    return x\n`
5894        // Operands: `f`, `x` (twice: assign + return), `"a"`, `"b"` →
5895        // u_operands = 4, N2 = 5.
5896        check_metrics::<PythonParser>(
5897            "def f():\n    x = \"a\" \"b\"\n    return x\n",
5898            "foo.py",
5899            |metric| {
5900                assert_eq!(metric.halstead.unique_operands(), 4);
5901                assert_eq!(metric.halstead.total_operands(), 5);
5902            },
5903        );
5904    }
5905
5906    #[cfg(feature = "python")]
5907    #[test]
5908    fn python_bare_tuple_of_strings_is_not_a_docstring() {
5909        // `"a", "b"` as a statement is a discarded tuple with two
5910        // expression children, so the docstring predicate's
5911        // `child_count == 1` gate keeps both literals as operands.
5912        //
5913        // Source: `def f():\n    "a", "b"\n    return 1\n`
5914        // Operands: `f`, `"a"`, `"b"`, `1` → u_operands = 4, N2 = 4.
5915        check_metrics::<PythonParser>(
5916            "def f():\n    \"a\", \"b\"\n    return 1\n",
5917            "foo.py",
5918            |metric| {
5919                assert_eq!(metric.halstead.unique_operands(), 4);
5920                assert_eq!(metric.halstead.total_operands(), 4);
5921            },
5922        );
5923    }
5924
5925    #[cfg(feature = "python")]
5926    #[test]
5927    fn python_parenthesized_docstring_suppressed() {
5928        // CPython drops the parentheses around a docstring (#1610), so
5929        // `("""doc""")` must bill what `"""doc"""` bills: neither the
5930        // string nor its `(`. Operators: `def`, `(` of the parameter list,
5931        // `:`, `return`; operands: `f`, `1`.
5932        let sources: [&[u8]; 4] = [
5933            b"def f():\n    \"\"\"doc\"\"\"\n    return 1\n",
5934            b"def f():\n    (\"\"\"doc\"\"\")\n    return 1\n",
5935            b"def f():\n    ((\"doc\"))\n    return 1\n",
5936            b"def f():\n    (\n        # c\n        \"doc\" \"more\"\n    )\n    return 1\n",
5937        ];
5938        for source in sources {
5939            let h = crate::test_support::metrics_verbatim(
5940                crate::LANG::Python,
5941                source,
5942                crate::MetricsOptions::default(),
5943            )
5944            .halstead;
5945            assert_eq!(
5946                [
5947                    h.unique_operators(),
5948                    h.total_operators(),
5949                    h.unique_operands(),
5950                    h.total_operands()
5951                ],
5952                [4, 4, 2, 2],
5953                "{}",
5954                String::from_utf8_lossy(source)
5955            );
5956        }
5957        // A parenthesized value keeps its `(` and its operand, and so does
5958        // a parenthesized name statement. Operators: `=`, `(`; operands:
5959        // `x`, `"a"`.
5960        check_metrics::<PythonParser>("x = (\"a\")\n", "foo.py", |metric| {
5961            let h = &metric.halstead;
5962            assert_eq!([h.total_operators(), h.total_operands()], [2, 2]);
5963        });
5964        // Operators: `(`; operands: `x`.
5965        check_metrics::<PythonParser>("(x)\n", "foo.py", |metric| {
5966            let h = &metric.halstead;
5967            assert_eq!([h.total_operators(), h.total_operands()], [1, 1]);
5968        });
5969        // A tuple led by a parenthesized string is a value, not a bare
5970        // string. Operators: `(`, `,`; operands: `"a"`, `"b"`.
5971        check_metrics::<PythonParser>("(\"a\"), \"b\"\n", "foo.py", |metric| {
5972            let h = &metric.halstead;
5973            assert_eq!([h.total_operators(), h.total_operands()], [2, 2]);
5974        });
5975    }
5976
5977    #[cfg(feature = "python")]
5978    #[test]
5979    fn python_empty_file_halstead() {
5980        check_metrics::<PythonParser>("", "empty.py", |metric| {
5981            let h = &metric.halstead;
5982            assert_eq!(h.unique_operators(), 0);
5983            assert_eq!(h.total_operands(), 0);
5984            assert_eq!(h.estimated_program_length(), 0.0);
5985            assert_eq!(h.purity_ratio(), 0.0);
5986            assert_eq!(h.volume(), 0.0);
5987            assert_eq!(h.difficulty(), 0.0);
5988            assert_eq!(h.level(), 0.0);
5989            assert_eq!(h.effort(), 0.0);
5990            assert_eq!(h.time(), 0.0);
5991            assert_eq!(h.bugs(), 0.0);
5992        });
5993    }
5994
5995    /// Regression #413, sub-fix (1): `await` was double-counted because the
5996    /// operator arm listed both the await-expression node (Await=237) and the
5997    /// nested `await` keyword token (Await2=95). Only the node should count,
5998    /// mirroring how `yield` counts only the Yield node.
5999    #[cfg(feature = "python")]
6000    #[test]
6001    fn python_await_counted_once_per_use() {
6002        check_metrics::<PythonParser>(
6003            "async def f():\n    await a()\n    await b()\n    await c()\n",
6004            "foo.py",
6005            |metric| {
6006                // expected operators: async, def, (), :, await  (5 unique)
6007                //   N1: async(1) def(1) await(3) = 5, plus the four call
6008                //   and parameter-list `(` and the block `:` (#1486) = 10.
6009                //   Before #413, Await + Await2 both matched, so `await` was a
6010                //   distinct operator twice: one more unique and three more total.
6011                assert_eq!(metric.halstead.unique_operators(), 5);
6012                assert_eq!(metric.halstead.total_operators(), 10);
6013            },
6014        );
6015    }
6016
6017    /// Regression #413, sub-fix (3): `lambda` was dropped entirely. Only the
6018    /// `lambda` keyword token (Lambda3=73) is classified, not the wrapping
6019    /// Lambda/Lambda2 expression nodes, to avoid an await-style double count.
6020    #[cfg(feature = "python")]
6021    #[test]
6022    fn python_lambda_counted_once() {
6023        check_metrics::<PythonParser>("g = lambda x: x + 1\n", "foo.py", |metric| {
6024            // expected operators: =, lambda, :, +  (4 unique, each used
6025            // once; the parameter-list `:` since #1486).
6026            // Before #413, lambda was absent: only =, + were counted.
6027            assert_eq!(metric.halstead.unique_operators(), 4);
6028            assert_eq!(metric.halstead.total_operators(), 4);
6029        });
6030    }
6031
6032    /// Regression #413, sub-fix (2): `match` / `case` keyword tokens
6033    /// (Match=26, Case=27) were dropped. Each should now count as an operator,
6034    /// matching the cyclomatic metric which already counts every `case`.
6035    #[cfg(feature = "python")]
6036    #[test]
6037    fn python_match_case_counted() {
6038        check_metrics::<PythonParser>(
6039            "match x:\n    case 1:\n        pass\n    case _:\n        pass\n",
6040            "foo.py",
6041            |metric| {
6042                // expected operators: match, case, pass, :  (4 unique)
6043                //   match(1) + case(2) + pass(2) + one block `:` per
6044                //   header (3, #1486) = 8 total occurrences.
6045                // Before #413, neither match nor case was counted (only pass).
6046                assert_eq!(metric.halstead.unique_operators(), 4);
6047                assert_eq!(metric.halstead.total_operators(), 8);
6048            },
6049        );
6050    }
6051
6052    /// Regression #413, sub-fix (2): `nonlocal` (Nonlocal=41) was dropped while
6053    /// `global` was already classified. Both should count, for parity.
6054    #[cfg(feature = "python")]
6055    #[test]
6056    fn python_nonlocal_and_global_counted() {
6057        check_metrics::<PythonParser>(
6058            "def f():\n    global a\n    nonlocal b\n",
6059            "foo.py",
6060            |metric| {
6061                // expected operators: def, (), :, global, nonlocal
6062                //   (5 unique, each once; `()` and `:` since #1486)
6063                // Before #413, nonlocal was absent: only def, global counted.
6064                assert_eq!(metric.halstead.unique_operators(), 5);
6065                assert_eq!(metric.halstead.total_operators(), 5);
6066            },
6067        );
6068    }
6069
6070    /// Regression #413, sub-fix (4): `not in` (Notin=193) and `is not`
6071    /// (Isnot=194) are single compound operators. The parent-guard suppresses
6072    /// the inner Not/In/Is leaves only under those compounds, so standalone
6073    /// `not x`, `a in b`, `a is b`, and `for x in y` still count their leaves.
6074    #[cfg(feature = "python")]
6075    #[test]
6076    fn python_not_in_is_not_counted_as_single_operator() {
6077        check_metrics::<PythonParser>(
6078            "a not in b\na is not b\nnot c\nd in e\nf is g\nfor h in i:\n    pass\n",
6079            "foo.py",
6080            |metric| {
6081                // expected operators (8 unique):
6082                //   "not in" (compound, once), "is not" (compound, once),
6083                //   "not" (standalone `not c`, once),
6084                //   "in" (standalone `d in e` + `for h in i` = twice),
6085                //   "is" (standalone `f is g`, once),
6086                //   "for" (once), "pass" (once), and the `for` header's
6087                //   block ":" (once, #1486)
6088                // Total occurrences: 1+1+1+2+1+1+1+1 = 9.
6089                // Before #413, `a not in b` counted not+in (two) and
6090                // `a is not b` counted is+not (two); the compounds were
6091                // never classified.
6092                assert_eq!(metric.halstead.unique_operators(), 8);
6093                assert_eq!(metric.halstead.total_operators(), 9);
6094            },
6095        );
6096    }
6097
6098    /// Asserts `count(halstead)` for each `(label, source, expected)`
6099    /// row, one parse per row so a row's counts are its own.
6100    #[cfg(any(
6101        feature = "c",
6102        feature = "cpp",
6103        feature = "groovy",
6104        feature = "java",
6105        feature = "javascript",
6106        feature = "kotlin",
6107        feature = "mozcpp",
6108        feature = "mozjs",
6109        feature = "objc",
6110        feature = "python",
6111        feature = "rust",
6112        feature = "typescript"
6113    ))]
6114    fn assert_halstead_rows<T: crate::MetricSuite>(
6115        file: &str,
6116        rows: &[(&str, &str, [u64; 2])],
6117        count: fn(&Stats) -> [u64; 2],
6118    ) {
6119        for (label, source, expected) in rows {
6120            crate::test_support::check_func_space_only::<T, _>(
6121                source,
6122                file,
6123                &[crate::Metric::Halstead],
6124                |space| {
6125                    assert_eq!(
6126                        count(&space.metrics.halstead),
6127                        *expected,
6128                        "{label}: {source:?}"
6129                    );
6130                },
6131            );
6132        }
6133    }
6134
6135    /// Asserts `[n1, N1]` for each row.
6136    #[cfg(any(
6137        feature = "c",
6138        feature = "cpp",
6139        feature = "groovy",
6140        feature = "java",
6141        feature = "javascript",
6142        feature = "kotlin",
6143        feature = "mozcpp",
6144        feature = "mozjs",
6145        feature = "objc",
6146        feature = "python",
6147        feature = "rust",
6148        feature = "typescript"
6149    ))]
6150    fn assert_operator_rows<T: crate::MetricSuite>(file: &str, rows: &[(&str, &str, [u64; 2])]) {
6151        assert_halstead_rows::<T>(file, rows, |h| [h.unique_operators(), h.total_operators()]);
6152    }
6153
6154    /// Asserts `[n2, N2]` for each row.
6155    #[cfg(any(feature = "java", feature = "typescript"))]
6156    fn assert_operand_rows<T: crate::MetricSuite>(file: &str, rows: &[(&str, &str, [u64; 2])]) {
6157        assert_halstead_rows::<T>(file, rows, |h| [h.unique_operands(), h.total_operands()]);
6158    }
6159
6160    /// #1486: Python's bracket openers, `;` and `:` are vocabulary, as
6161    /// they are in every other getter (#1395). Each row isolates one
6162    /// position, so the expected `[n1, N1]` is derivable from the row
6163    /// alone; a bracket pair bills its opener once, as `()` / `[]` / `{}`.
6164    #[cfg(feature = "python")]
6165    #[test]
6166    fn python_punctuation_is_vocabulary_1486() {
6167        const ROWS: &[(&str, &str, [u64; 2])] = &[
6168            // `()`
6169            ("call", "f(x)\n", [1, 1]),
6170            // `()`, `=`
6171            ("keyword argument", "f(a=1)\n", [2, 2]),
6172            // `()`, `*`, `,`, `**`
6173            ("splat arguments", "f(*a, **k)\n", [4, 4]),
6174            // `[]`
6175            ("index", "a[1]\n", [1, 1]),
6176            // `[]`, `:` twice
6177            ("slice", "a[1:2:3]\n", [2, 3]),
6178            // `{}`, `:`
6179            ("dict", "{k: v}\n", [2, 2]),
6180            // `{}`, `,`
6181            ("set", "{1, 2}\n", [2, 2]),
6182            // `[]`, `,` twice, `()`
6183            ("list and tuple", "[1, (2, 3)]\n", [3, 4]),
6184            // `()`, `for`, `in`
6185            ("generator", "(x for x in y)\n", [3, 3]),
6186            // `{}`, `:`, `for`, `in`
6187            ("dict comprehension", "{k: v for k in y}\n", [4, 4]),
6188            // `()`, `:=` — the walrus is its own token, not a `:`
6189            ("walrus", "(n := 3)\n", [2, 2]),
6190            // `lambda`, `:`
6191            ("lambda", "lambda x: x\n", [2, 2]),
6192            // `def`, `()`, `:` twice, `->`, `pass`
6193            ("annotations", "def f(x: int) -> int:\n    pass\n", [5, 6]),
6194            // `:`, `=`
6195            ("annotated assignment", "x: int = 1\n", [2, 2]),
6196            // `@`, `def`, `()`, `:`, `pass`
6197            ("decorator", "@d\ndef f():\n    pass\n", [5, 5]),
6198            // `if`, `:`, `pass`
6199            ("if block", "if x:\n    pass\n", [3, 3]),
6200            // `while`, `:`, `pass`
6201            ("while block", "while x:\n    pass\n", [3, 3]),
6202            // `for`, `in`, `:`, `pass`
6203            ("for block", "for i in y:\n    pass\n", [4, 4]),
6204            // `with`, `:`, `pass`
6205            ("with block", "with o:\n    pass\n", [3, 3]),
6206            // `try`, `except`, `:` twice, `pass` twice
6207            ("try block", "try:\n    pass\nexcept E:\n    pass\n", [4, 6]),
6208            // `class`, `:`, `pass` — the keyword is billed like `def`
6209            // (#1550)
6210            ("class block", "class C:\n    pass\n", [3, 3]),
6211            // `class` twice, `()`, `:` twice, `pass` — one `class` per
6212            // keyword, nothing for the `ClassDefinition` wrapper
6213            (
6214                "nested class",
6215                "class C(B):\n    class D:\n        pass\n",
6216                [4, 6],
6217            ),
6218            // `;`
6219            ("statement separator", "a; b\n", [1, 1]),
6220        ];
6221        assert_operator_rows::<PythonParser>("foo.py", ROWS);
6222    }
6223
6224    /// #1486: an f-string replacement field's `{` and format-spec `:` are
6225    /// spelling, not operations, like every other language's
6226    /// interpolation opener (#1314) — the f-string rows bill nothing.
6227    /// #1551 adds the self-documenting `=` (`f"{z=}"`), a directive of
6228    /// the same field. The guard reads the token's *parent*, so a dict,
6229    /// a keyword argument or a walrus written inside a replacement field
6230    /// keeps its own operators.
6231    #[cfg(feature = "python")]
6232    #[test]
6233    fn python_f_string_delimiters_are_not_operators_1486() {
6234        const ROWS: &[(&str, &str, [u64; 2])] = &[
6235            ("interpolation opener", "f\"{x}\"\n", [0, 0]),
6236            ("format spec colon", "f\"{x:>10}\"\n", [0, 0]),
6237            ("nested format expression", "f\"{a:{w}}\"\n", [0, 0]),
6238            // `{}`, `:`, `[]` of the dict and its subscript, nothing of
6239            // the replacement field around them
6240            ("dict inside a field", "f\"{ {k: v}[k] }\"\n", [3, 3]),
6241            ("self-documenting =", "f\"{z=}\"\n", [0, 0]),
6242            // the `=` keeps its spaces in the output; `!r` and `:` follow it
6243            ("spaced = with conversion", "f\"{z = !r:>10}\"\n", [0, 0]),
6244            // a `=` in a nested format expression (Python 3.12+)
6245            ("nested self-documenting =", "f\"{a:{w=}}\"\n", [0, 0]),
6246            // `()`, `=` — a keyword argument's `=` is a real operator
6247            ("keyword argument inside a field", "f\"{g(a=1)}\"\n", [2, 2]),
6248            // `()`, `:=`
6249            ("walrus inside a field", "f\"{(y := 1)}\"\n", [2, 2]),
6250        ];
6251        assert_operator_rows::<PythonParser>("foo.py", ROWS);
6252    }
6253
6254    /// #1552: a JS-family `class` keyword bills as `function` does, once
6255    /// per keyword. Shared by the four grammars of the macro; each runs
6256    /// its own call so a grammar whose `Class2` stops being the keyword
6257    /// leaf fails alone. Before #1552 every row billed one fewer.
6258    #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
6259    const JS_FAMILY_CLASS_ROWS: &[(&str, &str, [u64; 2])] = &[
6260        // `class`, `{}`
6261        ("declaration", "class C {}\n", [2, 2]),
6262        // `const`, `=`, `class`, `{}`, `;` — the class *expression*
6263        // node wraps the keyword and bills nothing of its own
6264        ("expression", "const D = class {};\n", [5, 5]),
6265        // `class` twice, `extends`, `{}` twice
6266        ("two classes", "class A {}\nclass B extends A {}\n", [3, 5]),
6267    ];
6268
6269    #[cfg(any(feature = "javascript", feature = "mozjs"))]
6270    #[test]
6271    fn js_class_keyword_is_an_operator_1552() {
6272        #[cfg(feature = "javascript")]
6273        assert_operator_rows::<JavascriptParser>("foo.js", JS_FAMILY_CLASS_ROWS);
6274        #[cfg(feature = "mozjs")]
6275        assert_operator_rows::<MozjsParser>("foo.js", JS_FAMILY_CLASS_ROWS);
6276    }
6277
6278    /// #1554: a function *expression* bills its `function` keyword once,
6279    /// as a declaration does — the `function_expression` wrapper bills
6280    /// nothing of its own. Before #1554 the plain and `async` rows billed
6281    /// one extra unique and total operator, and the nested row one extra
6282    /// unique and two extra totals. The declaration and generator rows
6283    /// are controls: neither node was ever in the operator arm.
6284    #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
6285    const JS_FAMILY_FUNCTION_ROWS: &[(&str, &str, [u64; 2])] = &[
6286        // `function`, `()`, `{}`
6287        ("declaration", "function h() {}\n", [3, 3]),
6288        // `const`, `=`, `function`, `()`, `{}`, `;`
6289        ("expression", "const f = function () {};\n", [6, 6]),
6290        // `const`, `=`, `async`, `function`, `()`, `{}`, `;`
6291        (
6292            "async expression",
6293            "const f = async function () {};\n",
6294            [7, 7],
6295        ),
6296        // `const`, `=`, `function`, `*`, `()`, `{}`, `;` — a generator
6297        // expression is its own `generator_function` node, never billed
6298        (
6299            "generator expression",
6300            "const f = function* () {};\n",
6301            [7, 7],
6302        ),
6303        // `const`, `=`, `return`, `;` x2, and `function`, `()`, `{}`
6304        // twice each
6305        (
6306            "nested expressions",
6307            "const f = function () { return function () {}; };\n",
6308            [7, 11],
6309        ),
6310    ];
6311
6312    #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
6313    #[test]
6314    fn js_family_function_expression_bills_function_once_1554() {
6315        #[cfg(feature = "javascript")]
6316        assert_operator_rows::<JavascriptParser>("foo.js", JS_FAMILY_FUNCTION_ROWS);
6317        #[cfg(feature = "mozjs")]
6318        assert_operator_rows::<MozjsParser>("foo.js", JS_FAMILY_FUNCTION_ROWS);
6319        #[cfg(feature = "typescript")]
6320        {
6321            assert_operator_rows::<TypescriptParser>("foo.ts", JS_FAMILY_FUNCTION_ROWS);
6322            assert_operator_rows::<TsxParser>("foo.tsx", JS_FAMILY_FUNCTION_ROWS);
6323        }
6324    }
6325
6326    /// #1552 for the two TypeScript grammars: the shared `class` rows,
6327    /// plus their own `interface` and `enum` declaration keywords.
6328    #[cfg(feature = "typescript")]
6329    #[test]
6330    fn typescript_declaration_keywords_are_operators_1552() {
6331        const ROWS: &[(&str, &str, [u64; 2])] = &[
6332            // `interface`, `{}`
6333            ("interface", "interface I {}\n", [2, 2]),
6334            // `enum`, `{}`
6335            ("enum", "enum E { A }\n", [2, 2]),
6336        ];
6337        assert_operator_rows::<TypescriptParser>("foo.ts", JS_FAMILY_CLASS_ROWS);
6338        assert_operator_rows::<TypescriptParser>("foo.ts", ROWS);
6339        assert_operator_rows::<TsxParser>("foo.tsx", JS_FAMILY_CLASS_ROWS);
6340        assert_operator_rows::<TsxParser>("foo.tsx", ROWS);
6341    }
6342
6343    /// #1552: Java's type-declaration keywords are operators, as C#'s
6344    /// are. The class-literal row pins the guard: the `class` tail of
6345    /// `C.class` is an operand (#1566), not a declaration keyword.
6346    #[cfg(feature = "java")]
6347    #[test]
6348    fn java_declaration_keywords_are_operators_1552() {
6349        const ROWS: &[(&str, &str, [u64; 2])] = &[
6350            // `class`, `{}`
6351            ("class", "class C {}\n", [2, 2]),
6352            // `interface`, `{}`
6353            ("interface", "interface I {}\n", [2, 2]),
6354            // `enum`, `{}`
6355            ("enum", "enum E { A }\n", [2, 2]),
6356            // `record`, `()`, `int`, `{}`
6357            ("record", "record R(int x) {}\n", [4, 4]),
6358            // `@interface`, `{}`
6359            ("annotation type", "@interface N {}\n", [2, 2]),
6360            // `class`, `{}`, `=`, `.`, `;` — one `class`: the literal's
6361            // tail is an operand (#1566)
6362            ("class literal", "class U { Object o = C.class; }\n", [5, 5]),
6363        ];
6364        assert_operator_rows::<JavaParser>("Foo.java", ROWS);
6365    }
6366
6367    /// #1560: a Java type name is an operand, as in C#, Rust, the C
6368    /// family, Go, Groovy and TypeScript. Each row isolates one position
6369    /// of the single `type_identifier` kind. The class-literal rows pin
6370    /// that a type inside `Foo.class` bills like any other (#1566),
6371    /// whether it sits there directly or under an array or scoped type,
6372    /// beside the literal's `class` tail; the `var` rows, that the inferred-type
6373    /// keyword bills nothing while a variable named `var` stays an
6374    /// operand.
6375    #[cfg(feature = "java")]
6376    #[test]
6377    fn java_type_names_are_operands_1560() {
6378        const ROWS: &[(&str, &str, [u64; 2])] = &[
6379            // `C`, `I` x2, `Foo`, `f`
6380            (
6381                "field type and implements",
6382                "class C implements I { Foo f; }\ninterface I {}\n",
6383                [4, 5],
6384            ),
6385            // `C`, `java`, `util`, `List`, `Foo`, `l`
6386            (
6387                "scoped and generic",
6388                "class C { java.util.List<Foo> l; }\n",
6389                [6, 6],
6390            ),
6391            // `C`, `T` x2, `Comparable`
6392            (
6393                "type parameter",
6394                "class C<T extends Comparable<T>> {}\n",
6395                [3, 4],
6396            ),
6397            // `C`, `m`, `Ex`
6398            ("throws", "class C { void m() throws Ex {} }\n", [3, 3]),
6399            // `C`, `Object`, `a`, `Foo`, `class`
6400            (
6401                "class literal",
6402                "class C { Object a = Foo.class; }\n",
6403                [5, 5],
6404            ),
6405            // `C`, `Object`, `a`, `Foo`, `class`
6406            (
6407                "array class literal",
6408                "class C { Object a = Foo[].class; }\n",
6409                [5, 5],
6410            ),
6411            // `C`, `Object`, `a`, `java`, `util`, `List`, `class`
6412            (
6413                "scoped class literal",
6414                "class C { Object a = java.util.List.class; }\n",
6415                [7, 7],
6416            ),
6417            // `C`, `m`, `x`, `1` — no `var`
6418            (
6419                "inferred type",
6420                "class C { void m() { var x = 1; } }\n",
6421                [4, 4],
6422            ),
6423            // `C`, `m`, `var`, `1` — the variable's name
6424            (
6425                "variable named var",
6426                "class C { void m() { int var = 1; } }\n",
6427                [4, 4],
6428            ),
6429        ];
6430        assert_operand_rows::<JavaParser>("Foo.java", ROWS);
6431    }
6432
6433    /// One class literal's own vocabulary: the source spelling, then the
6434    /// operators and operands it adds to the fixture's frame.
6435    #[cfg(any(feature = "groovy", feature = "java"))]
6436    type ClassLiteralParts = (
6437        &'static str,
6438        &'static [&'static str],
6439        &'static [&'static str],
6440    );
6441
6442    /// #1566: the class-literal shapes Java and Groovy spell alike. The
6443    /// expected parts are what Groovy — untouched by #1566 — already
6444    /// billed for `Foo.class`: the type name an operand, the `.` and
6445    /// `[]` operators, and the `class` tail an operand. A primitive or
6446    /// `void` receiver is an operator keyed by its spelling, as Java's
6447    /// keyword is; Groovy joined that half in #1568. Both tests below
6448    /// read this one table, so the two languages cannot drift apart.
6449    #[cfg(any(feature = "groovy", feature = "java"))]
6450    const SHARED_CLASS_LITERALS: &[ClassLiteralParts] = &[
6451        ("Foo.class", &["."], &["Foo", "class"]),
6452        ("Foo[].class", &[".", "[]"], &["Foo", "class"]),
6453        (
6454            "java.util.List.class",
6455            &["."],
6456            &["List", "class", "java", "util"],
6457        ),
6458        ("int.class", &[".", "int"], &["class"]),
6459        ("int[].class", &[".", "[]", "int"], &["class"]),
6460        ("void.class", &[".", "void"], &["class"]),
6461    ];
6462
6463    /// Asserts each literal's operator and operand vocabularies inside
6464    /// `frame`, which spells the fixture around it and contributes
6465    /// `frame_ops` / `frame_operands` of its own.
6466    #[cfg(any(feature = "groovy", feature = "java"))]
6467    #[track_caller]
6468    fn assert_class_literal_parts<T: crate::MetricSuite>(
6469        file: &str,
6470        frame: fn(&str) -> String,
6471        frame_ops: &[&str],
6472        frame_operands: &[&str],
6473        rows: &[ClassLiteralParts],
6474    ) {
6475        let merged = |frame: &[&str], parts: &[&str]| {
6476            let mut all: Vec<String> = frame.iter().chain(parts).map(|s| (*s).to_owned()).collect();
6477            all.sort_unstable();
6478            all.dedup();
6479            all
6480        };
6481        for (literal, ops, operands) in rows {
6482            let got = ops_of::<T>(&frame(literal), file);
6483            assert_eq!(
6484                got.operators,
6485                merged(frame_ops, ops),
6486                "operators of {literal}"
6487            );
6488            assert_eq!(
6489                got.operands,
6490                merged(frame_operands, operands),
6491                "operands of {literal}"
6492            );
6493        }
6494    }
6495
6496    /// #1566: a Java class literal bills its parts. JLS §15.8.2 makes it
6497    /// a primary expression rather than a literal (§3.10 lists none), so
6498    /// the type name is an operand as anywhere else, `.` and `[]` are
6499    /// operators, and the `class` tail is the operand they act on — the
6500    /// split Kotlin's and PHP's `Foo::class` and Groovy's `Foo.class`
6501    /// already made. Before the fix the literal billed as one operand
6502    /// spelled by its whole text while `.`, `[]` and a primitive inside
6503    /// it also billed as operators. The shared rows take their expected
6504    /// values from Groovy, whose primitive and `void` rows joined them
6505    /// in #1568.
6506    #[cfg(feature = "java")]
6507    #[test]
6508    fn java_class_literal_bills_its_parts_1566() {
6509        const FRAME_OPS: &[&str] = &[";", "=", "class", "{}"];
6510        const FRAME_OPERANDS: &[&str] = &["C", "Object", "c"];
6511        let frame = |literal: &str| format!("class C {{ Object c = {literal}; }}\n");
6512        assert_class_literal_parts::<JavaParser>(
6513            "Foo.java",
6514            frame,
6515            FRAME_OPS,
6516            FRAME_OPERANDS,
6517            SHARED_CLASS_LITERALS,
6518        );
6519        // The totals pin the multiplicities a vocabulary cannot: the frame
6520        // bills four operators (`class`, `{}`, `=`, `;`) and three operands
6521        // (`C`, `Object`, `c`), and each literal adds its parts once —
6522        // `java.util.List.class` its three `.` and four names.
6523        assert_halstead_rows::<JavaParser>(
6524            "Foo.java",
6525            &[
6526                ("int", "class C { Object c = int.class; }\n", [6, 4]),
6527                ("array", "class C { Object c = Foo[].class; }\n", [6, 5]),
6528                (
6529                    "scoped",
6530                    "class C { Object c = java.util.List.class; }\n",
6531                    [7, 7],
6532                ),
6533                ("void", "class C { Object c = void.class; }\n", [6, 4]),
6534            ],
6535            |h| [h.total_operators(), h.total_operands()],
6536        );
6537    }
6538
6539    /// #1566's oracle: Groovy bills the shared class-literal shapes the
6540    /// way Java now does. A Groovy field needs no `;`.
6541    #[cfg(feature = "groovy")]
6542    #[test]
6543    fn groovy_class_literal_bills_its_parts_1566() {
6544        assert_class_literal_parts::<GroovyParser>(
6545            "foo.groovy",
6546            |literal| format!("class C {{ Object c = {literal} }}\n"),
6547            &["=", "class", "{}"],
6548            &["C", "Object", "c"],
6549            SHARED_CLASS_LITERALS,
6550        );
6551    }
6552
6553    /// #1568: a Groovy primitive type bills as the operator Java's
6554    /// keyword does, keyed by its spelling, in every position the
6555    /// grammar gives it: a `type_identifier` where a type is declared,
6556    /// a plain `identifier` where the grammar misreads an uninitialised
6557    /// local (`int t` parses as two expression statements). Each row's
6558    /// operators are what the Java twin of the issue fixture bills, which
6559    /// `groovy_primitive_type_matches_java_1568` checks directly.
6560    #[cfg(feature = "groovy")]
6561    #[test]
6562    fn groovy_primitive_types_are_operators_1568() {
6563        const ROWS: &[(&str, &[&str], &[&str])] = &[
6564            (
6565                "class A { boolean m() { int x = 1; } }\n",
6566                &["()", ";", "=", "boolean", "class", "int", "{}"],
6567                &["1", "A", "m", "x"],
6568            ),
6569            (
6570                "void m(long a, char b) {}\n",
6571                &["()", ",", "char", "long", "void", "{}"],
6572                &["a", "b", "m"],
6573            ),
6574            (
6575                "def m() { short s\n  double d }\n",
6576                &["()", "def", "double", "short", "{}"],
6577                &["d", "m", "s"],
6578            ),
6579            (
6580                "byte[] a = (float) b as byte\n",
6581                &["()", "=", "[]", "as", "byte", "float"],
6582                &["a", "b"],
6583            ),
6584            // A user type's name stays an operand, however close its
6585            // spelling: only the exact keyword bills.
6586            (
6587                "Integer a\nInt b\ninte c\n",
6588                &[],
6589                &["Int", "Integer", "a", "b", "c", "inte"],
6590            ),
6591        ];
6592        for (source, operators, operands) in ROWS {
6593            let ops = ops_of::<GroovyParser>(source, "foo.groovy");
6594            assert_eq!(ops.operators, *operators, "operators of {source:?}");
6595            assert_eq!(ops.operands, *operands, "operands of {source:?}");
6596        }
6597    }
6598
6599    /// #1568's oracle: the issue's fixture scores the same in Groovy and
6600    /// Java, vocabulary and multiplicities alike.
6601    #[cfg(all(feature = "groovy", feature = "java"))]
6602    #[test]
6603    fn groovy_primitive_type_matches_java_1568() {
6604        let source = "class A { boolean m() { int x = 1; } }\n";
6605        let groovy = ops_of::<GroovyParser>(source, "foo.groovy");
6606        let java = ops_of::<JavaParser>(source, "Foo.java");
6607        assert_eq!(groovy.operators, java.operators);
6608        assert_eq!(groovy.operands, java.operands);
6609        // `()`, `;`, `=`, `boolean`, `class`, `int`, `{}` x2 / `A`, `m`,
6610        // `x`, `1`
6611        assert_halstead_rows::<GroovyParser>(
6612            "foo.groovy",
6613            &[("issue fixture", source, [8, 4])],
6614            |h| [h.total_operators(), h.total_operands()],
6615        );
6616        assert_halstead_rows::<JavaParser>("Foo.java", &[("issue fixture", source, [8, 4])], |h| {
6617            [h.total_operators(), h.total_operands()]
6618        });
6619    }
6620
6621    /// #1568: Groovy reserves the primitive type names, so one can name
6622    /// something only as a member selector after a `.`-family operator
6623    /// or as a map or named-argument label. Each position stays an
6624    /// operand; each row is its own source so a selector kind dropped
6625    /// from the gate fails by name.
6626    #[cfg(feature = "groovy")]
6627    #[test]
6628    fn groovy_primitive_spelled_names_stay_operands_1568() {
6629        for source in [
6630            "x.int\n",
6631            "x?.int\n",
6632            "x??.int\n",
6633            "x*.int\n",
6634            "x.@int\n",
6635            "x.&int\n",
6636            "x::int\n",
6637            "def m = [int: 1]\n",
6638            "f(int: 1)\n",
6639        ] {
6640            assert_keywords_are_operands_only::<GroovyParser>(source, "foo.groovy", &["int"]);
6641        }
6642    }
6643
6644    /// #1552: Groovy's type-declaration keywords are operators, as
6645    /// Java's are. `String.class` is a `field_access` whose `class` is a
6646    /// plain identifier, so it stays an operand with no guard needed.
6647    #[cfg(feature = "groovy")]
6648    #[test]
6649    fn groovy_declaration_keywords_are_operators_1552() {
6650        const ROWS: &[(&str, &str, [u64; 2])] = &[
6651            // `class`, `{}`
6652            ("class", "class C {}\n", [2, 2]),
6653            // `interface`, `{}`
6654            ("interface", "interface I {}\n", [2, 2]),
6655            // `trait`, `{}`
6656            ("trait", "trait T {}\n", [2, 2]),
6657            // `enum`, `{}`
6658            ("enum", "enum E { A }\n", [2, 2]),
6659            // `record`, `()`, `int`, `{}` — as Java's row, since #1568
6660            ("record", "record R(int x) {}\n", [4, 4]),
6661            // `def`, `=`, `.` — no `class`
6662            ("class reference", "def a = String.class\n", [3, 3]),
6663            // `=` twice: a script-level `record` / `trait` used as a name
6664            // falls into error recovery with the keyword leaf, which is
6665            // not a declaration and bills nothing
6666            ("contextual names", "record = 3\ntrait = 4\n", [1, 2]),
6667        ];
6668        assert_operator_rows::<GroovyParser>("foo.groovy", ROWS);
6669    }
6670
6671    /// #1565: Java bills every modifier, as C# and Kotlin do, each as
6672    /// its keyword leaf and never as the `modifiers` wrapper. Before the
6673    /// fix only `abstract`, `final`, `transient`, `synchronized` and
6674    /// `default` billed. The rows from "import static" on are controls:
6675    /// a directive's `static` and the contextual words used as names add
6676    /// no operator.
6677    #[cfg(feature = "java")]
6678    #[test]
6679    fn java_modifiers_are_operators_1565() {
6680        const ROWS: &[(&str, &str, [u64; 2])] = &[
6681            // `class`, `{}`, `public`, `private`, `protected`, `int` x3,
6682            // `;` x3
6683            (
6684                "visibility",
6685                "class C { public int a; private int b; protected int c; }\n",
6686                [7, 11],
6687            ),
6688            // `class`, `{}`, `static`, `final`, `int` x2, `=`, `;` x2,
6689            // `transient`, `volatile`
6690            (
6691                "field modifiers",
6692                "class C { static final int a = 1; transient volatile int b; }\n",
6693                [9, 11],
6694            ),
6695            // `class`, `{}` x3, `native`, `synchronized`, `strictfp`,
6696            // `void` x3, `()` x3, `;`
6697            (
6698                "method modifiers",
6699                "class C { native void n(); synchronized void s() {} strictfp void f() {} }\n",
6700                [8, 14],
6701            ),
6702            // `sealed`, `class` x2, `permits`, `{}` x2, `non-sealed`,
6703            // `extends`
6704            (
6705                "sealed hierarchy",
6706                "sealed class S permits T {}\nnon-sealed class T extends S {}\n",
6707                [6, 8],
6708            ),
6709            // `class`, `{}` x2, `static`: the initializer's `static`
6710            ("static initializer", "class C { static {} }\n", [3, 4]),
6711            // `class`, `{}` x2, `public`, `()`, `return`, `;`: the
6712            // annotation beside the modifier bills nothing of its own
6713            (
6714                "annotation in modifiers",
6715                "class C { @Override public String toString() { return s; } }\n",
6716                [6, 7],
6717            ),
6718            // `.` x3, `;`, `class`, `{}`: no `static`
6719            (
6720                "import static",
6721                "import static java.lang.Math.max;\nclass C {}\n",
6722                [4, 6],
6723            ),
6724            // `{}`, `;`: no `static`, and `transitive` stays unbilled
6725            // with the rest of the module directive
6726            (
6727                "requires static",
6728                "module m { requires static transitive a; }\n",
6729                [2, 2],
6730            ),
6731            // `class`, `{}` x2, `void`, `()`, `=` x3, `;` x3, `int`
6732            (
6733                "names",
6734                "class C { void m() { var sealed = 1; int permits = 2; sealed = permits; } }\n",
6735                [7, 12],
6736            ),
6737            // `class`, `{}`, `int` x2, `,`, `;` x2, `=`, `-`
6738            (
6739                "non-sealed as a subtraction",
6740                "class C { int non, sealed; int z = non-sealed; }\n",
6741                [7, 9],
6742            ),
6743        ];
6744        assert_operator_rows::<JavaParser>("Foo.java", ROWS);
6745    }
6746
6747    /// #1565: the issue's fixture, by vocabulary. Each modifier is
6748    /// listed by its own word, never by `modifiers`, and the contextual
6749    /// words used as names are operands only. No getter edit makes that
6750    /// second half fail, since Java's arms carry no guard on those
6751    /// words: it pins the grammar emitting an `identifier` for a name,
6752    /// so a bump that routes one to the keyword kind trips it.
6753    #[cfg(feature = "java")]
6754    #[test]
6755    fn java_modifier_vocabulary_1565() {
6756        let ops = ops_of::<JavaParser>(
6757            "public abstract class C { private static final int a = 1; \
6758             protected transient int b; public synchronized void m() {} }\n",
6759            "Foo.java",
6760        );
6761        assert_eq!(
6762            ops.operators,
6763            [
6764                "()",
6765                ";",
6766                "=",
6767                "abstract",
6768                "class",
6769                "final",
6770                "int",
6771                "private",
6772                "protected",
6773                "public",
6774                "static",
6775                "synchronized",
6776                "transient",
6777                "void",
6778                "{}",
6779            ]
6780        );
6781        assert_eq!(ops.operands, ["1", "C", "a", "b", "m"]);
6782
6783        assert_keywords_are_operands_only::<JavaParser>(
6784            "class C { int sealed() { return permits; } void m() { sealed(); } }\n",
6785            "Foo.java",
6786            &["sealed", "permits"],
6787        );
6788    }
6789
6790    /// #1565: Groovy bills every modifier, as Java now does. Its grammar
6791    /// has no `modifiers` wrapper, so each leaf sits directly under its
6792    /// declaration. The rows declare with `def`, so the only primitive
6793    /// type counted is `void`, an operator since #1568. The last two
6794    /// rows are controls.
6795    #[cfg(feature = "groovy")]
6796    #[test]
6797    fn groovy_modifiers_are_operators_1565() {
6798        const ROWS: &[(&str, &str, [u64; 2])] = &[
6799            // `class`, `{}`, `public`, `private`, `protected`, `def` x3
6800            (
6801                "visibility",
6802                "class C {\n  public def a\n  private def b\n  protected def c\n}\n",
6803                [6, 8],
6804            ),
6805            // `class`, `{}`, `static`, `final`, `def` x2, `=`,
6806            // `transient`, `volatile`
6807            (
6808                "field modifiers",
6809                "class C {\n  static final def a = 1\n  transient volatile def b\n}\n",
6810                [8, 9],
6811            ),
6812            // `class`, `{}` x3, `native`, `synchronized`, `strictfp`,
6813            // `void` x3 (#1568), `()` x3
6814            (
6815                "method modifiers",
6816                "class C {\n  native void n()\n  synchronized void s() {}\n  \
6817                 strictfp void f() {}\n}\n",
6818                [7, 13],
6819            ),
6820            // `sealed`, `class` x2, `permits`, `{}` x2, `non-sealed`,
6821            // `extends`
6822            (
6823                "sealed hierarchy",
6824                "sealed class S permits T {}\nnon-sealed class T extends S {}\n",
6825                [6, 8],
6826            ),
6827            // `sealed` x2, `interface`, `trait`, `permits` x2, `{}` x2
6828            (
6829                "sealed interface and trait",
6830                "sealed interface I permits A {}\nsealed trait T permits A {}\n",
6831                [5, 8],
6832            ),
6833            // `class`, `{}` x3, `static` x2, `void` (#1568), `()`: an
6834            // initializer and a script-level method
6835            (
6836                "static initializer and method",
6837                "class C {\n  static {}\n}\nstatic void top() {}\n",
6838                [5, 8],
6839            ),
6840            // `.` x3: no `static`, as `import` bills nothing
6841            (
6842                "import static",
6843                "import static java.lang.Math.max\n",
6844                [1, 3],
6845            ),
6846            // `=` x2, `.`, `()` x2, `def`: a statement opening with
6847            // `sealed` as a name fails into an `ERROR` holding the
6848            // keyword leaf, which bills nothing
6849            (
6850                "contextual names",
6851                "sealed = 1\nsealed.call()\ndef permits = 2\nfoo(permits)\n",
6852                [4, 6],
6853            ),
6854        ];
6855        assert_operator_rows::<GroovyParser>("foo.groovy", ROWS);
6856    }
6857
6858    /// #1565: the issue's Groovy fixture, by vocabulary, plus the
6859    /// modifier words in positions the grammar parses as a name. Unlike
6860    /// Java, Groovy accepts even its hard keywords as a map key, a named
6861    /// argument and a member name. No getter edit makes that half fail:
6862    /// it pins the grammar emitting an `identifier` there, so a bump
6863    /// that routes one to the keyword kind trips it.
6864    #[cfg(feature = "groovy")]
6865    #[test]
6866    fn groovy_modifier_vocabulary_1565() {
6867        let ops = ops_of::<GroovyParser>(
6868            "public abstract class C { private static final def a = 1; protected def m() {} }\n",
6869            "foo.groovy",
6870        );
6871        assert_eq!(
6872            ops.operators,
6873            [
6874                "()",
6875                ";",
6876                "=",
6877                "abstract",
6878                "class",
6879                "def",
6880                "final",
6881                "private",
6882                "protected",
6883                "public",
6884                "static",
6885                "{}",
6886            ]
6887        );
6888        assert_eq!(ops.operands, ["1", "C", "a", "m"]);
6889
6890        assert_keywords_are_operands_only::<GroovyParser>(
6891            "x = sealed\nfoo(permits)\ndef m = [static: 1, public: 2]\n\
6892             foo(native: 1, volatile: 2)\ny = obj.private\n",
6893            "foo.groovy",
6894            &[
6895                "sealed", "permits", "static", "public", "native", "volatile", "private",
6896            ],
6897        );
6898    }
6899
6900    /// #1565's oracle: Groovy accepts Java's modifier-heavy fixture
6901    /// verbatim, and the two languages bill it alike, vocabulary and
6902    /// multiplicities. Neither side's expectation is typed by the fix
6903    /// alone: a modifier one getter drops shows as a disagreement.
6904    #[cfg(all(feature = "groovy", feature = "java"))]
6905    #[test]
6906    fn groovy_modifiers_match_java_1565() {
6907        let source = "public abstract class C { private static final int a = 1; \
6908                      protected transient int b; public synchronized void m() {} }\n";
6909        let groovy = ops_of::<GroovyParser>(source, "foo.groovy");
6910        let java = ops_of::<JavaParser>(source, "Foo.java");
6911        assert_eq!(groovy.operators, java.operators);
6912        assert_eq!(groovy.operands, java.operands);
6913        // `public` x2, `abstract`, `class`, `{}` x2, `private`, `static`,
6914        // `final`, `int` x2, `=`, `;` x2, `protected`, `transient`,
6915        // `synchronized`, `void`, `()`
6916        let rows = [("modifier fixture", source, [15, 19])];
6917        assert_operator_rows::<GroovyParser>("foo.groovy", &rows);
6918        assert_operator_rows::<JavaParser>("Foo.java", &rows);
6919    }
6920
6921    /// #1552 rows shared by the two C++ clones. The template-parameter
6922    /// rows pin the synonym: `class T` is `typename T`, and the two
6923    /// spellings score alike. Both were unbilled until #1557 billed
6924    /// `typename`, and the parameter `class` with it.
6925    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
6926    const CPP_DECLARATION_KEYWORD_ROWS: &[(&str, &str, [u64; 2])] = &[
6927        // `class`, `{}`, `;`
6928        ("class", "class C {};\n", [3, 3]),
6929        // `struct`, `{}`, `;`
6930        ("struct", "struct S {};\n", [3, 3]),
6931        // `union`, `{}`, `int`, `;` twice
6932        ("union", "union U { int a; };\n", [4, 5]),
6933        // `enum`, `{}`, `;`
6934        ("enum", "enum E { A };\n", [3, 3]),
6935        // `enum`, `class`, `{}`, `;`
6936        ("enum class", "enum class F { B };\n", [4, 4]),
6937        // `struct`, `;` — an elaborated type bills its keyword
6938        ("elaborated type", "struct S s;\n", [2, 2]),
6939        // `template`, `<>`, `class`, `struct`, `{}`, `;` — the parameter
6940        // list's brackets are one pair (#1559)
6941        (
6942            "class parameter",
6943            "template <class T> struct W {};\n",
6944            [6, 6],
6945        ),
6946        // as above, `typename` in place of `class`
6947        (
6948            "typename parameter",
6949            "template <typename T> struct W {};\n",
6950            [6, 6],
6951        ),
6952        // `template`, `<>`, `class`, `=`, `int`, `struct`, `{}`, `;`
6953        (
6954            "optional class parameter",
6955            "template <class T = int> struct W {};\n",
6956            [8, 8],
6957        ),
6958        // `template`, `<>`, `class`, `struct`, `{}`, `;` — `...` is an
6959        // operand
6960        (
6961            "variadic class parameter",
6962            "template <class... T> struct W {};\n",
6963            [6, 6],
6964        ),
6965    ];
6966
6967    /// One call per clone, so reverting either arm fails its own row;
6968    /// Mozcpp owns no file extension, so this is its only coverage.
6969    #[cfg(all(feature = "cpp", feature = "mozcpp"))]
6970    #[test]
6971    fn cpp_declaration_keywords_are_operators_1552() {
6972        assert_operator_rows::<CppParser>("foo.cpp", CPP_DECLARATION_KEYWORD_ROWS);
6973        assert_operator_rows::<MozcppParser>("foo.cpp", CPP_DECLARATION_KEYWORD_ROWS);
6974    }
6975
6976    /// #1552: C bills `struct` / `union` / `enum` as C++ does, so a
6977    /// header scores the same whichever grammar parses it.
6978    #[cfg(feature = "c")]
6979    #[test]
6980    fn c_declaration_keywords_are_operators_1552() {
6981        const ROWS: &[(&str, &str, [u64; 2])] = &[
6982            // `struct`, `{}`, `int`, `;` twice
6983            ("struct", "struct S { int a; };\n", [4, 5]),
6984            // `union`, `{}`, `int`, `;` twice
6985            ("union", "union U { int a; };\n", [4, 5]),
6986            // `enum`, `{}`, `;`
6987            ("enum", "enum E { A };\n", [3, 3]),
6988            // `struct`, `;`
6989            ("elaborated type", "struct S s;\n", [2, 2]),
6990        ];
6991        assert_operator_rows::<CParser>("foo.c", ROWS);
6992    }
6993
6994    /// #1578: C bills each storage class, function specifier and type
6995    /// qualifier as its keyword leaf, never as the
6996    /// `storage_class_specifier` / `type_qualifier` wrapper. Before the
6997    /// fix none of them billed.
6998    #[cfg(feature = "c")]
6999    #[test]
7000    fn c_modifiers_are_operators_1578() {
7001        const ROWS: &[(&str, &str, [u64; 2])] = &[
7002            // `static`, `extern`, `register`, `auto`, `int` x4, `;` x4
7003            (
7004                "storage classes",
7005                "static int a;\nextern int b;\nregister int c;\nauto int d;\n",
7006                [6, 12],
7007            ),
7008            // `const`, `volatile`, `int` x3, `;` x3, `*`, `restrict`,
7009            // `_Atomic`
7010            (
7011                "qualifiers",
7012                "const volatile int a;\nint * restrict p;\n_Atomic int g;\n",
7013                [7, 11],
7014            ),
7015            // `inline`, `int`, `()` x2, `void` x3, `;` x2, `_Noreturn`
7016            (
7017                "function specifiers",
7018                "inline int f(void);\n_Noreturn void g(void);\n",
7019                [6, 10],
7020            ),
7021            // `__inline`, `int` x3, `()`, `void`, `;` x3, `__thread`, `*`,
7022            // `__restrict__`: each vendor spelling is its own operator
7023            (
7024                "vendor spellings",
7025                "__inline int f(void);\n__thread int t;\nint * __restrict__ q;\n",
7026                [8, 12],
7027            ),
7028            // `int`, `*`, `const`, `=`, `;`: a pointer's own qualifier
7029            ("pointer qualifier", "int * const p = 0;\n", [5, 5]),
7030            // `_Alignas`, `()`, `int`, `;`
7031            ("alignas", "_Alignas(8) int m;\n", [4, 4]),
7032            // `int`, `=`, `__extension__`, `;`: the expression prefix
7033            ("extension expression", "int q = __extension__ 1;\n", [4, 4]),
7034        ];
7035        assert_operator_rows::<CParser>("foo.c", ROWS);
7036    }
7037
7038    /// #1578: the issue's C fixture, by vocabulary.
7039    #[cfg(feature = "c")]
7040    #[test]
7041    fn c_modifier_vocabulary_1578() {
7042        let ops = ops_of::<CParser>(
7043            "static inline const volatile int a = 1;\nextern int b;\nregister int c;\n",
7044            "foo.c",
7045        );
7046        assert_eq!(
7047            ops.operators,
7048            [
7049                ";", "=", "const", "extern", "inline", "int", "register", "static", "volatile",
7050            ]
7051        );
7052        assert_eq!(ops.operands, ["1", "a", "b", "c"]);
7053    }
7054
7055    /// #1578 rows shared by the two C++ clones: C's modifiers plus C++'s
7056    /// own. Each bills as its keyword leaf, never as the
7057    /// `access_specifier` / `virtual_specifier` / `type_qualifier` /
7058    /// `explicit_function_specifier` wrapper.
7059    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
7060    const CPP_MODIFIER_ROWS: &[(&str, &str, [u64; 2])] = &[
7061        // `class`, `{}`, `public`, `protected`, `private`, `:` x3,
7062        // `int` x3, `;` x4
7063        (
7064            "access labels",
7065            "class C { public: int a; protected: int b; private: int c; };\n",
7066            [8, 15],
7067        ),
7068        // `struct`, `{}`, `static`, `int` x2, `;` x4, `mutable`,
7069        // `virtual`, `void`, `()`, `const`, `override`, `final`
7070        (
7071            "member modifiers",
7072            "struct S { static int a; mutable int b; virtual void f() const override final; };\n",
7073            [12, 16],
7074        ),
7075        // `struct`, `{}`, `friend`, `class`, `;` x3, `explicit`, `()`,
7076        // `int`
7077        (
7078            "friend and explicit",
7079            "struct S { friend class D; explicit S(int); };\n",
7080            [8, 10],
7081        ),
7082        // `extern`, `inline`, `constexpr`, `int`, `()`, `{}`, `return`,
7083        // `;`: the `"C"` is an operand
7084        (
7085            "linkage specification",
7086            "extern \"C\" inline constexpr int g() { return 1; }\n",
7087            [8, 8],
7088        ),
7089        // `consteval`, `int` x2, `()`, `;` x2, `constinit`,
7090        // `thread_local`, `=`
7091        (
7092            "C++20 specifiers",
7093            "consteval int h();\nconstinit thread_local int t = 1;\n",
7094            [7, 9],
7095        ),
7096        // `void` x2, `()` x3, `noexcept` x2, `;` x2: the leaf bills, the
7097        // `noexcept` wrapper does not
7098        (
7099            "exception specification",
7100            "void k() noexcept;\nvoid k2() noexcept(true);\n",
7101            [4, 9],
7102        ),
7103        // `struct`, `final`, `{}` x3, `;` x3, `class` x2, `:` x2,
7104        // `public`, `private`, `virtual`
7105        (
7106            "base clauses",
7107            "struct B final {};\nclass E : public B {};\nclass F : private virtual B {};\n",
7108            [9, 15],
7109        ),
7110        // `void`, `()` x2, `{}` x2, `if`, `constexpr`
7111        (
7112            "if constexpr",
7113            "void f() { if constexpr (true) {} }\n",
7114            [5, 7],
7115        ),
7116        // `inline`, `namespace`, `{}`
7117        ("inline namespace", "inline namespace v1 {}\n", [3, 3]),
7118        // `int` x2, `=` x2, `;` x2: the contextual words as names add
7119        // no operator
7120        (
7121            "contextual names",
7122            "int override = 1;\nint final = override;\n",
7123            [3, 6],
7124        ),
7125    ];
7126
7127    /// One call per clone, so reverting either arm fails its own row;
7128    /// Mozcpp owns no file extension, so this is its only lib coverage.
7129    #[cfg(all(feature = "cpp", feature = "mozcpp"))]
7130    #[test]
7131    fn cpp_modifiers_are_operators_1578() {
7132        assert_operator_rows::<CppParser>("foo.cpp", CPP_MODIFIER_ROWS);
7133        assert_operator_rows::<MozcppParser>("foo.cpp", CPP_MODIFIER_ROWS);
7134    }
7135
7136    /// #1578: the issue's C++ fixture, by vocabulary, in both clones,
7137    /// plus `override` and `final` used as names. No getter edit makes
7138    /// that second half fail, since the arms carry no guard on those
7139    /// words: it pins the grammar emitting an `identifier` for a name,
7140    /// so a bump that routes one to the keyword kind trips it.
7141    #[cfg(all(feature = "cpp", feature = "mozcpp"))]
7142    #[test]
7143    fn cpp_modifier_vocabulary_1578() {
7144        const SOURCE: &str = "class C { public: static int a; \
7145             virtual void f() const override final; mutable int b; };\n\
7146             extern \"C\" inline constexpr int g() { return 1; }\n";
7147        const NAMES: &str = "int override = 1;\nint final() { return override; }\n";
7148        let expected_operators = [
7149            "()",
7150            ":",
7151            ";",
7152            "class",
7153            "const",
7154            "constexpr",
7155            "extern",
7156            "final",
7157            "inline",
7158            "int",
7159            "mutable",
7160            "override",
7161            "public",
7162            "return",
7163            "static",
7164            "virtual",
7165            "void",
7166            "{}",
7167        ];
7168        let expected_operands = ["\"C\"", "1", "C", "a", "b", "f", "g"];
7169
7170        let cpp = ops_of::<CppParser>(SOURCE, "foo.cpp");
7171        assert_eq!(cpp.operators, expected_operators);
7172        assert_eq!(cpp.operands, expected_operands);
7173        let mozcpp = ops_of::<MozcppParser>(SOURCE, "foo.cpp");
7174        assert_eq!(mozcpp.operators, expected_operators);
7175        assert_eq!(mozcpp.operands, expected_operands);
7176
7177        assert_keywords_are_operands_only::<CppParser>(NAMES, "foo.cpp", &["override", "final"]);
7178        assert_keywords_are_operands_only::<MozcppParser>(NAMES, "foo.cpp", &["override", "final"]);
7179    }
7180
7181    /// #1578: every modifier spelling the arms list bills under its own
7182    /// name, one fixture line apiece, so dropping any one kind from an
7183    /// arm fails its own entry. The row tests above pin the counts; this
7184    /// one pins the spellings they leave out.
7185    #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
7186    #[track_caller]
7187    fn assert_modifier_spellings_bill<T: crate::MetricSuite>(file: &str, rows: &[(&str, &str)]) {
7188        for (spelling, source) in rows {
7189            let ops = ops_of::<T>(source, file);
7190            assert!(
7191                ops.operators.iter().any(|o| o == spelling),
7192                "{file}: `{spelling}` must be an operator in {source:?}; operators were {:?}",
7193                ops.operators
7194            );
7195            assert!(
7196                !ops.operands.iter().any(|o| o == spelling),
7197                "{file}: `{spelling}` must not be an operand; operands were {:?}",
7198                ops.operands
7199            );
7200        }
7201    }
7202
7203    /// The spellings C and C++ share, each in a declaration both
7204    /// grammars parse without error.
7205    #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
7206    const C_FAMILY_MODIFIER_SPELLINGS: &[(&str, &str)] = &[
7207        ("static", "static int a;\n"),
7208        ("extern", "extern int a;\n"),
7209        ("register", "register int a;\n"),
7210        ("inline", "inline int f(void);\n"),
7211        ("__inline", "__inline int f(void);\n"),
7212        ("__inline__", "__inline__ int f(void);\n"),
7213        ("__forceinline", "__forceinline int f(void);\n"),
7214        ("thread_local", "thread_local int a;\n"),
7215        ("__thread", "__thread int a;\n"),
7216        ("const", "const int a = 1;\n"),
7217        ("constexpr", "constexpr int a = 1;\n"),
7218        ("volatile", "volatile int a;\n"),
7219        ("restrict", "int * restrict p;\n"),
7220        ("__restrict__", "int * __restrict__ p;\n"),
7221        ("_Atomic", "_Atomic int a;\n"),
7222        ("_Noreturn", "_Noreturn void f(void);\n"),
7223        ("noreturn", "noreturn void f(void);\n"),
7224        ("_Nonnull", "int * _Nonnull p;\n"),
7225        ("alignas", "alignas(8) int a;\n"),
7226        ("_Alignas", "_Alignas(8) int a;\n"),
7227        ("__extension__", "__extension__ int a;\n"),
7228        // The one place the grammar emits `__volatile__`: as a GNU `asm`
7229        // qualifier, where the plain spelling bills too.
7230        (
7231            "__volatile__",
7232            "void f(void) { __asm__ __volatile__(\"nop\"); }\n",
7233        ),
7234        ("volatile", "void f(void) { asm volatile(\"nop\"); }\n"),
7235    ];
7236
7237    #[cfg(feature = "c")]
7238    #[test]
7239    fn c_modifier_spellings_bill_1578() {
7240        assert_modifier_spellings_bill::<CParser>("foo.c", C_FAMILY_MODIFIER_SPELLINGS);
7241        // C's storage class `auto`; C++ has none (see the Cpp arm).
7242        assert_modifier_spellings_bill::<CParser>("foo.c", &[("auto", "auto int a;\n")]);
7243    }
7244
7245    #[cfg(all(feature = "cpp", feature = "mozcpp"))]
7246    #[test]
7247    fn cpp_modifier_spellings_bill_1578() {
7248        const CPP_ONLY: &[(&str, &str)] = &[
7249            ("virtual", "struct S { virtual void f(); };\n"),
7250            ("mutable", "struct S { mutable int a; };\n"),
7251            ("constinit", "constinit int a = 1;\n"),
7252            ("consteval", "consteval int f() { return 1; }\n"),
7253            ("explicit", "struct S { explicit S(int); };\n"),
7254            ("friend", "struct S { friend class D; };\n"),
7255            ("public", "class C { public: int a; };\n"),
7256            ("private", "class C { private: int a; };\n"),
7257            ("protected", "class C { protected: int a; };\n"),
7258            ("override", "struct S { void f() override; };\n"),
7259            ("final", "struct S final {};\n"),
7260            ("noexcept", "void f() noexcept;\n"),
7261        ];
7262        for rows in [C_FAMILY_MODIFIER_SPELLINGS, CPP_ONLY] {
7263            assert_modifier_spellings_bill::<CppParser>("foo.cpp", rows);
7264            assert_modifier_spellings_bill::<MozcppParser>("foo.cpp", rows);
7265        }
7266    }
7267
7268    /// #1552: ObjC bills C's `struct` and its own class and protocol
7269    /// declaration keywords. `@class` already billed its `@` and stays
7270    /// as it was.
7271    #[cfg(feature = "objc")]
7272    #[test]
7273    fn objc_declaration_keywords_are_operators_1552() {
7274        const ROWS: &[(&str, &str, [u64; 2])] = &[
7275            // `@interface`, `:`
7276            ("interface", "@interface C : NSObject\n@end\n", [2, 2]),
7277            // `@implementation`
7278            ("implementation", "@implementation C\n@end\n", [1, 1]),
7279            // `@protocol`
7280            ("protocol", "@protocol P\n@end\n", [1, 1]),
7281            // `struct`, `{}`, `int`, `;` twice
7282            ("struct", "struct S { int a; };\n", [4, 5]),
7283            // `@`, `;` — unchanged by #1552
7284            ("forward class", "@class Fwd;\n", [2, 2]),
7285        ];
7286        assert_operator_rows::<ObjcParser>("foo.m", ROWS);
7287    }
7288
7289    /// #1579: ObjC bills C's declaration modifiers as `c.rs` does
7290    /// (#1578), and its own: ivar visibility labels, property attribute
7291    /// words, ARC ownership and nullability qualifiers, protocol section
7292    /// labels, method-type qualifiers and generic variance. Before the
7293    /// fix none of them billed.
7294    #[cfg(feature = "objc")]
7295    #[test]
7296    fn objc_modifiers_are_operators_1579() {
7297        const ROWS: &[(&str, &str, [u64; 2])] = &[
7298            // `static`, `const`, `int`, `=`, `;`: the issue's first line
7299            ("C modifiers", "static const int a = 1;\n", [5, 5]),
7300            // `@interface`, `{}`, `@public`, `@private`, `int` x2, `;` x2
7301            (
7302                "ivar visibility",
7303                "@interface C { @public int x; @private int y; }\n@end\n",
7304                [6, 8],
7305            ),
7306            // `@interface`, `()`, `nonatomic`, `,` x2, `strong`,
7307            // `getter`, `=`, `;`: `isY` is an operand, `id` and
7308            // `@property` bill nothing
7309            (
7310                "property attributes",
7311                "@interface C\n@property (nonatomic, strong, getter=isY) id y;\n@end\n",
7312                [8, 9],
7313            ),
7314            // `__weak`, `__strong`, `__block`, `int`, `;` x3
7315            (
7316                "ownership qualifiers",
7317                "__weak id a;\n__strong id b;\n__block int c;\n",
7318                [5, 7],
7319            ),
7320            // `*` x2, `_Nullable`, `nullable`, `;` x2
7321            (
7322                "nullability qualifiers",
7323                "NSString * _Nullable f;\nNSString * nullable k;\n",
7324                [4, 6],
7325            ),
7326            // `@protocol`, `@optional`, `@required`, `-` x2, `()` x3,
7327            // `void` x2, `;` x2, `oneway`, `:`, `out`, `*`
7328            (
7329                "protocol sections and method qualifiers",
7330                "@protocol P\n@optional\n- (void)m;\n@required\n\
7331                 - (oneway void)n:(out id *)b;\n@end\n",
7332                [11, 16],
7333            ),
7334            // `@interface`, `<>`, `__covariant`, `:`
7335            (
7336                "generic variance",
7337                "@interface G<__covariant T> : NSObject\n@end\n",
7338                [4, 4],
7339            ),
7340        ];
7341        assert_operator_rows::<ObjcParser>("foo.m", ROWS);
7342    }
7343
7344    /// #1579: the `nullable` qualifier and the `nullable` property
7345    /// attribute are one operator. The attribute has no keyword kind, so
7346    /// it is keyed by text, and keying the qualifier by kind would list
7347    /// the one word twice in `n1`.
7348    #[cfg(feature = "objc")]
7349    #[test]
7350    fn objc_nullable_is_one_operator_1579() {
7351        const SOURCE: &str = "@interface C\n@property (nullable) id y;\n- (nullable id)m;\n@end\n";
7352        // `@interface`, `()` x2, `nullable` x2, `-`, `;` x2
7353        assert_operator_rows::<ObjcParser>("foo.m", &[("nullable", SOURCE, [5, 8])]);
7354        let ops = ops_of::<ObjcParser>(SOURCE, "foo.m");
7355        assert_eq!(ops.operators, ["()", "-", ";", "@interface", "nullable"]);
7356        assert_eq!(ops.operands, ["C", "m", "y"]);
7357    }
7358
7359    /// #1579's oracle: a `.m` is a C translation unit, so C's
7360    /// modifier-heavy declarations score alike in both grammars,
7361    /// vocabulary and multiplicities. Neither expectation is typed by
7362    /// the fix: a modifier one getter drops shows as a disagreement.
7363    #[cfg(all(feature = "c", feature = "objc"))]
7364    #[test]
7365    fn objc_c_modifiers_match_c_1579() {
7366        let source = "static inline const volatile int a = 1;\nextern int b;\n\
7367                      register int c;\nauto int d;\nint * restrict p;\n_Atomic int g;\n\
7368                      __thread int t;\n_Noreturn void h(void);\n_Alignas(8) int m;\n";
7369        let objc = ops_of::<ObjcParser>(source, "foo.m");
7370        let c = ops_of::<CParser>(source, "foo.c");
7371        assert_eq!(objc.operators, c.operators);
7372        assert_eq!(objc.operands, c.operands);
7373        // `static`, `inline`, `const`, `volatile`, `int` x8, `=`, `;` x9,
7374        // `extern`, `register`, `auto`, `*`, `restrict`, `_Atomic`,
7375        // `__thread`, `_Noreturn`, `void` x2, `()` x2, `_Alignas`
7376        let rows = [("C modifiers", source, [18, 35])];
7377        assert_operator_rows::<ObjcParser>("foo.m", &rows);
7378        assert_operator_rows::<CParser>("foo.c", &rows);
7379    }
7380
7381    /// #1579: every modifier word the ObjC arm lists bills under its own
7382    /// name, one fixture apiece, so dropping any kind from the arm fails
7383    /// its own entry. The spellings C shares with C++ run through the
7384    /// #1578 table; `OBJC_ONLY` holds the rest, C's `auto` included, plus
7385    /// a sample of the text-keyed property attributes.
7386    #[cfg(feature = "objc")]
7387    #[test]
7388    fn objc_modifier_spellings_bill_1579() {
7389        const OBJC_ONLY: &[(&str, &str)] = &[
7390            ("auto", "auto int a;\n"),
7391            ("__const", "__const int a;\n"),
7392            ("_Complex", "double _Complex z;\n"),
7393            ("__complex", "double __complex z;\n"),
7394            ("nullable", "NSString * nullable p;\n"),
7395            ("_Nullable", "NSString * _Nullable p;\n"),
7396            ("__nullable", "NSString * __nullable p;\n"),
7397            ("_Nullable_result", "NSString * _Nullable_result p;\n"),
7398            ("_Null_unspecified", "NSString * _Null_unspecified p;\n"),
7399            ("__nonnull", "NSString * __nonnull p;\n"),
7400            ("__autoreleasing", "__autoreleasing id a;\n"),
7401            ("__block", "__block int a;\n"),
7402            ("__strong", "__strong id a;\n"),
7403            ("__unsafe_unretained", "__unsafe_unretained id a;\n"),
7404            ("__weak", "__weak id a;\n"),
7405            ("__bridge", "void f(void) { id x = (__bridge id)p; }\n"),
7406            (
7407                "__bridge_retained",
7408                "void f(void) { void *x = (__bridge_retained void *)p; }\n",
7409            ),
7410            (
7411                "__bridge_transfer",
7412                "void f(void) { id x = (__bridge_transfer id)p; }\n",
7413            ),
7414            ("__kindof", "__kindof NSView *v;\n"),
7415            (
7416                "__covariant",
7417                "@interface G<__covariant T> : NSObject\n@end\n",
7418            ),
7419            (
7420                "__contravariant",
7421                "@interface G<__contravariant T> : NSObject\n@end\n",
7422            ),
7423            ("in", "@protocol P\n- (void)m:(in id)a;\n@end\n"),
7424            ("out", "@protocol P\n- (void)m:(out id *)a;\n@end\n"),
7425            ("inout", "@protocol P\n- (void)m:(inout id *)a;\n@end\n"),
7426            ("bycopy", "@protocol P\n- (void)m:(bycopy id)a;\n@end\n"),
7427            ("byref", "@protocol P\n- (void)m:(byref id)a;\n@end\n"),
7428            ("oneway", "@protocol P\n- (oneway void)m;\n@end\n"),
7429            ("@public", "@interface C { @public int x; }\n@end\n"),
7430            ("@private", "@interface C { @private int x; }\n@end\n"),
7431            ("@protected", "@interface C { @protected int x; }\n@end\n"),
7432            ("@package", "@interface C { @package int x; }\n@end\n"),
7433            ("@optional", "@protocol P\n@optional\n- (void)m;\n@end\n"),
7434            ("@required", "@protocol P\n@required\n- (void)m;\n@end\n"),
7435            ("copy", "@interface C\n@property (copy) id y;\n@end\n"),
7436            (
7437                "readonly",
7438                "@interface C\n@property (readonly) id y;\n@end\n",
7439            ),
7440            ("class", "@interface C\n@property (class) id y;\n@end\n"),
7441            (
7442                "setter",
7443                "@interface C\n@property (setter=sY:) id y;\n@end\n",
7444            ),
7445        ];
7446        assert_modifier_spellings_bill::<ObjcParser>("foo.m", C_FAMILY_MODIFIER_SPELLINGS);
7447        assert_modifier_spellings_bill::<ObjcParser>("foo.m", OBJC_ONLY);
7448    }
7449
7450    /// #1579: the plain-word modifiers stay operands where they name a
7451    /// variable, and a property attribute's `getter=` value is the
7452    /// selector it names, not a modifier. The arms carry no guard on
7453    /// these words, so this pins the grammar: it emits an `identifier`
7454    /// for each name, and only the first word of a `property_attribute`
7455    /// bills.
7456    #[cfg(feature = "objc")]
7457    #[test]
7458    fn objc_modifier_words_as_names_stay_operands_1579() {
7459        assert_keywords_are_operands_only::<ObjcParser>(
7460            "int out = 1;\nint copy = 2;\nint oneway = 3;\nint strong = out + copy;\n",
7461            "foo.m",
7462            &["out", "copy", "oneway", "strong"],
7463        );
7464        assert_keywords_are_operands_only::<ObjcParser>(
7465            "@interface C\n@property (getter=atomic, setter=weak:) id y;\n@end\n",
7466            "foo.m",
7467            &["atomic", "weak"],
7468        );
7469    }
7470
7471    /// #1579: the Apple SDK macros the grammar files as storage classes
7472    /// and qualifiers bill nothing, as Mozcpp's `MOZ_*` macros do: each
7473    /// annotates a declaration, and a `.c` parses the spelling as a name.
7474    #[cfg(feature = "objc")]
7475    #[test]
7476    fn objc_sdk_macros_bill_nothing_1579() {
7477        // `int` x4, `;` x4, `()`, `void`, `{}`, `return`: the macros add
7478        // nothing, and the declared names and `1` are the operands
7479        const SOURCE: &str = "NS_INLINE int f(void) { return 1; }\nFOUNDATION_EXPORT int g;\n\
7480                              IBOutlet int h;\n__unused int u;\n";
7481        let ops = ops_of::<ObjcParser>(SOURCE, "foo.m");
7482        assert_eq!(ops.operators, ["()", ";", "int", "return", "void", "{}"]);
7483        assert_eq!(ops.operands, ["1", "f", "g", "h", "u"]);
7484    }
7485
7486    /// #1556: Kotlin's `interface` bills as its `class` and `object`
7487    /// do. The modifier rows score one above the bare keyword since
7488    /// #1558: `enum` in `enum class` is a `class_modifier` like `data`
7489    /// and `sealed`, and Kotlin bills its modifiers as C# does.
7490    #[cfg(feature = "kotlin")]
7491    #[test]
7492    fn kotlin_declaration_keywords_are_operators_1556() {
7493        const ROWS: &[(&str, &str, [u64; 2])] = &[
7494            // `class`, `{}`
7495            ("class", "class C {}\n", [2, 2]),
7496            // `interface`, `{}`
7497            ("interface", "interface I {}\n", [2, 2]),
7498            // `object`, `{}`
7499            ("object", "object O {}\n", [2, 2]),
7500            // `enum`, `class`, `{}`
7501            ("enum class", "enum class E { A }\n", [3, 3]),
7502            // `data`, `class`, `{}`
7503            ("data class", "data class D {}\n", [3, 3]),
7504            // `sealed`, `interface`, `{}`
7505            ("sealed interface", "sealed interface S {}\n", [3, 3]),
7506            // `interface`, `class`, `:`, `{}` twice
7507            (
7508                "implementing class",
7509                "interface J {}\nclass K : J {}\n",
7510                [4, 5],
7511            ),
7512        ];
7513        assert_operator_rows::<KotlinParser>("foo.kt", ROWS);
7514    }
7515
7516    /// #1558: Kotlin bills its modifier keywords as C# bills its own,
7517    /// one family per row, each keyword the leaf and never
7518    /// its `*_modifier` wrapper. The "names" row is the control: the same
7519    /// words used as names are operands, and add no operator. The rows
7520    /// after it are names the grammar fails to parse, whose recovery keeps
7521    /// the keyword leaf, and their control.
7522    #[cfg(feature = "kotlin")]
7523    #[test]
7524    fn kotlin_modifiers_are_operators_1558() {
7525        const ROWS: &[(&str, &str, [u64; 2])] = &[
7526            // `public`, `private`, `protected`, `internal`, `val` x4,
7527            // `=` x4, `class`, `{}`
7528            (
7529                "visibility",
7530                "class C {\n    public val a = 1\n    private val b = 1\n    \
7531                 protected val c = 1\n    internal val d = 1\n}\n",
7532                [8, 14],
7533            ),
7534            // `abstract` x2, `open`, `final`, `override`, `class`, `{}` x2,
7535            // `fun` x3, `()` x3, `=`
7536            (
7537                "inheritance and member",
7538                "abstract class A {\n    abstract fun f()\n    open fun g() {}\n    \
7539                 final override fun toString() = \"\"\n}\n",
7540                [9, 15],
7541            ),
7542            // `inner`, `value`, `annotation`, `class` x3, `()`, `val`, `:`
7543            (
7544                "class",
7545                "inner class I\nvalue class V(val x: Int)\nannotation class N\n",
7546                [7, 9],
7547            ),
7548            // `lateinit`, `companion`, `const`, `class`, `object`, `var`,
7549            // `val`, `:`, `=`, `{}` x2
7550            (
7551                "property and companion",
7552                "class C {\n    lateinit var a: String\n    companion object {\n        \
7553                 const val b = 1\n    }\n}\n",
7554                [10, 11],
7555            ),
7556            // six modifiers, `fun` x6, `()` x6, `{}` x5
7557            (
7558                "function",
7559                "tailrec fun a() {}\noperator fun b() {}\ninfix fun c() {}\n\
7560                 inline fun d() {}\nexternal fun e()\nsuspend fun f() {}\n",
7561                [9, 23],
7562            ),
7563            // `inline`, `noinline`, `crossinline`, `vararg`, `fun`,
7564            // `()` x3, `:` x3, `->` x2, `,` x2, `{}`
7565            (
7566                "parameter",
7567                "inline fun f(noinline a: () -> Unit, crossinline b: () -> Unit, \
7568                 vararg c: Int) {}\n",
7569                [10, 16],
7570            ),
7571            // `inline`, `reified`, `in`, `out`, `fun`, `class`, `<>` x2,
7572            // `,`, `()`, `{}`
7573            (
7574                "type parameter",
7575                "inline fun <reified T> f() {}\nclass Box<in A, out B>\n",
7576                [10, 11],
7577            ),
7578            // `expect`, `actual`, `fun` x2, `()` x2, `{}`
7579            ("platform", "expect fun f()\nactual fun g() {}\n", [5, 7]),
7580            // `fun`, `()`, `{}`: an `@N` use is the `annotation` node, not
7581            // the keyword, so it bills no `annotation` operator
7582            ("annotation use", "@N fun k() {}\n", [3, 3]),
7583            // `val` x3, `=` x3: `open`, `out` and `value` are names here
7584            (
7585                "names",
7586                "val open = 1\nval out = 2\nval value = 3\n",
7587                [2, 6],
7588            ),
7589            // The rows below are names the grammar fails to parse. Its
7590            // recovery keeps the keyword leaf, which must bill nothing;
7591            // each expected value is what the source scored before #1558.
7592            // `class`, `{}` x2, `var`, `=` x2, `fun`, `()`: `open = !open`
7593            // recovers into an `ERROR` holding `inheritance_modifier`.
7594            (
7595                "name stranded in an ERROR",
7596                "class Drawer {\n    var open = false\n    fun toggle() { open = !open }\n}\n",
7597                [6, 8],
7598            ),
7599            // `val`: `suspend` never parses as a name.
7600            ("suspend as a name", "val suspend = 1\n", [1, 1]),
7601            // `fun`, `()`, `:` x2, `=`: a modifier list followed by an
7602            // inserted zero-width parameter name.
7603            (
7604                "parameter named vararg",
7605                "fun f(vararg: Int): Int = vararg\n",
7606                [4, 5],
7607            ),
7608            // The same row with a comment before the inserted name: an
7609            // extra, so the name is still the next token. It scored
7610            // [5, 6], a phantom `vararg` operator.
7611            (
7612                "parameter named vararg, commented",
7613                "fun f(vararg /*note*/: Int): Int = vararg\n",
7614                [4, 5],
7615            ),
7616            // Control: a real `vararg` before a commented parameter keeps
7617            // its operator. `vararg`, `fun`, `()`, `:`, `{}`.
7618            (
7619                "vararg modifier, commented",
7620                "fun f(vararg /*note*/ x: Int) {}\n",
7621                [5, 5],
7622            ),
7623            // `class`, `()`, `:`
7624            ("class parameter named open", "class C(open: Int)\n", [3, 3]),
7625            // `val`, `=`, `{}`, `:`
7626            (
7627                "lambda parameter named open",
7628                "val l = { open: Int -> open }\n",
7629                [4, 4],
7630            ),
7631            // Control for the gate: a real `suspend` in a function type's
7632            // `type_modifiers` and in a declaration's `modifiers` bills
7633            // twice. `val`, `:`, `suspend` x2, `()` x2, `->`, `=`, `{}` x2,
7634            // `fun`.
7635            (
7636                "suspend modifiers",
7637                "val f: suspend () -> Unit = {}\nsuspend fun g() {}\n",
7638                [8, 11],
7639            ),
7640        ];
7641        assert_operator_rows::<KotlinParser>("foo.kt", ROWS);
7642    }
7643
7644    /// #1558: `reified` is the one Kotlin modifier spelled as an aliased
7645    /// named kind. `KotlinCode::is_primitive` keys it by its text, so
7646    /// `bca ops` lists the word written, not the grammar's
7647    /// `reification_modifier`. `n1` cannot see the difference — the kind
7648    /// has one spelling either way — so the vocabulary is the only place
7649    /// this is observable.
7650    #[cfg(feature = "kotlin")]
7651    #[test]
7652    fn kotlin_reified_is_listed_by_its_text_1558() {
7653        let ops = ops_of::<KotlinParser>("fun <reified T> f() {}\n", "foo.kt");
7654        assert!(
7655            ops.operators.iter().any(|op| op == "reified"),
7656            "{:?}",
7657            ops.operators
7658        );
7659        assert!(
7660            !ops.operators.iter().any(|op| op == "reification_modifier"),
7661            "{:?}",
7662            ops.operators
7663        );
7664    }
7665
7666    /// #1564: `typealias`, `init`, `where`, `by`, `get` and `set` bill
7667    /// as Kotlin's other declaration keywords do, and as C# bills its
7668    /// `get` / `set` / `init` / `where`. Each is the keyword leaf, never
7669    /// its wrapper. The rows after "accessors" are controls: the
7670    /// `@get:` / `@set:` use-site targets and the words used as names
7671    /// add no operator.
7672    #[cfg(feature = "kotlin")]
7673    #[test]
7674    fn kotlin_declaration_keywords_are_operators_1564() {
7675        const ROWS: &[(&str, &str, [u64; 2])] = &[
7676            // `typealias`, `=`
7677            ("typealias", "typealias A = Int\n", [2, 2]),
7678            // `class`, `init`, `{}` x2
7679            ("init", "class C {\n    init {}\n}\n", [3, 4]),
7680            // `class`, `<>`, `where`, `:`
7681            ("class where", "class C<T> where T : Any\n", [4, 4]),
7682            // `fun`, `<>`, `()`, `where`, `:`, `{}`
7683            ("function where", "fun <T> f() where T : Any {}\n", [6, 6]),
7684            // `val`, `by`, `{}`
7685            ("property delegate", "val d by lazy { 1 }\n", [3, 3]),
7686            // `class`, `()`, `:` x2, `by`
7687            ("class delegation", "class D(d: I) : I by d\n", [4, 5]),
7688            // `var`, `:`, `=` x3, `get`, `()` x2, `set`, `{}`
7689            (
7690                "accessors",
7691                "var p: Int = 0\n    get() = field\n    set(v) { field = v }\n",
7692                [7, 10],
7693            ),
7694            // `var`, `=`, `:` x2: the use-site targets bill nothing, as
7695            // `@field:` does
7696            ("use-site targets", "@get:A @set:B var q = 0\n", [3, 4]),
7697            // `val`, `=`
7698            (
7699                "names",
7700                "val get = 1\nval set = 2\nval by = 3\nval where = 4\nval init = 5\n",
7701                [2, 10],
7702            ),
7703            // `fun`, `()` x4, `:`, `{}`, `.` x2
7704            (
7705                "calls",
7706                "fun g(x: X) {\n    x.get(by)\n    x.set(where)\n    init(x)\n}\n",
7707                [5, 9],
7708            ),
7709            // `fun`, `()` x2, `{}`, `val`, `=`: valid Kotlin that kotlin-ng
7710            // misreads, leaving the call's `get` leaf in an `ERROR`
7711            (
7712                "call after a local val",
7713                "fun g() {\n    val f = 6\n    get(1)\n}\n",
7714                [5, 6],
7715            ),
7716        ];
7717        assert_operator_rows::<KotlinParser>("foo.kt", ROWS);
7718    }
7719
7720    /// #1564: the six words are listed by their own spelling, and the
7721    /// accessor body's `field` stays an operand like any variable.
7722    #[cfg(feature = "kotlin")]
7723    #[test]
7724    fn kotlin_declaration_keyword_vocabulary_1564() {
7725        let ops = ops_of::<KotlinParser>(
7726            "typealias A = Int\nclass C<T> where T : Any {\n    init {}\n    \
7727             val d by lazy { 1 }\n    var p: Int = 0\n        get() = field\n        \
7728             set(v) { field = v }\n}\n",
7729            "foo.kt",
7730        );
7731        for word in ["typealias", "init", "where", "by", "get", "set"] {
7732            assert!(
7733                ops.operators.iter().any(|op| op == word),
7734                "{word}: {:?}",
7735                ops.operators
7736            );
7737        }
7738        assert!(
7739            ops.operands.iter().any(|op| op == "field"),
7740            "{:?}",
7741            ops.operands
7742        );
7743
7744        // The words used as names are operands, not merely non-operators:
7745        // the operator rows above cannot see a name that bills nothing.
7746        let names = ops_of::<KotlinParser>(
7747            "fun get(x: X) {\n    val set = 1\n    val by = 2\n    x.init(where(set, by))\n}\n",
7748            "foo.kt",
7749        );
7750        for word in ["init", "where", "by", "get", "set"] {
7751            assert!(
7752                names.operands.iter().any(|op| op == word),
7753                "{word}: {:?}",
7754                names.operands
7755            );
7756            assert!(
7757                !names.operators.iter().any(|op| op == word),
7758                "{word}: {:?}",
7759                names.operators
7760            );
7761        }
7762    }
7763
7764    /// #1557 rows shared by the two C++ clones: the declaration keywords
7765    /// #1552 left out, one per row, plus the two disambiguator positions
7766    /// (`typename T::x`, `x.template f`) that bill the same leaf.
7767    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
7768    const CPP_MORE_DECLARATION_KEYWORD_ROWS: &[(&str, &str, [u64; 2])] = &[
7769        // `namespace`, `{}`
7770        ("namespace", "namespace N {}\n", [2, 2]),
7771        // `typedef`, `int`, `;`
7772        ("typedef", "typedef int T;\n", [3, 3]),
7773        // `using`, `=`, `int`, `;`
7774        ("alias declaration", "using U = int;\n", [4, 4]),
7775        // `using`, `namespace`, `;`
7776        ("using directive", "using namespace std;\n", [3, 3]),
7777        // `template`, `<>`, `typename`, `;` and the `struct` of `S`
7778        (
7779            "template declaration",
7780            "template <typename T> struct S;\n",
7781            [5, 5],
7782        ),
7783        // `typename`, `::`, `;`
7784        ("dependent type", "typename T::type x;\n", [3, 3]),
7785        // `void`, `()` twice, `{}`, `.`, `template`, `<>`, `int`, `;`
7786        (
7787            "template disambiguator",
7788            "void g() { x.template f<int>(); }\n",
7789            [8, 9],
7790        ),
7791    ];
7792
7793    /// One call per clone, as for the #1552 rows.
7794    #[cfg(all(feature = "cpp", feature = "mozcpp"))]
7795    #[test]
7796    fn cpp_more_declaration_keywords_are_operators_1557() {
7797        assert_operator_rows::<CppParser>("foo.cpp", CPP_MORE_DECLARATION_KEYWORD_ROWS);
7798        assert_operator_rows::<MozcppParser>("foo.cpp", CPP_MORE_DECLARATION_KEYWORD_ROWS);
7799    }
7800
7801    /// #1557: `typedef` is a C keyword, so C and ObjC bill it as C++
7802    /// does — a header scores alike whichever grammar parses it.
7803    #[cfg(any(feature = "c", feature = "objc"))]
7804    const C_TYPEDEF_ROWS: &[(&str, &str, [u64; 2])] = &[
7805        // `typedef`, `int`, `;`
7806        ("typedef", "typedef int T;\n", [3, 3]),
7807        // `typedef`, `struct`, `{}`, `int`, `;` twice
7808        ("typedef struct", "typedef struct S { int a; } S;\n", [5, 6]),
7809    ];
7810
7811    #[cfg(any(feature = "c", feature = "objc"))]
7812    #[test]
7813    fn c_family_typedef_is_an_operator_1557() {
7814        #[cfg(feature = "c")]
7815        assert_operator_rows::<CParser>("foo.c", C_TYPEDEF_ROWS);
7816        #[cfg(feature = "objc")]
7817        assert_operator_rows::<ObjcParser>("foo.m", C_TYPEDEF_ROWS);
7818    }
7819
7820    /// #1557: the TypeScript declaration keywords #1552 left out. The
7821    /// "keywords as names" row pins that each word, used as a *name*,
7822    /// stays an operand: the grammar emits an `identifier` there. The two
7823    /// exported-binding rows are the one place it emits the `type` leaf
7824    /// for a name, which must bill nothing; the last two rows are their
7825    /// controls, a type-only specifier and re-export whose `type` still
7826    /// bills.
7827    #[cfg(feature = "typescript")]
7828    const TS_DECLARATION_KEYWORD_ROWS: &[(&str, &str, [u64; 2])] = &[
7829        // `type`, `=`, `number`, `;`
7830        ("type alias", "type T = number;\n", [4, 4]),
7831        // `namespace`, `{}`
7832        ("namespace", "namespace N {}\n", [2, 2]),
7833        // `module`, `{}`. Billing the `module` declaration node instead
7834        // of its keyword leaf would score this row the same: every
7835        // spelling probed (bodiless, quoted, dotted, exported, ERROR
7836        // recovery) emits the two together, so only the getter's
7837        // choice of the leaf, not this row, keeps them from both billing.
7838        ("module", "module M {}\n", [2, 2]),
7839        // `declare`, `module`, `{}`
7840        ("ambient module", "declare module \"m\" {}\n", [3, 3]),
7841        // `declare`, `global`, `{}`
7842        ("global augmentation", "declare global {}\n", [3, 3]),
7843        // `import`, `type`, `{}`, `from`, `;`
7844        (
7845            "type-only import",
7846            "import type { A } from \"a\";\n",
7847            [5, 5],
7848        ),
7849        // `let`, `=` twice, `;` twice, `.` — no keyword
7850        (
7851            "keywords as names",
7852            "let type = 1;\nmodule.exports = declare;\n",
7853            [4, 6],
7854        ),
7855        // `const`, `=`, `;` twice, `export`, `{}` — no keyword. The
7856        // grammar recovers an exported binding named `type` as the
7857        // keyword leaf, under an `ERROR` or beside one in the specifier.
7858        (
7859            "exported binding named type",
7860            "const type = 1;\nexport { type };\n",
7861            [5, 6],
7862        ),
7863        (
7864            "renamed exported binding named type",
7865            "const type = 1;\nexport { type as kind };\n",
7866            [5, 6],
7867        ),
7868        // `export`, `{}`, `type`, `from`, `;` — a type-only specifier
7869        // parses cleanly, so its keyword still bills.
7870        (
7871            "type-only export specifier",
7872            "export { type Foo } from \"m\";\n",
7873            [5, 5],
7874        ),
7875        // `export`, `type`, `*`, `from`, `;` — the pinned grammar fails
7876        // TS 5.0's type-only re-export into an `ERROR` that hangs from the
7877        // statement, not from an export clause, and its keyword bills.
7878        (
7879            "type-only re-export",
7880            "export type * from \"./types\";\n",
7881            [5, 5],
7882        ),
7883    ];
7884
7885    /// #1557: a TypeScript type name is an operand, as C#, Rust and the
7886    /// C family bill theirs. Each row's names are `type_identifier`
7887    /// leaves except where noted, and before #1557 none of them billed.
7888    #[cfg(feature = "typescript")]
7889    const TS_TYPE_NAME_OPERAND_ROWS: &[(&str, &str, [u64; 2])] = &[
7890        // `T`
7891        ("alias name", "type T = number;\n", [1, 1]),
7892        // `I`
7893        ("interface name", "interface I {}\n", [1, 1]),
7894        // `C`
7895        ("class name", "class C {}\n", [1, 1]),
7896        // `x` (an identifier), `Foo`
7897        ("annotation", "let x: Foo;\n", [2, 2]),
7898        // `m`, `Map`, `K` twice
7899        ("generic", "let m: Map<K, K>;\n", [3, 4]),
7900        // `f`, `a` (identifiers), `U` twice
7901        ("type parameter", "function f<U>(a: U) {}\n", [3, 4]),
7902        // `q`, `ns` (identifiers), `T` — the `nested_type_identifier`
7903        // wrapper is unlisted, so its text is not a fourth operand
7904        ("qualified type", "let q: ns.T;\n", [3, 3]),
7905        // `C`, `I`
7906        ("implements", "class C implements I {}\n", [2, 2]),
7907    ];
7908
7909    #[cfg(feature = "typescript")]
7910    #[test]
7911    fn typescript_declaration_keywords_and_type_names_1557() {
7912        assert_operator_rows::<TypescriptParser>("foo.ts", TS_DECLARATION_KEYWORD_ROWS);
7913        assert_operator_rows::<TsxParser>("foo.tsx", TS_DECLARATION_KEYWORD_ROWS);
7914        assert_operand_rows::<TypescriptParser>("foo.ts", TS_TYPE_NAME_OPERAND_ROWS);
7915        assert_operand_rows::<TsxParser>("foo.tsx", TS_TYPE_NAME_OPERAND_ROWS);
7916    }
7917
7918    /// #1561: TypeScript's type-level operator keywords bill as their
7919    /// value-level siblings `as`, `typeof` and `extends` do, each as its
7920    /// keyword leaf under an unbilled wrapper. The `asserts a is Baz` row
7921    /// pins that the `asserts` node and its keyword token bill once
7922    /// between them, and the last row that the same words used as names
7923    /// are operands. Both TypeScript grammars carry the change.
7924    #[cfg(feature = "typescript")]
7925    #[test]
7926    fn typescript_type_operator_keywords_1561() {
7927        const ROWS: &[(&str, &str, [u64; 2])] = &[
7928            // `let`, `=`, `satisfies`, `;`
7929            ("satisfies", "let s = w satisfies Sat;\n", [4, 4]),
7930            // `type`, `=`, `keyof`, `;`
7931            ("keyof", "type K = keyof T;\n", [4, 4]),
7932            // `type`, `<>` x2, `=`, `extends`, `infer`, `?`, `:`, `;`
7933            (
7934                "infer",
7935                "type I<X> = X extends Array<infer E> ? E : X;\n",
7936                [8, 9],
7937            ),
7938            // `function`, `()`, `:` x2, `unknown`, `is`, `{}`, `return`, `;`
7939            (
7940                "type predicate",
7941                "function g(a: unknown): a is Baz { return true; }\n",
7942                [8, 9],
7943            ),
7944            // `function`, `()`, `:` x2, `unknown`, `asserts`, `is`, `{}`
7945            (
7946                "asserts predicate",
7947                "function h(a: unknown): asserts a is Baz {}\n",
7948                [7, 8],
7949            ),
7950            // `function`, `()`, `:` x2, `unknown`, `asserts`, `{}`
7951            (
7952                "bare asserts",
7953                "function k(a: unknown): asserts a {}\n",
7954                [6, 7],
7955            ),
7956            // `const`, `=` and `;` five times each: the words are names
7957            (
7958                "names",
7959                "const keyof = 1;\nconst infer = 2;\nconst is = 3;\nconst asserts = 4;\n\
7960                 const satisfies = 5;\n",
7961                [3, 15],
7962            ),
7963        ];
7964        assert_operator_rows::<TypescriptParser>("foo.ts", ROWS);
7965        assert_operator_rows::<TsxParser>("foo.tsx", ROWS);
7966    }
7967
7968    /// #1563: TypeScript bills its member modifiers as C# and Kotlin bill
7969    /// theirs, each as the keyword leaf and never as the
7970    /// `accessibility_modifier` / `override_modifier` wrapper. The rows
7971    /// after the class members are the same leaves outside a class; the
7972    /// last two are the words used as names, which add no operator.
7973    #[cfg(feature = "typescript")]
7974    const TS_MEMBER_MODIFIER_ROWS: &[(&str, &str, [u64; 2])] = &[
7975        // `class`, `{}`, `public`, `private`, `protected`, `=` x3, `;` x3
7976        (
7977            "visibility",
7978            "class C {\n  public a = 1;\n  private b = 2;\n  protected c = 3;\n}\n",
7979            [7, 11],
7980        ),
7981        // `class`, `extends`, `{}`, `static`, `readonly`, `override`,
7982        // `=` x2, `;` x2
7983        (
7984            "static, readonly, override",
7985            "class D extends B {\n  static readonly x = 1;\n  override y = 2;\n}\n",
7986            [8, 10],
7987        ),
7988        // `abstract` x2, `class`, `{}`, `()`, `:`, `void`, `;`
7989        (
7990            "abstract",
7991            "abstract class A {\n  abstract m(): void;\n}\n",
7992            [7, 8],
7993        ),
7994        // `class`, `{}`, `accessor`, `=`, `;`
7995        ("accessor", "class E {\n  accessor q = 1;\n}\n", [5, 5]),
7996        // `class`, `{}` x3, `static` x2, `()`
7997        (
7998            "static block and method",
7999            "class F {\n  static {}\n  static m() {}\n}\n",
8000            [4, 7],
8001        ),
8002        // `class`, `{}` x2, `()`, `private`, `readonly`, `:` x2, `number`,
8003        // `,`, `public`, `string`
8004        (
8005            "parameter properties",
8006            "class G {\n  constructor(private readonly p: number, public o: string) {}\n}\n",
8007            [10, 12],
8008        ),
8009        // `type`, `=`, `readonly`, `string`, `[]`, `;`
8010        ("readonly type", "type R = readonly string[];\n", [6, 6]),
8011        // `type`, `=`, `{}`, `-`, `readonly`, `[]`, `in`, `:`, `;`
8012        (
8013            "mapped type modifier",
8014            "type M = { -readonly [k in K]: T };\n",
8015            [9, 9],
8016        ),
8017        // `type`, `=`, `abstract`, `new`, `()`, `=>`, `void`, `;`
8018        (
8019            "abstract constructor type",
8020            "type A = abstract new () => void;\n",
8021            [8, 8],
8022        ),
8023        // `let`, `const` x2, `=` x3, `;` x4, `.`, `()`, `{}`, `:` x5,
8024        // `,` x4 — no modifier
8025        (
8026            "names",
8027            "let readonly = 1;\nobj.static;\nconst a = abstract();\n\
8028             const o = { public: 1, private: 2, protected: 3, override: 4, accessor: 5 };\n",
8029            [9, 22],
8030        ),
8031        // `class`, `{}` x2, `static`, `=`, `;` x2, `()` — the modifier
8032        // bills once; the field named `static`, the method named
8033        // `readonly` and the field named `public` bill none
8034        (
8035            "member names",
8036            "class K {\n  static static = 1;\n  readonly() {}\n  public;\n}\n",
8037            [6, 8],
8038        ),
8039    ];
8040
8041    #[cfg(feature = "typescript")]
8042    #[test]
8043    fn typescript_member_modifiers_are_operators_1563() {
8044        assert_operator_rows::<TypescriptParser>("foo.ts", TS_MEMBER_MODIFIER_ROWS);
8045        assert_operator_rows::<TsxParser>("foo.tsx", TS_MEMBER_MODIFIER_ROWS);
8046    }
8047
8048    /// #1563: the issue's own fixture, by vocabulary. Every modifier
8049    /// reaches the operator list once, by its word and never by its
8050    /// wrapper's kind name, and the names stay the only operands.
8051    #[cfg(feature = "typescript")]
8052    #[test]
8053    fn typescript_member_modifier_vocabulary_1563() {
8054        const SOURCE: &str = "abstract class C {\n  declare x: number;\n  \
8055                              public static readonly y = 1;\n  private z = 2;\n  \
8056                              protected override w = 3;\n  abstract m(): void;\n  \
8057                              accessor q = 4;\n}\n";
8058        const OPERATORS: &[&str] = &[
8059            "()",
8060            ":",
8061            ";",
8062            "=",
8063            "abstract",
8064            "accessor",
8065            "class",
8066            "declare",
8067            "number",
8068            "override",
8069            "private",
8070            "protected",
8071            "public",
8072            "readonly",
8073            "static",
8074            "void",
8075            "{}",
8076        ];
8077        const OPERANDS: &[&str] = &["1", "2", "3", "4", "C", "m", "q", "w", "x", "y", "z"];
8078        for (file, ops) in [
8079            ("foo.ts", ops_of::<TypescriptParser>(SOURCE, "foo.ts")),
8080            ("foo.tsx", ops_of::<TsxParser>(SOURCE, "foo.tsx")),
8081        ] {
8082            assert_eq!(ops.operators, OPERATORS, "{file}");
8083            assert_eq!(ops.operands, OPERANDS, "{file}");
8084        }
8085    }
8086
8087    /// #1563: the modifier words used as names are operands and never
8088    /// operators, in an expression, a member access, a call, an object
8089    /// key and a class member name. No getter edit makes this fail: the
8090    /// modifier arms key on the keyword kinds, and the grammar emits an
8091    /// identifier for each name. It pins that grammar property, so a
8092    /// grammar bump or a text-keyed arm that bills a name trips it.
8093    #[cfg(feature = "typescript")]
8094    #[test]
8095    fn typescript_modifier_words_as_names_are_operands_1563() {
8096        const SOURCE: &str = "let readonly = 1;\nobj.static;\nconst a = abstract();\n\
8097                              const o = { public: 1, private: 2, protected: 3, override: 4, \
8098                              accessor: 5 };\nclass K { static() {} }\n";
8099        const KEYWORDS: &[&str] = &[
8100            "readonly",
8101            "static",
8102            "abstract",
8103            "public",
8104            "private",
8105            "protected",
8106            "override",
8107            "accessor",
8108        ];
8109        assert_keywords_are_operands_only::<TypescriptParser>(SOURCE, "foo.ts", KEYWORDS);
8110        assert_keywords_are_operands_only::<TsxParser>(SOURCE, "foo.tsx", KEYWORDS);
8111    }
8112
8113    /// #1563: JavaScript's `static` bills as TypeScript's does, in a
8114    /// field, a method and a `static { … }` block. `static get` with the
8115    /// name on the next line is one token, so it is one operator. The
8116    /// last row is the word used as names, which adds no operator.
8117    #[cfg(any(feature = "javascript", feature = "mozjs"))]
8118    const JS_STATIC_ROWS: &[(&str, &str, [u64; 2])] = &[
8119        // `class`, `{}`, `static`, `=`, `;`
8120        ("static field", "class K {\n  static x = 1;\n}\n", [5, 5]),
8121        // `class`, `{}` x3, `static` x2, `()`
8122        (
8123            "static method and block",
8124            "class K {\n  static m() {}\n  static {}\n}\n",
8125            [4, 7],
8126        ),
8127        // `class`, `{}` x2, `static`, `get`, `()`
8128        (
8129            "static getter",
8130            "class K {\n  static get y() {}\n}\n",
8131            [5, 6],
8132        ),
8133        // `class`, `{}` x2, `static get`, `()`
8134        (
8135            "static get before a newline",
8136            "class K {\n  static get\n  y() {}\n}\n",
8137            [4, 5],
8138        ),
8139        // `.`, `;` x3, `()`, `var`, `=`, `{}`, `:` — no `static`
8140        (
8141            "names",
8142            "obj.static;\nstatic();\nvar o = { static: 1 };\n",
8143            [7, 9],
8144        ),
8145    ];
8146
8147    #[cfg(any(feature = "javascript", feature = "mozjs"))]
8148    #[test]
8149    fn javascript_static_is_an_operator_1563() {
8150        const NAMES: &str = "obj.static;\nstatic();\nvar o = { static: 1 };\n\
8151                             class K { static static = 1; }\n";
8152        #[cfg(feature = "javascript")]
8153        {
8154            assert_operator_rows::<JavascriptParser>("foo.js", JS_STATIC_ROWS);
8155            let ops = ops_of::<JavascriptParser>(NAMES, "foo.js");
8156            assert!(ops.operands.iter().any(|o| o == "static"), "{ops:?}");
8157            assert!(ops.operators.iter().any(|o| o == "static"), "{ops:?}");
8158        }
8159        #[cfg(feature = "mozjs")]
8160        {
8161            assert_operator_rows::<MozjsParser>("foo.js", JS_STATIC_ROWS);
8162            let ops = ops_of::<MozjsParser>(NAMES, "foo.js");
8163            assert!(ops.operands.iter().any(|o| o == "static"), "{ops:?}");
8164            assert!(ops.operators.iter().any(|o| o == "static"), "{ops:?}");
8165        }
8166    }
8167
8168    #[cfg(feature = "bash")]
8169    #[test]
8170    fn bash_operators_and_operands() {
8171        check_metrics::<BashParser>(
8172            "#!/bin/bash
8173f() {
8174    local x=1
8175    if [ $x -eq 1 ]; then
8176        echo 'one'
8177    fi
8178}",
8179            "foo.sh",
8180            |metric| {
8181                // Operators (9 unique, 9 occurrences): the opening
8182                // delimiters `()`/`{}`/`[]` (each folded to one glyph and
8183                // counted once per balanced pair, #695 — the closers no
8184                // longer add a second operator), `local`, `=`, `if`,
8185                // `then`, `fi`, `;`.
8186                // Operands (6 unique, 7 occurrences): `f`, `x` (the
8187                // assignment LHS `variable_name`, kind 160), `1` (twice:
8188                // `=1` and `-eq 1`), `$x` (the `simple_expansion` — its
8189                // inner `variable_name` leaf is now suppressed so `$x`
8190                // counts once, #695), `echo`, `'one'`.
8191                // N2 was 8 before #1351: `echo` counted twice, once as the
8192                // `command_name` wrapper and once as the `word` it wraps.
8193                assert_eq!(metric.halstead.unique_operators(), 9);
8194                assert_eq!(metric.halstead.total_operators(), 9);
8195                assert_eq!(metric.halstead.unique_operands(), 6);
8196                assert_eq!(metric.halstead.total_operands(), 7);
8197                insta::assert_json_snapshot!(metric.halstead);
8198            },
8199        );
8200    }
8201
8202    #[cfg(feature = "bash")]
8203    #[test]
8204    fn bash_interpolated_string_no_double_count() {
8205        // Regression: issue #180. A double-quoted Bash string containing
8206        // `$name`, `${name[…]}`, or `$(cmd)` used to be classified as a
8207        // Halstead operand AND have its inner `simple_expansion` /
8208        // `expansion` / `command_substitution` children classified as
8209        // operands too. We now skip the wrapping literal when it has an
8210        // expansion child so only the inner expansion contributes.
8211        //
8212        // expected: operands across `a="plain"\nb="$x"\n` —
8213        //   line 1: variable_name `a`, plain string `"plain"` (no
8214        //     expansion, still operand) → 2.
8215        //   line 2: variable_name `b`, wrapping `"$x"` skipped (has
8216        //     expansion), `simple_expansion` `$x` (its inner
8217        //     variable_name `x` leaf is suppressed under #695) → 2.
8218        // Total unique operands: 4 (`a`, `b`, `"plain"`, `$x`), each
8219        // appearing once → N2 = 4. Before #695 the inner `x` leaf of
8220        // `$x` was also counted (u_operands = 5, N2 = 5); before the
8221        // earlier #180 fix the wrapping `"$x"` literal was counted too.
8222        // The `=` is the only operator; appears twice (N1 = 2, n1 = 1).
8223        check_metrics::<BashParser>("a=\"plain\"\nb=\"$x\"\n", "foo.sh", |metric| {
8224            assert_eq!(metric.halstead.unique_operators(), 1);
8225            assert_eq!(metric.halstead.total_operators(), 2);
8226            assert_eq!(metric.halstead.unique_operands(), 4);
8227            assert_eq!(metric.halstead.total_operands(), 4);
8228            insta::assert_json_snapshot!(metric.halstead);
8229        });
8230    }
8231
8232    #[cfg(feature = "elixir")]
8233    #[test]
8234    fn elixir_interpolated_string_no_double_count() {
8235        // Regression: issue #180. Without the fix, an interpolated
8236        // Elixir `String` was classified as a single operand while its
8237        // inner `interpolation` identifier was also walked and
8238        // classified as its own operand — double-counting the
8239        // interpolated identifier's contribution to `N2`.
8240        //
8241        // expected: operand contributions for
8242        //   `def greet(name) do\n  msg = "Hi #{name}"\nend\n` —
8243        // `def`, `greet`, `name` (param), `msg`, and the inner `name`
8244        // (inside `#{...}`). With the fix, the wrapping
8245        // `"Hi #{name}"` literal is skipped (has `Interpolation`
8246        // child), so `name` is the only repeated operand:
8247        // u_operands = 4 (def, greet, name, msg), N2 = 5. Without the
8248        // fix, the wrapping literal would also count → u_operands = 5,
8249        // N2 = 6. Operators: `do`, `end`, `(`, `=` → u = N = 4.
8250        // Only the *opening* delimiters count after #695, so the `)`
8251        // and the `}` interpolation closer add no operator; #1314 then
8252        // dropped the `#{` opener too, on the rule that an
8253        // interpolation opener is spelling rather than an operation
8254        // (was 5 here, and 7 before #695).
8255        check_metrics::<ElixirParser>(
8256            "def greet(name) do\n  msg = \"Hi #{name}\"\nend\n",
8257            "foo.ex",
8258            |metric| {
8259                assert_eq!(metric.halstead.unique_operators(), 4);
8260                assert_eq!(metric.halstead.total_operators(), 4);
8261                assert_eq!(metric.halstead.unique_operands(), 4);
8262                assert_eq!(metric.halstead.total_operands(), 5);
8263                insta::assert_json_snapshot!(metric.halstead);
8264            },
8265        );
8266    }
8267
8268    #[cfg(feature = "elixir")]
8269    #[test]
8270    fn elixir_plain_string_still_operand() {
8271        // The fix for #180 only skips wrapping literals that contain
8272        // interpolation; a plain `"hello"` must still contribute exactly
8273        // one operand. expected: `def`, `f`, `"hello"` → 3 unique
8274        // operands (n2 = 3), each appearing once (N2 = 3).
8275        check_metrics::<ElixirParser>("def f do\n  \"hello\"\nend\n", "foo.ex", |metric| {
8276            assert_eq!(metric.halstead.unique_operands(), 3);
8277            assert_eq!(metric.halstead.total_operands(), 3);
8278        });
8279    }
8280
8281    #[cfg(feature = "elixir")]
8282    #[test]
8283    fn elixir_boolean_and_nil_literals_count_once() {
8284        // Regression: issue #1253. `boolean: choice("true", "false")`
8285        // and `nil: "nil"` each wrap a keyword leaf, and both the
8286        // wrapper and the leaf sat in the operand arm — so every
8287        // literal occurrence added +1 to N2. Operands are keyed by
8288        // source text, so the duplicate collapsed into the same
8289        // vocabulary entry and n2 stayed correct, which is why nothing
8290        // caught it.
8291        //
8292        // Source is the issue's reproducer plus a repeat of `true` and
8293        // `nil`, so N2 exceeds n2 and the assertions can tell "counted
8294        // once per occurrence" from "deduplicated into the vocabulary".
8295        // All three keywords appear, so restoring any one of `True`,
8296        // `False`, or `Nil2` to the operand arm trips this test.
8297        //
8298        // Operands by text key: `x`, `y`, `z`, `w`, `v`, `true` × 2,
8299        // `nil` × 2, `false` ⇒ n2 = 8, N2 = 10. Before the fix each of
8300        // the five literals counted twice ⇒ N2 = 15.
8301        //
8302        // This also guards the drift in the other direction. Elixir
8303        // classifies the wrapper and drops the leaf outright rather
8304        // than parent-guarding it, so a grammar bump that stopped
8305        // emitting `boolean` / `nil` would leave the leaves unclassified
8306        // and the literals would vanish from N2 entirely (⇒ 5 / 5)
8307        // rather than merely being miscounted.
8308        check_metrics::<ElixirParser>(
8309            "x = true\ny = nil\nz = false\nw = true\nv = nil\n",
8310            "foo.ex",
8311            |metric| {
8312                assert_eq!(metric.halstead.unique_operands(), 8);
8313                assert_eq!(metric.halstead.total_operands(), 10);
8314            },
8315        );
8316    }
8317
8318    #[cfg(feature = "elixir")]
8319    #[test]
8320    fn elixir_reserved_word_after_a_dot_stays_an_operand() {
8321        // Companion to the test above (#1253). Elixir drops `True` /
8322        // `False` / `Nil2` from the operand arm outright, which is only
8323        // safe because the one grammar position that accepts a reserved
8324        // word outside the `boolean` / `nil` wrapper — the right-hand
8325        // side of a remote dot — aliases it to `identifier`. This pins
8326        // that alias: if a grammar bump emitted the bare keyword there
8327        // instead, `Foo.nil` and `Foo.true` would silently stop
8328        // contributing an operand.
8329        //
8330        // Source: a = Foo.nil / b = Foo.true / c = nil
8331        //
8332        // Operands by text key: `a`, `Foo` × 2, `nil` × 2 (the aliased
8333        // identifier and the real literal, which share a text key),
8334        // `b`, `true`, `c` ⇒ n2 = 6, N2 = 8. Losing the alias drops the
8335        // two dotted references ⇒ N2 = 6.
8336        check_metrics::<ElixirParser>("a = Foo.nil\nb = Foo.true\nc = nil\n", "foo.ex", |metric| {
8337            assert_eq!(metric.halstead.unique_operands(), 6);
8338            assert_eq!(metric.halstead.total_operands(), 8);
8339        });
8340    }
8341
8342    #[cfg(feature = "elixir")]
8343    #[test]
8344    fn elixir_interpolated_sigil_no_double_count() {
8345        // Sigils mirror strings under #180. For `~r/foo#{name}/`, the
8346        // wrapping `Sigil` is skipped, but `SigilName` (`r`) and the
8347        // inner `name` identifier each contribute one operand.
8348        // expected: `def`, `f`, `name` (param), `re`, `r` (sigil name),
8349        // `name` (inside `#{...}`) → u_operands = 5, N2 = 6 (`name`
8350        // twice).
8351        check_metrics::<ElixirParser>(
8352            "def f(name) do\n  re = ~r/foo#{name}/\nend\n",
8353            "foo.ex",
8354            |metric| {
8355                assert_eq!(metric.halstead.unique_operands(), 5);
8356                assert_eq!(metric.halstead.total_operands(), 6);
8357            },
8358        );
8359    }
8360
8361    #[cfg(feature = "elixir")]
8362    #[test]
8363    fn elixir_interpolated_charlist_no_double_count() {
8364        // Charlists mirror strings and sigils under #180. The
8365        // `E::String | E::Charlist | E::Sigil` arm in `get_op_type`
8366        // skips any wrapping literal that has an `Interpolation`
8367        // child; this test exercises the `Charlist` branch
8368        // specifically.
8369        //
8370        // expected: for `def f(name) do\n  cl = 'Hi #{name}'\nend\n` —
8371        // `def`, `f`, `name` (param), `cl`, and the inner `name`
8372        // (inside `#{...}`). With the fix, the wrapping
8373        // `'Hi #{name}'` is skipped → u_operands = 4 (def, f, name,
8374        // cl), N2 = 5 (`name` twice).
8375        check_metrics::<ElixirParser>(
8376            "def f(name) do\n  cl = 'Hi #{name}'\nend\n",
8377            "foo.ex",
8378            |metric| {
8379                assert_eq!(metric.halstead.unique_operands(), 4);
8380                assert_eq!(metric.halstead.total_operands(), 5);
8381            },
8382        );
8383    }
8384
8385    #[cfg(feature = "elixir")]
8386    #[test]
8387    fn elixir_sigil_delimiters_are_not_operators() {
8388        // Regression: issue #1256. Sigil delimiter tokens share their
8389        // kind ids with real operators (`SLASH`, `LPAREN`, `LBRACE`,
8390        // …) and were classified unconditionally, so `~r/abc/`
8391        // fabricated two division operators and the author's delimiter
8392        // choice moved n1/N1. The parent guard suppresses them under
8393        // `Sigil`; `~` stays the single per-sigil operator.
8394        //
8395        // expected: operators `=` × 3 and `~` × 3 → n1 = 2, N1 = 6.
8396        // Without the guard the delimiters added `/` × 2, `(`, `{` →
8397        // n1 = 5, N1 = 10. Operands: `a`, `~r/abc/i`, `r`, `i` (sigil
8398        // modifiers), `b`, `~w(one two)`, `w`, `c`, `~s{hi}`, `s` →
8399        // n2 = N2 = 10.
8400        check_metrics::<ElixirParser>(
8401            "a = ~r/abc/i\nb = ~w(one two)\nc = ~s{hi}\n",
8402            "foo.ex",
8403            |metric| {
8404                assert_eq!(metric.halstead.unique_operators(), 2);
8405                assert_eq!(metric.halstead.total_operators(), 6);
8406                assert_eq!(metric.halstead.unique_operands(), 10);
8407                assert_eq!(metric.halstead.total_operands(), 10);
8408            },
8409        );
8410    }
8411
8412    #[cfg(feature = "elixir")]
8413    #[test]
8414    fn elixir_sigil_delimiter_choice_is_invariant() {
8415        // Companion to the test above (#1256): two sigils differing
8416        // only in delimiter are the same literal, so every delimiter
8417        // choice must produce identical Halstead counts. `(` `[` `{`
8418        // `<` `/` `|` are the operator-kind delimiters the guard
8419        // covers; `"` and `'` never had an operator arm and pin the
8420        // already-correct path.
8421        //
8422        // expected per variant: operators `=`, `~` → n1 = 2, N1 = 2;
8423        // operands `x`, the sigil literal text, `w` (sigil name) →
8424        // n2 = 3, N2 = 3.
8425        for (open, close) in [
8426            ("(", ")"),
8427            ("[", "]"),
8428            ("{", "}"),
8429            ("<", ">"),
8430            ("/", "/"),
8431            ("|", "|"),
8432            ("\"", "\""),
8433            ("'", "'"),
8434        ] {
8435            assert_halstead_counts::<ElixirParser>(
8436                &format!("x = ~w{open}one two{close}\n"),
8437                "foo.ex",
8438                [2, 2, 3, 3],
8439                &format!("delimiter pair {open} {close}"),
8440            );
8441        }
8442    }
8443
8444    #[cfg(feature = "elixir")]
8445    #[test]
8446    fn elixir_standalone_operators_survive_the_sigil_guard() {
8447        // Control for #1256: the guard is parent-scoped, so the same
8448        // token kinds outside a sigil still count. Covers every guarded
8449        // kind standalone: `/` (division), `<` / `>` (comparison), `[`
8450        // and `|` (list cons), `(` (call), `{` (map literal, with its
8451        // `%`).
8452        //
8453        // expected: operators `=` × 6, `/`, `<`, `>`, `[`, `|`, `(`,
8454        // `%`, `{` → n1 = 9, N1 = 14. Operands: `x`, `a`, `b`, `y`,
8455        // `c`, `d`, `z`, `e`, `f`, `q`, `h`, `t`, `p`, `g`, `1`, `m`,
8456        // the `k:` keyword, `2` → n2 = N2 = 18.
8457        check_metrics::<ElixirParser>(
8458            "x = a / b\ny = c < d\nz = e > f\nq = [h | t]\np = g(1)\nm = %{k: 2}\n",
8459            "foo.ex",
8460            |metric| {
8461                assert_eq!(metric.halstead.unique_operators(), 9);
8462                assert_eq!(metric.halstead.total_operators(), 14);
8463                assert_eq!(metric.halstead.unique_operands(), 18);
8464                assert_eq!(metric.halstead.total_operands(), 18);
8465            },
8466        );
8467    }
8468
8469    #[cfg(feature = "elixir")]
8470    #[test]
8471    fn elixir_interpolated_sigil_keeps_inner_nodes_counting() {
8472        // Interpolation inside a sigil after #1256: the `{` delimiter
8473        // is suppressed (its parent is the `Sigil`), while the
8474        // `interpolation` child is a separate node whose inner
8475        // identifier must still count — the guard must not reach past
8476        // the delimiter tokens.
8477        //
8478        // expected: operators `=`, `~` → n1 = N1 = 2. Operands:
8479        // `v`, `s` (sigil name), `b` (interpolated identifier); the
8480        // wrapping sigil is skipped (`Interpolation` child, #180) and
8481        // `quoted_content` is unclassified → n2 = N2 = 3. The `#{`
8482        // marker was a third operator until #1314 dropped it.
8483        check_metrics::<ElixirParser>("v = ~s{a#{b} c}\n", "foo.ex", |metric| {
8484            assert_eq!(metric.halstead.unique_operators(), 2);
8485            assert_eq!(metric.halstead.total_operators(), 2);
8486            assert_eq!(metric.halstead.unique_operands(), 3);
8487            assert_eq!(metric.halstead.total_operands(), 3);
8488        });
8489
8490        // A guarded kind *inside* the interpolation: the `/` in
8491        // `#{a / b}` has `binary_operator` as its parent but the
8492        // `Sigil` as a further ancestor, so this input is the one
8493        // discriminator between the correct parent-scoped guard and a
8494        // wrong ancestor-scoped one that would swallow it.
8495        //
8496        // expected: operators `=`, `~`, `/` → n1 = N1 = 3 (the `#{`
8497        // opener stopped counting with #1314); operands `v`, `s`, `a`,
8498        // `b` → n2 = N2 = 4. The division is what this row is for, and
8499        // it still counts — the ancestor-scoped mutant drops it.
8500        check_metrics::<ElixirParser>("v = ~s{x #{a / b} y}\n", "foo.ex", |metric| {
8501            assert_eq!(metric.halstead.unique_operators(), 3);
8502            assert_eq!(metric.halstead.total_operators(), 3);
8503            assert_eq!(metric.halstead.unique_operands(), 4);
8504            assert_eq!(metric.halstead.total_operands(), 4);
8505        });
8506    }
8507
8508    #[cfg(feature = "bash")]
8509    #[test]
8510    fn bash_all_expansion_kinds_skip_wrapper() {
8511        // Exercises every node kind tested by
8512        // `bash_string_has_expansion`: `simple_expansion` (`$v`),
8513        // `expansion` (`${v[0]}`), `command_substitution` (`$(date)`),
8514        // and `arithmetic_expansion` (`$((1+2))`). A typo replacing
8515        // one kind with an aliased neighbour in `language_bash.rs`
8516        // (e.g., `ExpansionBody` instead of `Expansion`) would leave
8517        // the corresponding wrapping string counted as an operand and
8518        // shift the totals.
8519        //
8520        // expected: operands across the four lines —
8521        //   line 1 `a="$v"`: var_name `a`, simple_expansion `$v` (its
8522        //     inner var_name `v` leaf is suppressed under #695; wrapper
8523        //     skipped) → 2
8524        //   line 2 `b="${v[0]}"`: var_name `b`, var_name `v` (inside
8525        //     subscript — parent is `expansion`, not `simple_expansion`,
8526        //     so it still counts), number `0` (wrapper skipped,
8527        //     `expansion` itself is not in the operand list) → 3
8528        //   line 3 `c="$(date)"`: var_name `c`, the `word` `date` under
8529        //     the `command_name` (wrapper skipped, `command_substitution`
8530        //     not in operand list, and since #1351 the `command_name`
8531        //     wrapper is not either) → 2
8532        //   line 4 `d="$((1+2))"`: var_name `d`, numbers `1` and `2`
8533        //     (wrapper skipped, `arithmetic_expansion` not in operand
8534        //     list) → 3
8535        // Unique operands: a, b, c, d, $v, v, 0, date, 1, 2 → 10. Total
8536        // occurrences: 10 (`v` appears once — only line 2's subscript
8537        // leaf; line 1's `$v` inner leaf is suppressed — and `date` once,
8538        // as the `word`; before #1351 the `command_name` wrapping it
8539        // added a second `date` and N2 was 11). Operators after
8540        // #695: only the openers `[` (folded `[]`) and `+`, plus `=` four
8541        // times — the `}`/`)`/`))`/`]` closers no longer count.
8542        check_metrics::<BashParser>(
8543            "a=\"$v\"\nb=\"${v[0]}\"\nc=\"$(date)\"\nd=\"$((1+2))\"\n",
8544            "foo.sh",
8545            |metric| {
8546                assert_eq!(metric.halstead.unique_operators(), 3);
8547                assert_eq!(metric.halstead.total_operators(), 6);
8548                assert_eq!(metric.halstead.unique_operands(), 10);
8549                assert_eq!(metric.halstead.total_operands(), 10);
8550            },
8551        );
8552    }
8553
8554    /// Regression for #695. A bare `$x` (outside any string) parses as a
8555    /// `simple_expansion` wrapping a `variable_name` leaf — and `$?` / `$1`
8556    /// as a `simple_expansion` wrapping a `special_variable_name` leaf. Both
8557    /// the wrapper and the inner leaf used to be classified as operands, so
8558    /// each bare variable reference double-counted (the same hazard Tcl
8559    /// guards with its `Id2` exclusion and iRules with a parent check). The
8560    /// `variable_name` / `special_variable_name` arm now yields `Unknown`
8561    /// when its parent is a `simple_expansion`, so `$x` contributes exactly
8562    /// one operand while the assignment LHS `variable_name` (`x` in `x=…`,
8563    /// parent is `variable_assignment`) still counts.
8564    #[cfg(feature = "bash")]
8565    #[test]
8566    fn bash_bare_variable_no_double_count() {
8567        let source = "x=1\necho $x\necho $?\n";
8568        let path = PathBuf::from("foo.sh");
8569        let parser = BashParser::new(source.as_bytes().to_vec(), &path, None);
8570        let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
8571        let bare_x = ops.operands.iter().filter(|o| o.as_str() == "$x").count();
8572        let special = ops.operands.iter().filter(|o| o.as_str() == "$?").count();
8573        // Each bare reference is exactly one operand; the inner leaf is not
8574        // double-counted. If the guard regressed, the inner `variable_name`
8575        // `x` would add a second `x` occurrence (text-colliding with the
8576        // assignment LHS) and the inner `special_variable_name` `?` would
8577        // appear as a standalone `?` operand.
8578        assert_eq!(
8579            bare_x, 1,
8580            "bare $x must be one operand; operands were {:?}",
8581            ops.operands
8582        );
8583        assert_eq!(
8584            special, 1,
8585            "bare $? must be one operand; operands were {:?}",
8586            ops.operands
8587        );
8588        assert!(
8589            !ops.operands.iter().any(|o| o.as_str() == "?"),
8590            "the inner special_variable_name `?` leaf must be suppressed; operands were {:?}",
8591            ops.operands
8592        );
8593        // The assignment LHS `variable_name` `x` (parent `variable_assignment`,
8594        // not `simple_expansion`) must still be an operand.
8595        assert!(
8596            ops.operands.iter().any(|o| o.as_str() == "x"),
8597            "assignment LHS `x` must still be an operand; operands were {:?}",
8598            ops.operands
8599        );
8600    }
8601
8602    /// Drives one row of a Bash wrapper/leaf double-count table: a grammar
8603    /// wrapper whose entire contribution to `N2` was its single,
8604    /// already-counted child (`command_name`, #1351; `translated_string`,
8605    /// #1358).
8606    ///
8607    /// `expected` is the post-fix `[n1, N1, n2, N2]`. `(n2_before,
8608    /// N2_before)` is what the same source scored while `wrapper` was still
8609    /// classified as an operand. Both columns are checked against the
8610    /// *current* parse under the model "one spurious operand per wrapper
8611    /// node" — they are not measured against the old code, which no longer
8612    /// exists to run:
8613    ///
8614    /// - `N2_before - N2` must equal the number of `wrapper` nodes. That
8615    ///   identity *is* the defect: one spurious operand per wrapper.
8616    /// - `n2_before` must equal the post-fix operand vocabulary unioned
8617    ///   with the wrapper spellings. It exceeds `n2` wherever the wrapper's
8618    ///   whole text is not already an operand in its own right — either
8619    ///   because it differs from its single child's (`"$cmd"`, `${cmd}`,
8620    ///   `$"ls"`) or because it spans several (`foo$x`, `$(which ls)`,
8621    ///   `{1..3}`).
8622    ///
8623    /// So a mistyped or stale column fails rather than misinforming the next
8624    /// reader — one did, during review of #1351 — but neither check can
8625    /// catch a *production* regression, because both hold whatever
8626    /// `get_op_type` answers. `assert_halstead_counts` below is the only
8627    /// assertion here that fails when the wrapper arm comes back; measured
8628    /// by neutralising it under the pre-fix arm, which leaves both tables
8629    /// green.
8630    #[cfg(feature = "bash")]
8631    #[track_caller]
8632    fn assert_bash_wrapper_sheds_one_operand(
8633        source: &str,
8634        expected: [u64; 4],
8635        (n2_before, total_before): (u64, u64),
8636        wrapper: Bash,
8637    ) {
8638        let name: &'static str = wrapper.clone().into();
8639        let kind = wrapper as u16;
8640        let code = source.as_bytes();
8641        let parser = BashParser::new(code.to_vec(), &PathBuf::from("foo.sh"), None);
8642        let spellings: Vec<&str> = parser
8643            .root()
8644            .preorder()
8645            .filter(|node| node.kind_id() == kind)
8646            .filter_map(|node| node.utf8_text(code))
8647            .collect();
8648        assert!(
8649            !spellings.is_empty(),
8650            "row {source:?} parses without a {name}, so it witnesses nothing",
8651        );
8652        // Phrased as an addition rather than `total_before - expected[3]`: a
8653        // future edit that inverts the two would underflow `u64` and panic
8654        // with a raw overflow message instead of the one below.
8655        assert_eq!(
8656            total_before,
8657            expected[3] + spellings.len() as u64,
8658            "row {source:?} must shed exactly one operand per {name}; \
8659             recorded N2_before {total_before}",
8660        );
8661
8662        let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
8663        let mut vocabulary: HashSet<&str> = ops.operands.iter().map(String::as_str).collect();
8664        vocabulary.extend(spellings);
8665        assert_eq!(
8666            vocabulary.len() as u64,
8667            n2_before,
8668            "row {source:?}: n2 before the fix is the post-fix vocabulary \
8669             plus the {name} spellings; got {vocabulary:?}",
8670        );
8671
8672        assert_halstead_counts::<BashParser>(source, "foo.sh", expected, source);
8673    }
8674
8675    /// Regression for #1351, the command-name sibling of #695's bare
8676    /// `$x`. `command_name` is a pure wrapper: the grammar gives it
8677    /// exactly one required child (a `_primary_expression` or a
8678    /// `concatenation`) and it adds no text of its own, so classifying it
8679    /// as an operand *and* letting the walk reach the child counted every
8680    /// command name twice in `N2`.
8681    ///
8682    /// The table is the grammar-dispatch §6 evidence that deleting the arm
8683    /// zeroes nothing: it names every kind `command_name` can wrap, and in
8684    /// every row the command name still contributes at least one operand
8685    /// once the trailing `arg` is discounted.
8686    ///
8687    /// `n2_before` / `N2_before` are what each row scores with the
8688    /// `command_name` arm restored *against today's tree*, not a historical
8689    /// measurement — the `$"ls" arg` row has since lost a
8690    /// `translated_string` wrapper to #1358 as well.
8691    /// `assert_bash_wrapper_sheds_one_operand` re-derives both from the
8692    /// current parse rather than trusting them.
8693    #[cfg(feature = "bash")]
8694    #[test]
8695    fn bash_command_name_wrapper_no_double_count() {
8696        // (source, [n1, N1, n2, N2], (n2_before, N2_before))
8697        let cases: [(&str, [u64; 4], (u64, u64)); 14] = [
8698            // word
8699            ("ls bar\n", [0, 0, 2, 2], (2, 3)),
8700            // number
8701            ("1 arg\n", [0, 0, 2, 2], (2, 3)),
8702            // string, inert
8703            ("\"ls\" arg\n", [0, 0, 2, 2], (2, 3)),
8704            // string wrapping an expansion: the wrapper string is already
8705            // skipped (#180), so before #1351 the `command_name` was the
8706            // only thing counting the quoted spelling — which also planted
8707            // a spurious `"$cmd"` entry in n2 beside `$cmd`.
8708            ("\"$cmd\" arg\n", [0, 0, 2, 2], (3, 3)),
8709            // raw_string
8710            ("'ls' arg\n", [0, 0, 2, 2], (2, 3)),
8711            // ansi_c_string
8712            ("$'ls' arg\n", [0, 0, 2, 2], (2, 3)),
8713            // translated_string. Two wrappers stacked here — `command_name`
8714            // over `translated_string` — so the pre-#1351 column below is
8715            // measured against a tree #1358 has already thinned to
8716            // `"ls"` + `arg`.
8717            ("$\"ls\" arg\n", [0, 0, 2, 2], (3, 3)),
8718            // simple_expansion
8719            ("$cmd arg\n", [0, 0, 2, 2], (2, 3)),
8720            // brace expansion: counts through its inner `variable_name`,
8721            // whose `SimpleExpansion` parent guard does not apply here.
8722            ("${cmd} arg\n", [0, 0, 2, 2], (3, 3)),
8723            // command_substitution: counts through the nested command.
8724            // Two command names here — the outer one and `which`.
8725            ("$(which ls) arg\n", [0, 0, 3, 3], (4, 5)),
8726            // process_substitution, likewise two command names.
8727            ("<(ls) arg\n", [0, 0, 2, 2], (3, 4)),
8728            // arithmetic_expansion
8729            ("$((1+1)) arg\n", [1, 1, 2, 3], (3, 4)),
8730            // brace_expression
8731            ("{1..3} arg\n", [1, 1, 3, 3], (4, 4)),
8732            // concatenation
8733            ("foo$x arg\n", [0, 0, 3, 3], (4, 4)),
8734        ];
8735        for (source, expected, before) in cases {
8736            assert!(
8737                expected[3] > 1,
8738                "row {source:?} must leave the command name at least one \
8739                 operand beside `arg`; a zero here means the deleted arm \
8740                 was load-bearing for this spelling",
8741            );
8742            assert_bash_wrapper_sheds_one_operand(source, expected, before, Bash::CommandName);
8743        }
8744    }
8745
8746    /// Regression for #1358, the sibling of #1351 in the same `match`.
8747    /// `translated_string: $ => seq('$', $.string)` carries exactly one
8748    /// required `string` child and no text of its own beyond the `$`, so
8749    /// classifying the wrapper *and* letting the walk reach the child
8750    /// counted every `$"…"` twice in `N2`.
8751    ///
8752    /// The rows assert the total, `N2`. `n2` alone cannot witness this
8753    /// double count: the wrapper and its child spell *different* text
8754    /// (`$"x"` against `"x"`), so the pre-fix tree split one operand into
8755    /// two vocabulary entries rather than repeating one — which is why the
8756    /// `n2_before` column is carried too, and checked against the wrapper
8757    /// spellings the row actually parses to.
8758    #[cfg(feature = "bash")]
8759    #[test]
8760    fn bash_translated_string_wrapper_no_double_count() {
8761        // (source, [n1, N1, n2, N2], (n2_before, N2_before))
8762        let cases: [(&str, [u64; 4], (u64, u64)); 7] = [
8763            // Assignment RHS.
8764            ("a=$\"x\"\n", [1, 1, 2, 2], (3, 3)),
8765            // Empty translated string. The grammar makes the inner `string`
8766            // required, so the wrapper is never childless — the condition
8767            // grammar-dispatch §6 warns that deletion would regress.
8768            ("a=$\"\"\n", [1, 1, 2, 2], (3, 3)),
8769            // The one spelling the deleted arm *was* load-bearing for: the
8770            // inner `string` wraps an operandless brace `expansion`, so the
8771            // whole `$"…"` now scores zero. That is #1351's residue one
8772            // level down rather than a new gap — a plain `a="${#}"` already
8773            // scored `[1, 1, 1, 1]` — and
8774            // `bash_translated_string_scores_alike_in_both_positions` pins
8775            // that both positions agree on it.
8776            ("a=$\"${#}\"\n", [1, 1, 1, 1], (2, 2)),
8777            // `${!}` is the same residue plus a `!`, which the operator arm
8778            // counts; it must not turn into an operand along the way.
8779            ("a=$\"${!}\"\n", [2, 2, 1, 1], (2, 2)),
8780            // `case` subject.
8781            ("case $\"y\" in *) ;; esac\n", [4, 4, 1, 1], (2, 2)),
8782            // Command-name position, where #1351's wrapper stacks on top of
8783            // this one; the `_before` columns here price the
8784            // `translated_string` alone.
8785            ("$\"ls\" arg\n", [0, 0, 2, 2], (3, 3)),
8786            // Interpolating form. `bash_string_has_expansion` inspects the
8787            // node's own children, and a `translated_string` has only `$`
8788            // and `string`, so it never saw the `simple_expansion` one level
8789            // down: the wrapper reintroduced exactly the double count #180
8790            // removed, for the `$"…"` spelling alone. Only `$y` and `b` may
8791            // survive.
8792            ("b=$\"$y\"\n", [1, 1, 2, 2], (3, 3)),
8793        ];
8794        for (source, expected, before) in cases {
8795            assert_bash_wrapper_sheds_one_operand(source, expected, before, Bash::TranslatedString);
8796        }
8797    }
8798
8799    /// The load-bearing half of #1358's grammar-dispatch §6 argument, which
8800    /// nothing else asserts. In ordinary argument position the grammar emits
8801    /// no `translated_string` at all — just a `$` token and a `string` — so
8802    /// the `string` is the node present for *every* spelling, and dropping
8803    /// the wrapper makes the wrapper-bearing positions agree with argument
8804    /// position rather than newly disagree. If a grammar bump starts
8805    /// emitting the wrapper here, the deletion has to be re-derived.
8806    #[cfg(feature = "bash")]
8807    #[test]
8808    fn bash_translated_string_scores_alike_in_both_positions() {
8809        // `${#}` carries the residue: it contributes no operand of its own,
8810        // so the second row pins that the deletion left *both* positions at
8811        // zero rather than only the wrapper-bearing one.
8812        //
8813        // Only the command-name half moves under the pre-fix arm; the
8814        // argument-position half is the reference the parity is measured
8815        // against, and `cmd $"…"` is asserted nowhere else. Measured — with
8816        // the argument assertions dropped this test still fails pre-fix,
8817        // with the command-name ones dropped it passes — so do not
8818        // "simplify" it down to the failing half.
8819        for (spelling, expected) in [("$\"hi\"", [0, 0, 2, 2]), ("$\"${#}\"", [0, 0, 1, 1])] {
8820            let as_command_name = format!("{spelling} arg\n");
8821            let as_argument = format!("cmd {spelling}\n");
8822            let parse = |source: &str| {
8823                BashParser::new(source.as_bytes().to_vec(), &PathBuf::from("foo.sh"), None)
8824            };
8825            assert!(
8826                !ast_has_kind_id(&parse(&as_argument), Bash::TranslatedString as u16),
8827                "argument position must still parse {spelling} as a bare `$` \
8828                 plus a `string`; a grammar bump that emits the wrapper here \
8829                 invalidates the parity this test rests on",
8830            );
8831            assert!(
8832                ast_has_kind_id(&parse(&as_command_name), Bash::TranslatedString as u16),
8833                "command-name position must still emit the wrapper, or the \
8834                 pair witnesses nothing",
8835            );
8836            assert_halstead_counts::<BashParser>(
8837                &as_command_name,
8838                "foo.sh",
8839                expected,
8840                &as_command_name,
8841            );
8842            assert_halstead_counts::<BashParser>(&as_argument, "foo.sh", expected, &as_argument);
8843        }
8844    }
8845
8846    /// Pins the one residue of #1351: a brace `expansion` with no
8847    /// `variable_name` leaf. `${#}` (positional-parameter count) and `${!}`
8848    /// (last background PID) hold only anonymous tokens, and
8849    /// `Bash::Expansion` is not an operand, so the whole expansion
8850    /// contributes nothing.
8851    ///
8852    /// That was already true in *argument* position before the fix, which
8853    /// is the reason grammar-dispatch §6's gate-don't-delete rule did not
8854    /// apply to the `command_name` arm: deleting it made command-name
8855    /// position agree with argument position rather than newly disagree.
8856    /// The parity is the load-bearing half of that argument and nothing
8857    /// else asserts it, so if a future arm starts classifying these the
8858    /// two positions have to move together.
8859    #[cfg(feature = "bash")]
8860    #[test]
8861    fn bash_operandless_expansion_scores_alike_in_both_positions() {
8862        // `${!}` carries a `!`, which the operator arm counts; `${#}`'s `#`
8863        // is not an operator. Both positions must agree per spelling.
8864        for (spelling, expected) in [("${#}", [0, 0, 1, 1]), ("${!}", [1, 1, 1, 1])] {
8865            let as_command_name = format!("{spelling} arg\n");
8866            let as_argument = format!("cmd {spelling}\n");
8867            assert_halstead_counts::<BashParser>(
8868                &as_command_name,
8869                "foo.sh",
8870                expected,
8871                &as_command_name,
8872            );
8873            assert_halstead_counts::<BashParser>(&as_argument, "foo.sh", expected, &as_argument);
8874        }
8875    }
8876
8877    /// Drift marker for #1351 (lesson 34 / grammar-dispatch §2). `_concat`
8878    /// (`Bash::Concat`) is a hidden zero-width external token the scanner
8879    /// emits between `concatenation` parts; the parser never surfaces it as
8880    /// a node, and it spells no operand, so `BashCode::get_op_type` lists
8881    /// neither it nor the visible `concatenation` wrapper. If a grammar
8882    /// bump starts emitting it, this fails and the classification must be
8883    /// re-derived rather than assumed still absent.
8884    ///
8885    /// Measured, not assumed: putting `Bash::Concat` back in the operand
8886    /// arm fails no test in the suite, because the token is unreachable.
8887    /// Unreachability is the only coverage such an arm can have, which is
8888    /// why this test asserts it directly instead of asserting a count.
8889    #[cfg(feature = "bash")]
8890    #[test]
8891    fn bash_hidden_concat_token_is_unreachable() {
8892        let source = "a=foo$x\nb=pre\"$y\"post\ncmd bar$z\n";
8893        let path = PathBuf::from("foo.sh");
8894        let parser = BashParser::new(source.as_bytes().to_vec(), &path, None);
8895        // Non-vacuity: the visible `concatenation` this source is written
8896        // to produce must actually be in the parse, so the negative below
8897        // is about `_concat` and not about a source that concatenates
8898        // nothing.
8899        assert!(
8900            ast_has_kind_id(&parser, Bash::Concatenation as u16),
8901            "expected a visible `concatenation` node in the parse",
8902        );
8903        assert!(
8904            !ast_has_kind_id(&parser, Bash::Concat as u16),
8905            "the hidden `_concat` token surfaced; re-derive its \
8906             classification in BashCode::get_op_type against the new grammar",
8907        );
8908    }
8909
8910    #[cfg(feature = "tcl")]
8911    #[test]
8912    fn tcl_operators_and_operands() {
8913        check_metrics::<TclParser>(
8914            "proc f {a b} {
8915    set x [expr {$a + $b}]
8916    if {$x > 0 && $x != 0} {
8917        return $x
8918    }
8919    return 0
8920}",
8921            "foo.tcl",
8922            |metric| {
8923                // Anchored per the snapshot policy in AGENTS.md, which
8924                // this call predates. Operators `proc`, `set`, `[]`,
8925                // `{}`, `expr`, `+`, `if`, `>`, `&&`, `!=` → n1 = 10,
8926                // N1 = 14 (`{}` × 4 for the proc parameter list, the
8927                // proc body, the two `expr`/`if` conditions and the
8928                // `if` body — the `expr` braces are an `Expr`, the
8929                // bodies a `BracedWord`). Operands `f`, `a`, `b`, `x`,
8930                // `$a`, `$b`, `$x`, `0` and `return` → n2 = 9,
8931                // N2 = 14. Before #1354 the proc body and the `if`
8932                // body were operands too → 11 / 16.
8933                assert_eq!(metric.halstead.unique_operators(), 10);
8934                assert_eq!(metric.halstead.total_operators(), 14);
8935                assert_eq!(metric.halstead.unique_operands(), 9);
8936                assert_eq!(metric.halstead.total_operands(), 14);
8937                insta::assert_json_snapshot!(metric.halstead);
8938            },
8939        );
8940    }
8941
8942    #[cfg(feature = "tcl")]
8943    #[test]
8944    fn tcl_bitwise_ternary_string_ops() {
8945        // Exercises operator families not covered by tcl_operators_and_operands:
8946        // bitwise (&, |, ^, ~, <<, >>), ternary (?), and string-comparison (eq, ne, in, ni).
8947        check_metrics::<TclParser>(
8948            "proc f {a b} {
8949    set bits [expr {$a & $b | $a ^ ~$b}]
8950    set sh [expr {$a << 1 | $b >> 1}]
8951    set t [expr {$a > 0 ? $a : $b}]
8952    if {$a eq {x} || $a ne {y}} {
8953        return $a
8954    }
8955    return $b
8956}",
8957            "foo.tcl",
8958            |metric| {
8959                // Anchored per the snapshot policy in AGENTS.md, which
8960                // this call predates. N1 fell 33 → 31 with #1314: the
8961                // `if` condition's `{x}` and `{y}` are braced *words*,
8962                // so their openers stopped fabricating a `{}` operator.
8963                // n1 is unchanged at 18 because the `{}` entry survives
8964                // on the proc body and the `expr` braces — which is
8965                // exactly why the fabrication was invisible in n1 and
8966                // is the reason to assert N1 as well (#1294).
8967                //
8968                // The operand columns fell 17 / 30 → 13 / 26 with
8969                // #1354, and the operator columns did not move: the two
8970                // script bodies (the proc's and the `if`'s) stopped
8971                // being operands, and the `x` / `y` inside the braced
8972                // words `{x}` and `{y}` are now part of the one operand
8973                // each word contributes.
8974                assert_eq!(metric.halstead.unique_operators(), 18);
8975                assert_eq!(metric.halstead.total_operators(), 31);
8976                assert_eq!(metric.halstead.unique_operands(), 13);
8977                assert_eq!(metric.halstead.total_operands(), 26);
8978                insta::assert_json_snapshot!(metric.halstead);
8979            },
8980        );
8981    }
8982
8983    #[cfg(feature = "tcl")]
8984    #[test]
8985    fn tcl_array_reference_bills_the_reference_and_the_index() {
8986        // `$arr($i)` is the reference plus the index Tcl substitutes
8987        // inside the parens, and `arr(k)` as a `set` target is the name
8988        // plus the literal index; the `array_index` wrapper is neither.
8989        // The quoted spelling is deliberate — the vendored grammar
8990        // mis-parses a bare `$arr(k)` in command-word position. Pinned
8991        // per dialect and as a parity in `tests/parity/`, because the
8992        // iRules twin listed the wrapper as a third operand.
8993        assert_ops_operands::<TclParser>(
8994            "set arr(k) 1\nset z \"$arr($i)\"\n",
8995            "foo.tcl",
8996            6,
8997            vec!["arr", "k", "1", "z", "$arr($i)", "$i"],
8998        );
8999    }
9000
9001    #[cfg(feature = "tcl")]
9002    #[test]
9003    fn tcl_bare_variable_operand() {
9004        // Bare `$varname` produces a VariableSubstitution node (already an operand).
9005        // Its anonymous Id2 child must NOT be counted separately; each reference is 1 operand.
9006        check_metrics::<TclParser>(
9007            "proc f {x} {
9008    return $x
9009}",
9010            "foo.tcl",
9011            |metric| {
9012                // Anchored per the snapshot policy in AGENTS.md, which
9013                // this call predates. Operators `proc` and `{}` × 2
9014                // (the parameter list and the body) → n1 = 2, N1 = 3.
9015                // Operands `f`, the parameter `x`, `return` and `$x` —
9016                // one occurrence each, so a re-counted `x` leaf inside
9017                // `$x` would show up in N2 even though it collides with
9018                // the parameter in n2. Before #1354 the proc body was a
9019                // fifth operand.
9020                assert_eq!(metric.halstead.unique_operators(), 2);
9021                assert_eq!(metric.halstead.total_operators(), 3);
9022                assert_eq!(metric.halstead.unique_operands(), 4);
9023                assert_eq!(metric.halstead.total_operands(), 4);
9024                insta::assert_json_snapshot!(metric.halstead);
9025            },
9026        );
9027    }
9028
9029    #[cfg(feature = "tcl")]
9030    #[test]
9031    fn tcl_inert_quoted_word_counts_as_operand() {
9032        // Regression for #277. A `"..."` literal with no `$var` / `[cmd]`
9033        // interpolation must contribute exactly one operand (the wrapping
9034        // `QuotedWord`). The string content `hello world` is exposed as a
9035        // single `_quoted_word_content` token (not itself classified by
9036        // `get_op_type`), so the only operands here are `f`, `s`, and the
9037        // quoted string. `set` is the anonymous `Set2` keyword and is
9038        // classified as an operator, not an operand.
9039        check_metrics::<TclParser>(
9040            "proc f {} {
9041    set s \"hello world\"
9042}",
9043            "foo.tcl",
9044            |metric| {
9045                // Operands: `f`, the `set` target `s`, `"hello world"` —
9046                // 3 unique, 3 total. Before #1294 this read 3/3 for a
9047                // different reason, with `s` missing and the proc-body
9048                // `braced_word` making the count coincidentally
9049                // plausible; #1354 removed that body operand, so the
9050                // three named here are now the whole list. The wrapping
9051                // `QuotedWord` must still contribute exactly one operand
9052                // when it carries no interpolation children; dropping to 2
9053                // would mean the inert case was over-guarded.
9054                assert_eq!(metric.halstead.unique_operands(), 3);
9055                assert_eq!(metric.halstead.total_operands(), 3);
9056                insta::assert_json_snapshot!(metric.halstead);
9057            },
9058        );
9059    }
9060
9061    #[cfg(feature = "tcl")]
9062    #[test]
9063    fn tcl_interpolated_quoted_word_no_double_count() {
9064        // Regression for #277. Before the fix, `"$x is $y"` produced an
9065        // extra operand for the wrapping `QuotedWord` on top of the two
9066        // inner `VariableSubstitution` operands (`$x`, `$y`), giving 7.
9067        // After the fix, the wrapper is `TokenRole::Unknown` whenever
9068        // it carries an interpolation child, so operand attribution
9069        // belongs solely to the inner substitutions.
9070        check_metrics::<TclParser>(
9071            "proc f {x y} {
9072    set s \"$x is $y\"
9073}",
9074            "foo.tcl",
9075            |metric| {
9076                // Operands: `f`, `x`, `y` (proc args), the `set` target
9077                // `s`, `$x`, `$y` — 6 unique, 6 total. The wrapping
9078                // `QuotedWord` contributes nothing, and since #1354
9079                // neither does the proc-body `braced_word`. Before #277
9080                // the wrapper double-counted.
9081                assert_eq!(metric.halstead.unique_operands(), 6);
9082                assert_eq!(metric.halstead.total_operands(), 6);
9083                insta::assert_json_snapshot!(metric.halstead);
9084            },
9085        );
9086    }
9087
9088    #[cfg(feature = "tcl")]
9089    #[test]
9090    fn tcl_command_substitution_quoted_word_no_double_count() {
9091        // Regression for #277. A `"...[cmd]..."` literal exposes the
9092        // bracketed command as a `command_substitution` child whose inner
9093        // identifiers/literals contribute their own operands. The wrapping
9094        // `QuotedWord` must not also be classified as an operand, or the
9095        // command's identifier would be counted alongside a phantom
9096        // wrapper operand.
9097        check_metrics::<TclParser>(
9098            "proc f {} {
9099    set s \"result: [foo]\"
9100}",
9101            "foo.tcl",
9102            |metric| {
9103                // Operands: `f`, the `set` target `s`, `foo` — 3 unique,
9104                // 3 total. The wrapping `QuotedWord` and the inert text
9105                // `result: ` do not contribute extra operands, and since
9106                // #1354 neither does the proc-body `braced_word`. Before
9107                // #277 the wrapper double-counted.
9108                assert_eq!(metric.halstead.unique_operands(), 3);
9109                assert_eq!(metric.halstead.total_operands(), 3);
9110                insta::assert_json_snapshot!(metric.halstead);
9111            },
9112        );
9113    }
9114
9115    /// Regression for #1294. The `set` target parses as the anonymous
9116    /// `id` token (`Tcl::Id2`) — the same kind as the leaf inside a
9117    /// `variable_substitution` — and the getter used to exclude that kind
9118    /// wholesale, so every variable a Tcl script assigned was absent from
9119    /// n2/N2. The guard is now parent-scoped: a target `id` counts, a
9120    /// var-sub leaf does not. Exact occurrence counts distinguish this
9121    /// fix from a regression in either direction: a re-blanketed
9122    /// exclusion drops `s`/`t` (total 2), while losing the guard
9123    /// double-counts the `$s` leaf as a second `s` (total 5).
9124    #[cfg(feature = "tcl")]
9125    #[test]
9126    fn tcl_set_target_is_operand() {
9127        let source = "set s 1\nset t $s\n";
9128        let path = PathBuf::from("foo.tcl");
9129        let parser = TclParser::new(source.as_bytes().to_vec(), &path, None);
9130        let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
9131        // expected operands: targets `s` and `t`, literal `1`, reference
9132        // `$s` (wrapper only) — 4 total, each exactly once.
9133        for operand in ["s", "t", "1", "$s"] {
9134            assert_eq!(
9135                ops.operands
9136                    .iter()
9137                    .filter(|o| o.as_str() == operand)
9138                    .count(),
9139                1,
9140                "`{operand}` must be exactly one operand; operands were {:?}",
9141                ops.operands
9142            );
9143        }
9144        assert_eq!(
9145            ops.operands.len(),
9146            4,
9147            "operands must be exactly s, t, 1, $s; got {:?}",
9148            ops.operands
9149        );
9150
9151        check_metrics::<TclParser>(source, "foo.tcl", |metric| {
9152            // expected: n2 = 4 (s, t, 1, $s), N2 = 4; operators are the
9153            // two `set` keywords — n1 = 1, N1 = 2.
9154            assert_eq!(metric.halstead.unique_operands(), 4);
9155            assert_eq!(metric.halstead.total_operands(), 4);
9156            assert_eq!(metric.halstead.unique_operators(), 1);
9157            assert_eq!(metric.halstead.total_operators(), 2);
9158        });
9159    }
9160
9161    /// Drift marker for #1294 (lesson 34 / grammar-dispatch §2): the
9162    /// *named* `id` rule (`Tcl::Id`, kind_id 84) never surfaces at the
9163    /// pinned tree-sitter-tcl — the parser emits the anonymous `Id2` in
9164    /// both positions the getter guards (the `set` target and the
9165    /// var-sub leaf). The `Tcl::Id` arm in `get_op_type` is therefore
9166    /// defensive; if a grammar bump starts emitting 84 this fails and
9167    /// the arm's classification must be re-derived instead of trusted.
9168    #[cfg(feature = "tcl")]
9169    #[test]
9170    fn tcl_named_id_variant_is_unreachable() {
9171        let source = "proc f {x} {\n    set s $x\n    foreach v {1 2} { puts \"$v\" }\n}\n";
9172        let path = PathBuf::from("foo.tcl");
9173        let parser = TclParser::new(source.as_bytes().to_vec(), &path, None);
9174        // Non-vacuity: the anonymous token must be present in this parse
9175        // (both the `set` target and the `$x` / `$v` leaves emit it).
9176        assert!(
9177            ast_has_kind_id(&parser, Tcl::Id2 as u16),
9178            "expected the anonymous Tcl::Id2 token to appear in the parse",
9179        );
9180        assert!(
9181            !ast_has_kind_id(&parser, Tcl::Id as u16),
9182            "the named Tcl::Id rule surfaced; re-derive the defensive \
9183             `Tcl::Id` arm in TclCode::get_op_type against the new grammar",
9184        );
9185    }
9186
9187    #[cfg(feature = "tcl")]
9188    #[test]
9189    fn tcl_braced_word_delimiter_is_not_an_operator() {
9190        // Regression: issue #1314, the Tcl sibling of Elixir #1256 and
9191        // Ruby/Perl #1312. A braced *word* — a literal value, not a
9192        // script — carries its `{` as an `LBRACE` child, the kind id a
9193        // real block uses, so `set a {braced word}` reported a `{}`
9194        // operator with no block in the source.
9195        //
9196        // expected: operator `set` × 3 → n1 = 1, N1 = 3. Operands
9197        // `a`, `b`, `c`, `$a` and `{braced word}` × 2 → n2 = 5,
9198        // N2 = 6. Before the guard the two openers added `{}` →
9199        // n1 = 2, N1 = 5; before #1354 the inner `braced` / `word`
9200        // counted alongside the word containing them → n2 = 7,
9201        // N2 = 10.
9202        check_metrics::<TclParser>(
9203            "set a {braced word}\nset b {braced word}\nset c $a\n",
9204            "foo.tcl",
9205            |metric| {
9206                assert_eq!(metric.halstead.unique_operators(), 1);
9207                assert_eq!(metric.halstead.total_operators(), 3);
9208                assert_eq!(metric.halstead.unique_operands(), 5);
9209                assert_eq!(metric.halstead.total_operands(), 6);
9210            },
9211        );
9212    }
9213
9214    #[cfg(feature = "tcl")]
9215    #[test]
9216    fn tcl_script_bodies_keep_their_braces() {
9217        // Control for #1314, and the reason a kind-scoped guard is safe
9218        // in Tcl where it would not be elsewhere: the grammar gives the
9219        // literal and the block *different* kinds. A `proc` body, an
9220        // `if` body and an `if` condition are `BracedWord` (88) and
9221        // `Expr` (97); only the value form is `BracedWordSimple` (89).
9222        // This fixture nests a braced word inside a real script body,
9223        // so a guard that keyed on the brace alone would drop the
9224        // block's `{}` and fail here.
9225        //
9226        // expected: operators `proc`, `set` × 2, `if`, `>`, and `{}`
9227        // × 4 (the proc parameter list, the proc body, the `if`
9228        // condition, the `if` body) → n1 = 5, N1 = 9. Operands, all
9229        // distinct → n2 = N2 = 8: `p`, `x`, `a`, `$x`, `1`, `b` and
9230        // the two braced *words* `{v w}` and `{y}`, each one operand
9231        // rather than one per inner word.
9232        //
9233        // Before #1354 this read 13 / 13. The five extra entries were
9234        // the inner words `v`, `w`, `y` and the two *script* bodies,
9235        // which were `BracedWord` operands in their own right — so a
9236        // block counted twice over, once as the operand and once as
9237        // the `{}` operator this test is about. Both halves are gone;
9238        // the operator columns are what this test guards and they did
9239        // not move.
9240        check_metrics::<TclParser>(
9241            "proc p {x} {\n  set a {v w}\n  if {$x > 1} { set b {y} }\n}\n",
9242            "foo.tcl",
9243            |metric| {
9244                assert_eq!(metric.halstead.unique_operators(), 5);
9245                assert_eq!(metric.halstead.total_operators(), 9);
9246                assert_eq!(metric.halstead.unique_operands(), 8);
9247                assert_eq!(metric.halstead.total_operands(), 8);
9248            },
9249        );
9250    }
9251
9252    #[cfg(feature = "tcl")]
9253    #[test]
9254    fn tcl_braced_word_guard_is_parent_scoped_not_ancestor_scoped() {
9255        // The input that separates the parent-scoped guard from the
9256        // ancestor-scanning mutant. I first recorded this distinction
9257        // as *unobservable* in Tcl, reasoning that a braced word holds
9258        // only simple words and nested braced words. `bca dump` says
9259        // otherwise: the grammar parses a `[…]` command substitution
9260        // inside a braced word, and the `if` inside it brings an `Expr`
9261        // condition and a `BracedWord` body, each with its own `{`,
9262        // both of them non-immediate descendants of the
9263        // `BracedWordSimple`. An ancestor scan swallows both.
9264        //
9265        // (Real Tcl does not substitute inside braces — this is the
9266        // grammar modelling structure it will not evaluate. What the
9267        // classifier sees is what the metric reports, so it is the
9268        // right fixture regardless.)
9269        //
9270        // expected: operators `set`, `[]`, `if`, and `{}` × 2 (the
9271        // `if` condition's `Expr` and its `BracedWord` body; the outer
9272        // value word's own `{` is suppressed) → n1 = 4, N1 = 5.
9273        // Operands `z`, the whole braced word, and `$q` / `puts` / `w`
9274        // from inside the command substitution → n2 = N2 = 5. Under
9275        // the ancestor-scoped mutant both surviving braces vanish:
9276        // n1 = 3, N1 = 3.
9277        //
9278        // #1354 widened the guard from the `{` alone to every direct
9279        // child of the braced word, which is why `x` and `v` are no
9280        // longer operands and the nested script body no longer is
9281        // either. It did not change the *scope*: the three operands
9282        // from inside the command substitution are grandchildren, and
9283        // an ancestor-scoped guard would drop them too.
9284        check_metrics::<TclParser>("set z {x [if {$q} {puts w}] v}\n", "foo.tcl", |metric| {
9285            assert_eq!(metric.halstead.unique_operators(), 4);
9286            assert_eq!(metric.halstead.total_operators(), 5);
9287            assert_eq!(metric.halstead.unique_operands(), 5);
9288            assert_eq!(metric.halstead.total_operands(), 5);
9289        });
9290    }
9291
9292    #[cfg(feature = "irules")]
9293    #[test]
9294    fn irules_braced_word_guard_is_parent_scoped_not_ancestor_scoped() {
9295        // The iRules twin of the test above — the two getters are
9296        // clones, so the mutant must fail in both.
9297        //
9298        // expected: operators `when`, `set`, `[]`, `if`, `{}` × 3 (the
9299        // handler body, the `if` condition and the `if` body) → n1 = 5,
9300        // N1 = 7. Operands `HTTP_REQUEST`, `z`, the whole braced word,
9301        // and `$q` / `log` / `w` from inside the command substitution →
9302        // n2 = N2 = 6 (10 before #1354, which also took the direct
9303        // children `x` / `v` and the two script bodies out of the
9304        // operand set without moving the operator columns this test
9305        // guards).
9306        check_metrics::<IrulesParser>(
9307            "when HTTP_REQUEST {\n  set z {x [if {$q} {log w}] v}\n}\n",
9308            "foo.irule",
9309            |metric| {
9310                assert_eq!(metric.halstead.unique_operators(), 5);
9311                assert_eq!(metric.halstead.total_operators(), 7);
9312                assert_eq!(metric.halstead.unique_operands(), 6);
9313                assert_eq!(metric.halstead.total_operands(), 6);
9314            },
9315        );
9316    }
9317
9318    /// How one dialect of the Tcl family spells the braced-word
9319    /// construct. The two grammars are deliberate clones with different
9320    /// id blocks, so #1354's guard is derived once and instantiated
9321    /// twice — and a fix that landed in only one dialect fails the
9322    /// second instantiation rather than going unnoticed.
9323    struct BracedWordKinds {
9324        /// `braced_word_simple`, the literal *value* form the guard
9325        /// keys on.
9326        wrapper: u16,
9327        /// `braced_word`, the *script* form #1354 gated on holding a
9328        /// command: an operand only when it holds none.
9329        script_body: u16,
9330        /// `comment`, the one named child of a script that is not a
9331        /// command and that the gate must not mistake for one.
9332        comment: u16,
9333        /// Every named kind node-types.json admits directly inside
9334        /// `wrapper`. A child outside this set means the grammar moved
9335        /// and the parent-keyed arm has to be re-derived
9336        /// (grammar-dispatch §1).
9337        children: [u16; 6],
9338        /// The `{` / `}` the wrapper also holds. Suppressing the opener
9339        /// was the whole of #1314's narrower guard; the closer has
9340        /// never been classified, since `get_operator_id_as_str` folds
9341        /// the pair to one `{}` glyph.
9342        delimiters: [u16; 2],
9343        /// Of `children`, the three the operand arm classified
9344        /// unconditionally before the guard.
9345        operand_children: [u16; 3],
9346        /// `quoted_word`, which was an operand only when *inert* —
9347        /// `string_operand_type`'s own rule, replicated here so the
9348        /// shed count below is what `N2` actually shed.
9349        quoted_word: u16,
9350        /// The interpolation kinds that decide that.
9351        interpolation: [u16; 2],
9352    }
9353
9354    #[cfg(feature = "tcl")]
9355    const TCL_BRACED_WORD_KINDS: BracedWordKinds = BracedWordKinds {
9356        wrapper: Tcl::BracedWordSimple as u16,
9357        script_body: Tcl::BracedWord as u16,
9358        comment: Tcl::Comment as u16,
9359        children: [
9360            Tcl::SimpleWord as u16,
9361            Tcl::EscapedCharacter as u16,
9362            Tcl::QuotedWord as u16,
9363            Tcl::VariableSubstitution as u16,
9364            Tcl::CommandSubstitution as u16,
9365            Tcl::BracedWordSimple as u16,
9366        ],
9367        delimiters: [Tcl::LBRACE as u16, Tcl::RBRACE as u16],
9368        operand_children: [
9369            Tcl::SimpleWord as u16,
9370            Tcl::VariableSubstitution as u16,
9371            Tcl::BracedWordSimple as u16,
9372        ],
9373        quoted_word: Tcl::QuotedWord as u16,
9374        interpolation: [
9375            Tcl::VariableSubstitution as u16,
9376            Tcl::CommandSubstitution as u16,
9377        ],
9378    };
9379
9380    #[cfg(feature = "irules")]
9381    const IRULES_BRACED_WORD_KINDS: BracedWordKinds = BracedWordKinds {
9382        wrapper: Irules::BracedWordSimple as u16,
9383        script_body: Irules::BracedWord as u16,
9384        comment: Irules::Comment as u16,
9385        children: [
9386            Irules::SimpleWord as u16,
9387            Irules::EscapedCharacter as u16,
9388            Irules::QuotedWord as u16,
9389            Irules::VariableSubstitution as u16,
9390            Irules::CommandSubstitution as u16,
9391            Irules::BracedWordSimple as u16,
9392        ],
9393        delimiters: [Irules::LBRACE as u16, Irules::RBRACE as u16],
9394        operand_children: [
9395            Irules::SimpleWord as u16,
9396            Irules::VariableSubstitution as u16,
9397            Irules::BracedWordSimple as u16,
9398        ],
9399        quoted_word: Irules::QuotedWord as u16,
9400        interpolation: [
9401            Irules::VariableSubstitution as u16,
9402            Irules::CommandSubstitution as u16,
9403        ],
9404    };
9405
9406    /// The operand occurrences #1354 removed from `source`, as their
9407    /// source texts, plus the set of `braced_word_simple` child kinds
9408    /// the fixture witnessed.
9409    ///
9410    /// Walks with `for_each_node_with_chain`, which maintains the
9411    /// ancestor chain exactly as `spaces::compute` does, so "parent"
9412    /// here means what `Ancestors::parent` means inside the guard.
9413    /// Doubles as the grammar-dispatch §1 / §2 drift marker: a child of
9414    /// the wrapper outside `children` ∪ `delimiters` fails on the spot,
9415    /// which is what makes keying the arm on the parent alone — rather
9416    /// than on an enumerated child list — safe to rely on.
9417    #[cfg(any(feature = "irules", feature = "tcl"))]
9418    fn braced_word_shed<L: crate::LanguageInfo>(
9419        source: &str,
9420        kinds: &BracedWordKinds,
9421    ) -> (Vec<String>, HashSet<u16>) {
9422        let code = source.as_bytes();
9423        let mut shed = Vec::new();
9424        let mut witnessed = HashSet::new();
9425        for_each_node_with_chain::<L>(code, |node, chain| {
9426            let text = || {
9427                node.utf8_text(code)
9428                    .expect("fixture is valid UTF-8")
9429                    .to_owned()
9430            };
9431            // The script form was an operand of its own wherever it
9432            // appeared until #1354, which now gates it on holding a
9433            // command — a named child that is not a comment. This is not
9434            // a child of the wrapper, so it is counted before the parent
9435            // test below.
9436            if node.kind_id() == kinds.script_body
9437                && node
9438                    .children()
9439                    .any(|child| child.is_named() && child.kind_id() != kinds.comment)
9440            {
9441                shed.push(text());
9442            }
9443            if chain.last().is_none_or(|p| p.kind_id() != kinds.wrapper) {
9444                return;
9445            }
9446            assert!(
9447                kinds.children.contains(&node.kind_id())
9448                    || kinds.delimiters.contains(&node.kind_id()),
9449                "`{source}`: a `{}` inside a braced word is a child this guard \
9450                 was not derived against; re-read node-types.json before \
9451                 trusting it",
9452                node.kind(),
9453            );
9454            if kinds.children.contains(&node.kind_id()) {
9455                witnessed.insert(node.kind_id());
9456            }
9457            let was_operand = kinds.operand_children.contains(&node.kind_id())
9458                || (node.kind_id() == kinds.quoted_word && !node.wraps_any(&kinds.interpolation));
9459            if was_operand {
9460                shed.push(text());
9461            }
9462        });
9463        (shed, witnessed)
9464    }
9465
9466    /// One row of the #1354 tables: a fixture, what it measures now,
9467    /// what it measured before, and the operand texts behind the counts.
9468    struct BracedWordCase {
9469        source: &'static str,
9470        /// `[n1, N1, n2, N2]` with the guard in place.
9471        counts: [u64; 4],
9472        /// `[n2, N2]` without it. Re-derived by the loop rather than
9473        /// trusted, so a stale row fails instead of misinforming.
9474        before: [u64; 2],
9475        operands: &'static [&'static str],
9476    }
9477
9478    /// Every row is measured in *both* dialects, so a fix applied to
9479    /// one getter and not its clone fails here. `braced_word_shed`'s
9480    /// drift assertion likewise runs against both grammars.
9481    #[cfg(any(feature = "irules", feature = "tcl"))]
9482    fn check_braced_word_cases<T: crate::MetricSuite, L: crate::LanguageInfo>(
9483        cases: &[BracedWordCase],
9484        file: &str,
9485        kinds: &BracedWordKinds,
9486    ) -> HashSet<u16> {
9487        let mut witnessed = HashSet::new();
9488        for case in cases {
9489            let (shed, seen) = braced_word_shed::<L>(case.source, kinds);
9490            witnessed.extend(seen);
9491
9492            // Phrased as an addition rather than a subtraction so a
9493            // future edit that inverts the two underflows nothing.
9494            assert_eq!(
9495                case.before[1],
9496                case.counts[3] + shed.len() as u64,
9497                "{file} `{}`: N2 must shed exactly one occurrence per \
9498                 previously-billed part; recorded {}, parts {shed:?}",
9499                case.source,
9500                case.before[1],
9501            );
9502            let mut vocabulary: HashSet<&str> = case.operands.iter().copied().collect();
9503            vocabulary.extend(shed.iter().map(String::as_str));
9504            assert_eq!(
9505                vocabulary.len() as u64,
9506                case.before[0],
9507                "{file} `{}`: n2 before the fix is the post-fix vocabulary \
9508                 plus those parts; got {vocabulary:?}",
9509                case.source,
9510            );
9511
9512            assert_halstead_counts::<T>(case.source, file, case.counts, case.source);
9513            assert_ops_operands::<T>(
9514                case.source,
9515                file,
9516                case.operands.len(),
9517                case.operands.to_vec(),
9518            );
9519        }
9520        witnessed
9521    }
9522
9523    /// The rows shared by both dialects: one per named child kind
9524    /// `braced_word_simple` admits, the childless spelling, the
9525    /// repeated-value row that separates `n2` from `N2` (#1294), and
9526    /// the braced/quoted parity pair #1317 asks for.
9527    #[cfg(any(feature = "irules", feature = "tcl"))]
9528    const BRACED_WORD_CASES: [BracedWordCase; 11] = [
9529        // simple_word, the reported fixture. Two words inside one
9530        // value scored two operands beside the value itself.
9531        BracedWordCase {
9532            source: "set x {literal here}\n",
9533            counts: [1, 1, 2, 2],
9534            before: [4, 4],
9535            operands: &["x", "{literal here}"],
9536        },
9537        // The childless spelling, and the reason the wrapper is kept
9538        // rather than dropped in favour of its contents
9539        // (grammar-dispatch §6): with nothing inside, the wrapper is
9540        // the empty string's only carrier. Nothing is shed here, so
9541        // this row asserts the two columns agree.
9542        BracedWordCase {
9543            source: "set y {}\n",
9544            counts: [1, 1, 2, 2],
9545            before: [2, 2],
9546            operands: &["y", "{}"],
9547        },
9548        // braced_word_simple inside braced_word_simple: the nesting
9549        // that makes the over-count unbounded in depth. One value
9550        // spelled six vocabulary entries.
9551        BracedWordCase {
9552            source: "set a {x {y z}}\n",
9553            counts: [1, 1, 2, 2],
9554            before: [6, 6],
9555            operands: &["a", "{x {y z}}"],
9556        },
9557        // quoted_word, inert — an operand in its own right elsewhere,
9558        // and shed here.
9559        BracedWordCase {
9560            source: "set a {x \"q w\" v}\n",
9561            counts: [1, 1, 2, 2],
9562            before: [5, 5],
9563            operands: &["a", "{x \"q w\" v}"],
9564        },
9565        // quoted_word carrying an interpolation, which was *not* an
9566        // operand before the guard either (`string_operand_type` had
9567        // already suppressed it) — so only `x` and `v` are shed. Its
9568        // `$q` is a grandchild of the braced word and still counts,
9569        // which is the parent-scoping this arm inherits from #1314.
9570        BracedWordCase {
9571            source: "set a {x \"$q\" v}\n",
9572            counts: [1, 1, 3, 3],
9573            before: [5, 5],
9574            operands: &["a", "$q", "{x \"$q\" v}"],
9575        },
9576        // escaped_character, never classified — so it sheds nothing
9577        // and the row measures the two `simple_word`s around it.
9578        BracedWordCase {
9579            source: "set a {x \\n y}\n",
9580            counts: [1, 1, 2, 2],
9581            before: [4, 4],
9582            operands: &["a", "{x \\n y}"],
9583        },
9584        // variable_substitution. Tcl substitutes nothing between
9585        // braces, so `{$x}` is the two-character string `$x` — the row
9586        // that makes "the wrapper is the value" more than a tie-break.
9587        BracedWordCase {
9588            source: "set a {$x}\n",
9589            counts: [1, 1, 2, 2],
9590            before: [3, 3],
9591            operands: &["a", "{$x}"],
9592        },
9593        // command_substitution, likewise never an operand itself. Its
9594        // interior is, and stays so: `$q`, `puts` and `w` are
9595        // grandchildren. The nested `{puts w}` is a *script* body and
9596        // sheds under the other half of #1354.
9597        BracedWordCase {
9598            source: "set z {x [if {$q} {puts w}] v}\n",
9599            counts: [4, 5, 5, 5],
9600            before: [8, 8],
9601            operands: &["z", "$q", "puts", "w", "{x [if {$q} {puts w}] v}"],
9602        },
9603        // The same value twice: n2 3 against N2 4, so a row that
9604        // asserted only the vocabulary could not tell the two axes
9605        // apart (#1294).
9606        BracedWordCase {
9607            source: "set a {b c}\nset d {b c}\n",
9608            counts: [1, 2, 3, 4],
9609            before: [5, 8],
9610            operands: &["a", "d", "{b c}"],
9611        },
9612        // The parity pair #1317 named: two spellings of one literal
9613        // value must score alike. They did not before — braced 4 / 4
9614        // against quoted 2 / 2 — which is the spelling sensitivity
9615        // #695, #1312 and #1314 each removed elsewhere.
9616        BracedWordCase {
9617            source: "set a {one two}\n",
9618            counts: [1, 1, 2, 2],
9619            before: [4, 4],
9620            operands: &["a", "{one two}"],
9621        },
9622        BracedWordCase {
9623            source: "set a \"one two\"\n",
9624            counts: [1, 1, 2, 2],
9625            before: [2, 2],
9626            operands: &["a", "\"one two\""],
9627        },
9628    ];
9629
9630    /// The script half of #1354, shared by both dialects: a
9631    /// `braced_word` holding commands is a block whose contents the
9632    /// walk already counts, so it is no longer also an operand
9633    /// spanning the whole block.
9634    ///
9635    /// The three childless rows are the gate, and the reason this is a
9636    /// gate and not a deletion (grammar-dispatch §6). `braced_word` is
9637    /// not only the script kind: it is the value slot of every command
9638    /// the grammar does not special-case, where `lappend l {}` is an
9639    /// empty list whose brace pair is its only carrier. Deleting the
9640    /// kind scored that zero while its `lappend l ""` synonym — the
9641    /// last row, the control — scored one. An empty `proc` body is
9642    /// spelled identically and so also keeps an operand; #1318 tells
9643    /// the two roles apart by the enclosing command, but it bills them
9644    /// alike, so this gate is still what keeps the operand.
9645    ///
9646    /// The rows' *operator* columns moved with #1318 wherever the
9647    /// enclosing command takes a value, which is why the `lappend`
9648    /// rows here read `0` where the `proc` and `if` rows read the
9649    /// block's `{}`. The operand columns are untouched by it — #1318
9650    /// revises the brace and nothing else — so every `before` here
9651    /// still describes #1354 alone.
9652    #[cfg(any(feature = "irules", feature = "tcl"))]
9653    const SCRIPT_BODY_CASES: [BracedWordCase; 9] = [
9654        BracedWordCase {
9655            source: "proc p {} { set b 1 }\n",
9656            counts: [3, 4, 3, 3],
9657            before: [4, 4],
9658            operands: &["p", "b", "1"],
9659        },
9660        BracedWordCase {
9661            source: "proc p {} {}\n",
9662            counts: [2, 3, 2, 2],
9663            before: [2, 2],
9664            operands: &["p", "{}"],
9665        },
9666        // A comment is a named child of the script but not a command,
9667        // and no arm bills it, so a comment-only body scores like the
9668        // empty one above rather than like nothing: the block's whole
9669        // text is its one operand. Gating on "any named child" billed
9670        // it zero — adding a comment to an empty block lowered N2.
9671        BracedWordCase {
9672            source: "proc p {} {\n    # only a comment\n}\n",
9673            counts: [2, 3, 2, 2],
9674            before: [2, 2],
9675            operands: &["p", "{\n    # only a comment\n}"],
9676        },
9677        BracedWordCase {
9678            source: "if {$q} {\n    # noop\n}\n",
9679            counts: [2, 3, 2, 2],
9680            before: [2, 2],
9681            operands: &["$q", "{\n    # noop\n}"],
9682        },
9683        // The control: a comment *beside* a command changes nothing, so a
9684        // gate keyed on "contains a comment" would fail this row.
9685        BracedWordCase {
9686            source: "proc p {} {\n    # c\n    set b 1\n}\n",
9687            counts: [3, 4, 3, 3],
9688            before: [4, 4],
9689            operands: &["p", "b", "1"],
9690        },
9691        // The value-role twin: inside a literal string Tcl performs no
9692        // substitution, so `# x` is not a comment at all, and the word
9693        // is its one operand exactly as `lappend l {}` below is. Its
9694        // operator columns are zero since #1318 — `lappend` takes a
9695        // value, so the brace pair is a quote and not a block.
9696        BracedWordCase {
9697            source: "lappend l {\n    # x\n}\n",
9698            counts: [0, 0, 3, 3],
9699            before: [3, 3],
9700            operands: &["lappend", "l", "{\n    # x\n}"],
9701        },
9702        // A `braced_word` in *value* position — the #1318 role, where
9703        // the same kind carries a literal list. Its interior words
9704        // counted before #1354 and still do; what #1354 removed is the
9705        // whole-block operand that was double-billing them, and what
9706        // #1318 removed is the `{}` operator (`lappend` takes a value,
9707        // so the brace pair quotes rather than opens).
9708        BracedWordCase {
9709            source: "lappend l {a b}\n",
9710            counts: [0, 0, 4, 4],
9711            before: [5, 5],
9712            operands: &["lappend", "l", "a", "b"],
9713        },
9714        // …and the childless spelling of that value, which sheds
9715        // nothing: the brace pair is the empty list's only carrier.
9716        BracedWordCase {
9717            source: "lappend l {}\nreturn {}\n",
9718            counts: [0, 0, 4, 5],
9719            before: [4, 5],
9720            operands: &["lappend", "l", "return", "{}"],
9721        },
9722        // The control the row above is measured against: the quoted
9723        // spelling of the same empty value, which never depended on
9724        // the arm and must keep scoring one operand. Since #1318 the
9725        // two rows agree on all four columns rather than only on the
9726        // operand pair — an empty list is an empty list however it is
9727        // spelled.
9728        BracedWordCase {
9729            source: "lappend l \"\"\nreturn \"\"\n",
9730            counts: [0, 0, 4, 5],
9731            before: [4, 5],
9732            operands: &["lappend", "l", "return", "\"\""],
9733        },
9734    ];
9735
9736    /// The table must exercise every named child kind the grammar
9737    /// admits inside a braced word, and no other — the other half of
9738    /// the drift marker in `braced_word_shed`, which can only police
9739    /// kinds a fixture actually produces.
9740    #[cfg(any(feature = "irules", feature = "tcl"))]
9741    fn assert_braced_word_children_witnessed(
9742        witnessed: &HashSet<u16>,
9743        kinds: &BracedWordKinds,
9744        dialect: &str,
9745    ) {
9746        let mut got: Vec<u16> = witnessed.iter().copied().collect();
9747        got.sort_unstable();
9748        let mut expected = kinds.children;
9749        expected.sort_unstable();
9750        assert_eq!(
9751            got.as_slice(),
9752            expected.as_slice(),
9753            "{dialect}: the fixtures must witness every child kind \
9754             node-types.json admits inside a braced word",
9755        );
9756    }
9757
9758    /// Regression for #1354 and #1317. `braced_word_simple` is a
9759    /// literal value and `braced_word` a script, and both were
9760    /// operands *beside* the content the walk counts anyway: a braced
9761    /// word was billed once per inner word plus once for itself, and a
9762    /// block once for every command in it plus once for its whole
9763    /// text. `set x {literal here}` scored n2 4 / N2 4 for one value.
9764    /// The name is the invariant either way — the value's content is
9765    /// the literal, the script's is its commands, and each is billed
9766    /// once.
9767    ///
9768    /// Each row's `before` column is re-derived by the loop from the
9769    /// current parse rather than trusted. That derivation is a replica
9770    /// of the pre-#1354 arms and could in principle drift from what
9771    /// they did, so every column was also measured directly against a
9772    /// build of the old getters — `lappend l {a b}` at n2 5 / N2 5,
9773    /// `lappend l {}` at 4 / 5, `proc p {} {}` at 2 / 2 — rather than
9774    /// derived only from the model here.
9775    ///
9776    /// The same walk doubles as the drift marker for the parent-keyed
9777    /// arm: it fails if the grammar ever puts a seventh kind directly
9778    /// inside a braced word, and the union assertion below fails if a
9779    /// bump stops emitting one of the six.
9780    #[cfg(feature = "tcl")]
9781    #[test]
9782    fn tcl_braced_word_bills_its_content_once_1354() {
9783        let mut witnessed = check_braced_word_cases::<TclParser, TclCode>(
9784            &BRACED_WORD_CASES,
9785            "foo.tcl",
9786            &TCL_BRACED_WORD_KINDS,
9787        );
9788        witnessed.extend(check_braced_word_cases::<TclParser, TclCode>(
9789            &SCRIPT_BODY_CASES,
9790            "foo.tcl",
9791            &TCL_BRACED_WORD_KINDS,
9792        ));
9793        assert_braced_word_children_witnessed(&witnessed, &TCL_BRACED_WORD_KINDS, "tcl");
9794    }
9795
9796    /// The iRules twin. The tables are shared, so a fix that reached
9797    /// only `big-code-analysis-ast/src/getter/tcl.rs` fails every row
9798    /// here — the two getters are deliberate clones and #1354 names
9799    /// both.
9800    #[cfg(feature = "irules")]
9801    #[test]
9802    fn irules_braced_word_bills_its_content_once_1354() {
9803        let mut witnessed = check_braced_word_cases::<IrulesParser, IrulesCode>(
9804            &BRACED_WORD_CASES,
9805            "foo.irule",
9806            &IRULES_BRACED_WORD_KINDS,
9807        );
9808        witnessed.extend(check_braced_word_cases::<IrulesParser, IrulesCode>(
9809            &SCRIPT_BODY_CASES,
9810            "foo.irule",
9811            &IRULES_BRACED_WORD_KINDS,
9812        ));
9813        // The `when` handler body is the iRules-only spelling of a
9814        // script body, and the largest instance of the defect: its
9815        // operand text was the entire event handler.
9816        let handlers: [BracedWordCase; 3] = [
9817            BracedWordCase {
9818                source: "when HTTP_REQUEST { set x 1 }\n",
9819                counts: [3, 3, 3, 3],
9820                before: [4, 4],
9821                operands: &["HTTP_REQUEST", "x", "1"],
9822            },
9823            BracedWordCase {
9824                source: "when HTTP_REQUEST {}\n",
9825                counts: [2, 2, 2, 2],
9826                before: [2, 2],
9827                operands: &["HTTP_REQUEST", "{}"],
9828            },
9829            // The comment-only twin of the empty handler above; see the
9830            // shared table for why it scores like it.
9831            BracedWordCase {
9832                source: "when HTTP_REQUEST {\n    # only a comment\n}\n",
9833                counts: [2, 2, 2, 2],
9834                before: [2, 2],
9835                operands: &["HTTP_REQUEST", "{\n    # only a comment\n}"],
9836            },
9837        ];
9838        witnessed.extend(check_braced_word_cases::<IrulesParser, IrulesCode>(
9839            &handlers,
9840            "foo.irule",
9841            &IRULES_BRACED_WORD_KINDS,
9842        ));
9843        assert_braced_word_children_witnessed(&witnessed, &IRULES_BRACED_WORD_KINDS, "irules");
9844    }
9845
9846    /// One row of the #1318 tables: a fixture and the four Halstead
9847    /// columns plus the operand vocabulary behind them.
9848    ///
9849    /// No `before` column, unlike [`BracedWordCase`]: #1318 is not a
9850    /// guard whose absence a walk can model, it is a *classification*.
9851    /// The pre-fix counts are recorded per row in prose instead, and
9852    /// each was measured against a build without the
9853    /// `get_op_type_with_code` overrides.
9854    struct BracedWordValueCase {
9855        source: &'static str,
9856        /// `[n1, N1, n2, N2]`.
9857        counts: [u64; 4],
9858        operands: &'static [&'static str],
9859    }
9860
9861    /// Runs one #1318 table against one dialect. Both dialects run
9862    /// every shared row, so a fix that reached one getter and not its
9863    /// clone fails here.
9864    #[cfg(any(feature = "irules", feature = "tcl"))]
9865    fn check_braced_word_value_cases<T: crate::MetricSuite>(
9866        cases: &[BracedWordValueCase],
9867        file: &str,
9868    ) {
9869        for case in cases {
9870            assert_halstead_counts::<T>(case.source, file, case.counts, case.source);
9871            assert_ops_operands::<T>(
9872                case.source,
9873                file,
9874                case.operands.len(),
9875                case.operands.to_vec(),
9876            );
9877        }
9878    }
9879
9880    /// The #1318 rows both dialects share. Every independent path
9881    /// through the role test gets a row of its own
9882    /// (`.claude/rules/testing.md`, "give one input to each independent
9883    /// path"): the value default, the script-command list, a modelled
9884    /// construct reached through a `word_list`, the two modelled slots
9885    /// that hold values, an unresolvable command name, and both
9886    /// nesting directions.
9887    ///
9888    /// **Every row asserts the operand columns too, and they are the
9889    /// half that must not move.** #1318 revises one operator and
9890    /// nothing else, so a mutant that suppressed a value word's
9891    /// *contents* — the tidier-looking rule, which collapses an
9892    /// `oo::class create C {…}` body into a single operand — fails
9893    /// here rather than passing as an improvement. #1382 keeps to the
9894    /// same discipline: it withdraws further `{}` operators and moves
9895    /// no operand column in this table.
9896    ///
9897    /// The trailing rows are #1382's. The first four are two *pairs*:
9898    /// a braced value slot beside the bare spelling of the same
9899    /// argument, which must score alike. Before #1382 they did not —
9900    /// the braced spelling billed one extra `{}`. The four after them
9901    /// carry no bare partner: each braces a value with a space in it
9902    /// (`proc {my proc}`, `{a b}`, `{code msg}`), which is the whole
9903    /// reason the braces are there. Their pre-#1382 counts are
9904    /// recorded per row instead.
9905    #[cfg(any(feature = "irules", feature = "tcl"))]
9906    const BRACED_WORD_VALUE_CASES: [BracedWordValueCase; 24] = [
9907        // The rule reaches the opener and *only* the opener. A `;`
9908        // separating two commands is a direct child of the
9909        // `braced_word` — `_terminator` is a hidden rule, so it is
9910        // inlined rather than wrapped — and both dialects classify
9911        // `SEMI` as an operator, so a revision keyed on the parent
9912        // kind alone swallowed it too: this row read n1 0 / N1 0 and
9913        // `halstead.effort` 0.0, against a line that is two commands.
9914        // The pairing with the `eval` row below is the point — same
9915        // brace, same separator, one command name apart.
9916        BracedWordValueCase {
9917            source: "lappend l {puts a ; puts b}\n",
9918            counts: [1, 1, 5, 6],
9919            operands: &["lappend", "l", "puts", "a", "b"],
9920        },
9921        BracedWordValueCase {
9922            source: "eval {puts a ; puts b}\n",
9923            counts: [2, 2, 4, 5],
9924            operands: &["eval", "puts", "a", "b"],
9925        },
9926        // The reported fixture. `lappend` takes a list, so `{a b}`
9927        // quotes rather than opens: no operator. Before #1318 this
9928        // read n1 1 / N1 1 — a `{}` block the line does not contain.
9929        BracedWordValueCase {
9930            source: "lappend l {a b}\n",
9931            counts: [0, 0, 4, 4],
9932            operands: &["lappend", "l", "a", "b"],
9933        },
9934        // The issue's second line, and the one showing the defect
9935        // needs no second argument to appear.
9936        BracedWordValueCase {
9937            source: "puts {c d}\n",
9938            counts: [0, 0, 3, 3],
9939            operands: &["puts", "c", "d"],
9940        },
9941        // The headline: one value, two commands, and until #1318 two
9942        // different operator counts — `set` reported none, `lappend` a
9943        // `{}`. `set` is an operator and `lappend` an operand because
9944        // the grammar models one command and not the other; that
9945        // asymmetry is the grammar's, not this rule's.
9946        BracedWordValueCase {
9947            source: "set x {a b}\nlappend y {a b}\n",
9948            counts: [1, 1, 6, 6],
9949            operands: &["lappend", "x", "y", "{a b}", "a", "b"],
9950        },
9951        // The script side of the same shape, and the reason `LBRACE`
9952        // is gated rather than deleted (grammar-dispatch §6): `eval`
9953        // evaluates its argument, so the block is real and keeps its
9954        // `{}`.
9955        BracedWordValueCase {
9956            source: "eval {puts hi}\n",
9957            counts: [1, 1, 3, 3],
9958            operands: &["eval", "puts", "hi"],
9959        },
9960        // The same command named through the global namespace, which
9961        // is the spelling a `namespace eval` body uses to guarantee the
9962        // core command over a local proc shadowing it. `::eval` *is*
9963        // `eval`, so the two rows must agree on the operator column;
9964        // before the leading `::` was stripped this one fell to the
9965        // value default and lost its block.
9966        BracedWordValueCase {
9967            source: "::eval {puts hi}\n",
9968            counts: [1, 1, 3, 3],
9969            operands: &["::eval", "puts", "hi"],
9970        },
9971        // The control for that strip: only the *leading* qualifier is
9972        // a namespace path to the core command. `ns::eval` is a
9973        // different proc living in `ns`, so it takes the value default
9974        // like any other unrecognised name — a strip of every `::`
9975        // segment would have promoted it to a script.
9976        BracedWordValueCase {
9977            source: "ns::eval {a b}\n",
9978            counts: [0, 0, 3, 3],
9979            operands: &["ns::eval", "a", "b"],
9980        },
9981        // The default for an unrecognised command — a user proc here,
9982        // but equally any package command. Nothing distinguishes it
9983        // from `lappend` structurally, so the row exists to pin which
9984        // way the coin lands. Note the words inside are still
9985        // operands: the rule withdraws the block, never the code.
9986        BracedWordValueCase {
9987            source: "myproc {g h}\n",
9988            counts: [0, 0, 3, 3],
9989            operands: &["myproc", "g", "h"],
9990        },
9991        // `namespace` is the one construct either grammar models that
9992        // still reaches its body through a `word_list`, so it is the
9993        // row that fails if the rule stops at the parent instead of
9994        // reading the grandparent. Its body must stay a script.
9995        BracedWordValueCase {
9996            source: "namespace eval ns {\n    set a 1\n}\n",
9997            counts: [3, 3, 4, 4],
9998            operands: &["eval", "ns", "a", "1"],
9999        },
10000        // A literal nested in a literal: neither brace opens a block,
10001        // and both words survive.
10002        BracedWordValueCase {
10003            source: "puts {a {b c}}\n",
10004            counts: [0, 0, 4, 4],
10005            operands: &["puts", "a", "b", "c"],
10006        },
10007        // A `set` inside a literal. The `set` keyword and the `[]`
10008        // substitution are real operators of a command the walk
10009        // descends into, and they stay — the rule reaches exactly the
10010        // one brace whose parent it can classify, never a subtree.
10011        // `{x [foo] v}` is a `braced_word_simple`, so #1354 bills it
10012        // whole and its `[foo]` interior separately, unchanged.
10013        BracedWordValueCase {
10014            source: "lappend l {set a {x [foo] v}}\n",
10015            counts: [2, 2, 5, 5],
10016            operands: &["lappend", "l", "a", "{x [foo] v}", "foo"],
10017        },
10018        // A braced word in the command *name* position. Legal Tcl —
10019        // `{puts} hi` invokes `puts` — and the one generic position
10020        // that is not an argument, so it is answered before the
10021        // command lookup. It must also never reach the `switch`-arm
10022        // rescue, which is what the braced-pattern row in the Tcl
10023        // table checks.
10024        BracedWordValueCase {
10025            source: "{puts} hi\n",
10026            counts: [0, 0, 2, 2],
10027            operands: &["puts", "hi"],
10028        },
10029        // A defaulted `proc` parameter — the other modelled slot that
10030        // holds a value, decided by the parent kind rather than by a
10031        // command name. `{x y}` is data the interpreter assigns, never
10032        // a script it runs, and it billed a fourth `{}` before #1318.
10033        // The three that remain are the parameter list, the parameter
10034        // spec and the body: those two list delimiters group a
10035        // declaration rather than quote a value, the line #1354 drew
10036        // for `Arguments`.
10037        BracedWordValueCase {
10038            source: "proc p {a {b {x y}}} { puts $a }\n",
10039            counts: [2, 4, 7, 7],
10040            operands: &["p", "a", "b", "x", "y", "puts", "$a"],
10041        },
10042        // A literal nested in a script, the direction that must keep
10043        // working: the `proc` body is a block (`{}` twice with the
10044        // parameter list) and the list it appends is not.
10045        BracedWordValueCase {
10046            source: "proc p {} { lappend l {a b} }\n",
10047            counts: [2, 3, 5, 5],
10048            operands: &["p", "lappend", "l", "a", "b"],
10049        },
10050        // A computed command name resolves to nothing, so the value
10051        // default decides. `$c` is the reference's own operand.
10052        BracedWordValueCase {
10053            source: "set c puts\n$c {a b}\n",
10054            counts: [1, 1, 5, 5],
10055            operands: &["c", "puts", "$c", "a", "b"],
10056        },
10057        // #1382's headline, and the pair that names the defect: `after`
10058        // is a script-taking command, but only from its *second*
10059        // argument — the first is a millisecond count. Both spellings
10060        // of that count are one operand and no operator; before #1382
10061        // the braced one billed a `{}` for a block the line does not
10062        // contain, reading N1 2 against the bare form's 1. The `{}`
10063        // both rows do report is the `{puts hi}` script, which is real.
10064        BracedWordValueCase {
10065            source: "after {100} {puts hi}\n",
10066            counts: [1, 1, 4, 4],
10067            operands: &["after", "100", "puts", "hi"],
10068        },
10069        BracedWordValueCase {
10070            source: "after 100 {puts hi}\n",
10071            counts: [1, 1, 4, 4],
10072            operands: &["after", "100", "puts", "hi"],
10073        },
10074        // The same pair for `time script ?count?`, whose value slot is
10075        // the *last* rather than the first — so a rule that hard-coded
10076        // "argument 0 is the value" passes the `after` pair and fails
10077        // this one. The script argument keeps its `{}` in both rows,
10078        // which is what makes the operator column a 1 and not a 0.
10079        //
10080        // The *bare* row is the half that catches it, which is not the
10081        // half it looks like. Measured by giving `time` `after`'s slot
10082        // rule: `time {puts hi} 3` reads n1 0 / N1 0, its one block
10083        // demoted to a value — while the braced row below is unmoved
10084        // at [1, 1, 4, 4], because `{puts hi}` loses the `{}` that
10085        // `{3}` gains and the operand texts are the same either way.
10086        // Do not trim the bare spelling as redundant with the braced
10087        // one; here it is the discriminating row of the two.
10088        BracedWordValueCase {
10089            source: "time {puts hi} {3}\n",
10090            counts: [1, 1, 4, 4],
10091            operands: &["time", "puts", "hi", "3"],
10092        },
10093        BracedWordValueCase {
10094            source: "time {puts hi} 3\n",
10095            counts: [1, 1, 4, 4],
10096            operands: &["time", "puts", "hi", "3"],
10097        },
10098        // A braced `proc` *name*, the one value slot the grammar names
10099        // as a field rather than by position. `{my proc}` is an
10100        // identifier containing a space, not a body, and billed a
10101        // fourth `{}` before #1382. The three operators left are the
10102        // `proc` keyword and the two braces of the parameter list and
10103        // the body — and note the interior words survive, `proc` being
10104        // an operand here and an operator one column over.
10105        BracedWordValueCase {
10106            source: "proc {my proc} {} {}\n",
10107            counts: [2, 3, 3, 3],
10108            operands: &["my", "proc", "{}"],
10109        },
10110        // The two `namespace` subcommands that take values throughout,
10111        // both of them Tcl 8.6 standard-library spellings. Their braced
10112        // argument is a pattern list and a dictionary; neither is
10113        // evaluated, and each billed a `{}` before #1382 — n1 2 / N1 2
10114        // against the 1 / 1 here. The `namespace` keyword is the
10115        // operator that remains, which is what keeps these two rows
10116        // from reading `effort` 0.0.
10117        BracedWordValueCase {
10118            source: "namespace export {a b}\n",
10119            counts: [1, 1, 3, 3],
10120            operands: &["export", "a", "b"],
10121        },
10122        BracedWordValueCase {
10123            source: "namespace ensemble create -map {a b}\n",
10124            counts: [1, 1, 5, 5],
10125            operands: &["ensemble", "create", "-map", "a", "b"],
10126        },
10127        // A `trap` clause written outside a `try`, which is how both
10128        // grammars reach the generic-command path for it. This is the
10129        // `ScriptSlots::LastOf` row — the pattern and the variable list
10130        // are values and the handler is the script — and the only one
10131        // whose slot is counted from the *end* of the list, so a rule
10132        // that read `Only(2)` from the front would need the arity to
10133        // pass it. Before #1382 all three braces billed: N1 3.
10134        BracedWordValueCase {
10135            source: "trap {POSIX} {code msg} {puts hi}\n",
10136            counts: [1, 1, 6, 6],
10137            operands: &["trap", "POSIX", "code", "msg", "puts", "hi"],
10138        },
10139    ];
10140
10141    /// Regression for #1318. `braced_word` carries both a block and a
10142    /// plain literal, and the grammars spell the two identically, so
10143    /// #1314's kind-scoped guard reached only the value slots they
10144    /// special-case: `set x {a b}` reported no operator while
10145    /// `lappend x {a b}` reported a `{}` for a block the line does not
10146    /// contain. The role is recognised out-of-band, by the enclosing
10147    /// command's leading word (grammar-dispatch §9).
10148    ///
10149    /// The Tcl-only rows are the constructs this grammar models with
10150    /// no node of its own (#467, #1264) — `switch` and `for` — plus the
10151    /// braced `switch` *pattern* that must not be mistaken for an arm
10152    /// body, in both the one-arm-per-line and the one-line layouts.
10153    /// Their iRules counterparts are dedicated nodes and are covered by
10154    /// the sibling test.
10155    #[cfg(feature = "tcl")]
10156    #[test]
10157    fn tcl_braced_word_role_follows_the_enclosing_command_1318() {
10158        check_braced_word_value_cases::<TclParser>(&BRACED_WORD_VALUE_CASES, "foo.tcl");
10159        let tcl_only: [BracedWordValueCase; 8] = [
10160            // `switch` arm bodies. The grammar flattens `pat body pat
10161            // body` into a `command` named after the first pattern, so
10162            // the bodies read as arguments of a command called `a` —
10163            // the shape the value default would call a literal. The
10164            // arm list's own `switch` name is what rescues them, and
10165            // `puts` occurring in both bodies is what separates n2
10166            // from N2.
10167            BracedWordValueCase {
10168                source: "switch $v { a { puts A } b { puts B } }\n",
10169                counts: [1, 3, 7, 8],
10170                operands: &["switch", "$v", "a", "b", "puts", "A", "B"],
10171            },
10172            // …and the limit of that rescue. A braced *pattern* is the
10173            // arm command's `name`, not an argument, so it is a
10174            // literal — `switch -regexp $v { {^a.*b$} {…} }` is
10175            // idiomatic and its regex must not be wrapped in a block.
10176            // Rescuing the whole arm command would read N1 4 here.
10177            // The leading `-exact` / `--` options are `simple_word`
10178            // operands of the *`switch`* command and shift nothing:
10179            // the arm list is found as the sole `braced_word` argument,
10180            // not by index (grammar-dispatch §9).
10181            BracedWordValueCase {
10182                source: "switch -exact -- $v {\n    {a b}   { puts X }\n    default { puts Y }\n}\n",
10183                counts: [1, 3, 10, 11],
10184                operands: &[
10185                    "switch", "-exact", "--", "$v", "a", "b", "puts", "X", "default", "Y",
10186                ],
10187            },
10188            // The same arms on one line. Only the *first* pattern is
10189            // the arm command's name; `{^b}` is its third argument, and
10190            // the name test alone rescued it as a body — N1 4 here,
10191            // against N1 3 for the identical arms written one per line.
10192            // The arm list is `pattern body pattern body …` by Tcl's
10193            // own definition, so the arguments alternate and the
10194            // even-indexed ones are the bodies.
10195            BracedWordValueCase {
10196                source: "switch -regexp $v { {^a} { puts A } {^b} { puts B } }\n",
10197                counts: [1, 3, 8, 9],
10198                operands: &["switch", "-regexp", "$v", "^a", "puts", "A", "^b", "B"],
10199            },
10200            // The words that can interpose between a pattern and its
10201            // body take one slot each and leave the parity intact: a
10202            // `-` fall-through body and a `default` pattern are both
10203            // `simple_word`s. The two braced bodies are at indexes 2
10204            // and 4 of the run and both keep their `{}`.
10205            BracedWordValueCase {
10206                source: "switch $v { a - b { puts AB } default { puts D } }\n",
10207                counts: [1, 3, 9, 10],
10208                operands: &["switch", "$v", "a", "-", "b", "puts", "AB", "default", "D"],
10209            },
10210            // `for` has four braced arguments and all four are
10211            // evaluated, so all four keep their braces. `i` appears as
10212            // the `set` target and again inside `incr`, and `$i` in
10213            // the condition and the body, which is what separates
10214            // n2 = 8 from N2 = 10. `<` is an *operand* here rather
10215            // than a comparison operator: with no `for` rule there is
10216            // no `expr` slot, so the condition parses as a command and
10217            // its comparison never surfaces as a token — the same
10218            // grammar limitation #1264 recorded for ABC.
10219            BracedWordValueCase {
10220                source: "for {set i 0} {$i < 3} {incr i} { puts $i }\n",
10221                counts: [2, 5, 8, 10],
10222                operands: &["for", "i", "0", "$i", "<", "3", "incr", "puts"],
10223            },
10224            // #1382's guard rows: an arm *pattern* spelled like a
10225            // command whose value slots the rule now withdraws. An arm
10226            // list parses as commands, so the pattern becomes a real
10227            // command name and the arm *body* lands in one of that
10228            // command's value slots — where only `Getter::is_switch_arm`
10229            // keeps it a block. Both rows are `switch` bodies that must
10230            // keep their `{}`, and both were measured with the guard
10231            // short-circuited to `false`.
10232            //
10233            // This one is the positional half: `after`'s slot 0 is the
10234            // millisecond count, so without the guard `{ puts B }` reads
10235            // as that value and the row drops to N1 1.
10236            BracedWordValueCase {
10237                source: "switch $v { after { puts B } }\n",
10238                counts: [1, 2, 5, 5],
10239                operands: &["switch", "$v", "after", "puts", "B"],
10240            },
10241            // …and this is the field-keyed half. `proc` needs three
10242            // words, so the arm patterns must be written one per line
10243            // for the grammar to build a `procedure` at all — on one
10244            // line it recovers into an `ERROR` and no rule fires. Here
10245            // it swallows two arms, taking `{ puts A }` as its `name`
10246            // field and `{ puts B }` as its body; without the guard the
10247            // name's brace goes and the row reads N1 5. The `namespace`
10248            // arm is the third spelling of the same hazard and is not
10249            // discriminating: its subcommand slot holds the braced body
10250            // itself rather than a `simple_word`, so no layout resolves
10251            // and the construct-wide script answer stands either way.
10252            // `proc` and `namespace` are operators here and `after` an
10253            // operand, which is the three patterns' whole difference.
10254            BracedWordValueCase {
10255                source: "switch $v {\n    proc { puts A }\n    after { puts B }\n    namespace { puts C }\n}\n",
10256                counts: [3, 6, 7, 9],
10257                operands: &["switch", "$v", "after", "puts", "A", "B", "C"],
10258            },
10259            // The arity guard on `ScriptSlots::LastOf`, Tcl-only
10260            // because the iRules grammar models `trap_handler` and so
10261            // never reaches the generic-command path (#1382 review).
10262            // Tcl models neither `on` nor `trap`, so the whole tail
10263            // after the `try` body parses as *one* `trap` command with
10264            // five arguments — `{p} {v} {puts b} finally {puts c}`.
10265            // Reading "the script is the last argument" off that names
10266            // the `finally` body and calls `{puts b}` a value, which
10267            // dropped the `{}` of a block the line does contain and
10268            // read N1 3. All five braces are blocks here, which is the
10269            // answer a list the signature cannot explain has to get.
10270            // The `trap` row in the shared table is the same clause at
10271            // its documented arity, where two of the three braces
10272            // really are values — so a guard stuck on "always script"
10273            // fails there and one stuck on "always last" fails here.
10274            BracedWordValueCase {
10275                source: "try {puts a} trap {p} {v} {puts b} finally {puts c}\n",
10276                counts: [2, 6, 8, 10],
10277                operands: &["a", "b", "c", "finally", "p", "puts", "trap", "v"],
10278            },
10279        ];
10280        check_braced_word_value_cases::<TclParser>(&tcl_only, "foo.tcl");
10281    }
10282
10283    /// The iRules twin of the test above. The shared table runs
10284    /// unchanged — the two getters are deliberate clones and #1318
10285    /// names both — and the dialect rows cover the handler bodies,
10286    /// which have no Tcl spelling.
10287    #[cfg(feature = "irules")]
10288    #[test]
10289    fn irules_braced_word_role_follows_the_enclosing_command_1318() {
10290        check_braced_word_value_cases::<IrulesParser>(&BRACED_WORD_VALUE_CASES, "foo.irule");
10291        let irules_only: [BracedWordValueCase; 4] = [
10292            // A `when` handler body is a modelled slot, so the literal
10293            // inside it is classified independently of the block that
10294            // holds it.
10295            BracedWordValueCase {
10296                source: "when HTTP_REQUEST {\n    lappend b {x y}\n}\n",
10297                counts: [2, 2, 5, 5],
10298                operands: &["HTTP_REQUEST", "lappend", "b", "x", "y"],
10299            },
10300            // This grammar models `switch` with `switch_arm` children,
10301            // so the arm body fills a modelled slot and never reaches
10302            // the command-name list the Tcl rows need.
10303            BracedWordValueCase {
10304                source: "when HTTP_REQUEST {\n    switch $v { a { pool p1 } }\n}\n",
10305                counts: [3, 5, 5, 5],
10306                operands: &["HTTP_REQUEST", "$v", "a", "pool", "p1"],
10307            },
10308            // `on` and `trap` are why the command list carries two
10309            // words no *Tcl* command spells. The grammar emits
10310            // `on_handler` / `trap_handler` only under `try` — pinned
10311            // by `irules_try_handler_kinds_appear_only_under_try` — so
10312            // a statement-level handler is a generic command, and
10313            // without the list entry its body would lose the `{}` that
10314            // the second occurrence here counts.
10315            BracedWordValueCase {
10316                source: "when RULE_INIT { set x 1 }\non error { log local0. oops }\n",
10317                counts: [3, 4, 8, 8],
10318                operands: &[
10319                    "RULE_INIT",
10320                    "x",
10321                    "1",
10322                    "on",
10323                    "error",
10324                    "log",
10325                    "local0.",
10326                    "oops",
10327                ],
10328            },
10329            // #1382 inside a modelled handler slot: the `when` body is
10330            // a block and keeps its `{}`, and the `after` delay nested
10331            // in it is a value and does not. Before #1382 this read
10332            // N1 4. The shared table's `after` pair already runs for
10333            // this dialect; what this row adds is that the rule reaches
10334            // a command nested inside a handler, where the `{}` billed
10335            // for the handler could otherwise mask a withdrawal.
10336            BracedWordValueCase {
10337                source: "when HTTP_REQUEST {\n    after {100} {puts hi}\n}\n",
10338                counts: [2, 3, 5, 5],
10339                operands: &["HTTP_REQUEST", "after", "100", "puts", "hi"],
10340            },
10341        ];
10342        check_braced_word_value_cases::<IrulesParser>(&irules_only, "foo.irule");
10343    }
10344
10345    #[cfg(feature = "php")]
10346    #[test]
10347    fn php_operators_and_operands() {
10348        check_metrics::<PhpParser>(
10349            "<?php
10350            function avg(int $a, int $b, int $c): int {
10351                return ($a + $b + $c) / 3;
10352            }",
10353            "foo.php",
10354            |metric| {
10355                // After #695 only the opening delimiters count: `()` and
10356                // `{}` fold to one operator each per balanced pair, so the
10357                // former `)`/`}` closers no longer inflate n1/N1 (was
10358                // 11 unique / 15 total).
10359                //
10360                // Operands after #1293, tallied by `get_id` (source bytes):
10361                //   `avg` × 1, `int` × 4 (the `primitive_type` wrapper at
10362                //   all four type positions — its `int` keyword child is
10363                //   suppressed under it), `$a` / `$b` / `$c` × 2 each,
10364                //   `3` × 1 ⇒ n2 = 6, N2 = 12. Between #1259 and #1293 the
10365                //   keyword leaf doubled the type count ⇒ 6 / 16; before
10366                //   #1259 each `$v` also contributed its sigil-less `name`
10367                //   leaf ⇒ 9 / 22.
10368                assert_eq!(metric.halstead.unique_operators(), 9);
10369                assert_eq!(metric.halstead.total_operators(), 12);
10370                assert_eq!(metric.halstead.unique_operands(), 6);
10371                assert_eq!(metric.halstead.total_operands(), 12);
10372                insta::assert_json_snapshot!(metric.halstead);
10373            },
10374        );
10375    }
10376
10377    #[cfg(feature = "php")]
10378    #[test]
10379    fn php_simple_function() {
10380        check_metrics::<PhpParser>(
10381            "<?php
10382            function inc(int $x): int { return $x + 1; }",
10383            "foo.php",
10384            |metric| {
10385                // After #695 only opening delimiters count: the `)`/`}`
10386                // closers no longer add operators (was 9 unique / 9 total).
10387                //
10388                // Operands after #1293: `inc` × 1, `int` × 2 (the
10389                // `primitive_type` wrapper at both type positions, its
10390                // `int` keyword child suppressed under it), `$x` × 2,
10391                // `1` × 1 ⇒ n2 = 4, N2 = 6. Between #1259 and #1293 the
10392                // keyword leaf doubled the type count ⇒ 4 / 8; before
10393                // #1259 `$x` also contributed its `x` leaf twice ⇒ 5 / 10.
10394                assert_eq!(metric.halstead.unique_operators(), 7);
10395                assert_eq!(metric.halstead.total_operators(), 7);
10396                assert_eq!(metric.halstead.unique_operands(), 4);
10397                assert_eq!(metric.halstead.total_operands(), 6);
10398                insta::assert_json_snapshot!(metric.halstead);
10399            },
10400        );
10401    }
10402
10403    #[cfg(feature = "php")]
10404    #[test]
10405    fn php_variable_reference_counts_once() {
10406        // Regression: issue #1259. `$x` parses as a `variable_name`
10407        // wrapping a `name` leaf, and both kinds were in the operand
10408        // arm — so every variable reference contributed twice to N2 and
10409        // planted a sigil-less twin (`x` beside `$x`) in the n2
10410        // vocabulary. Since `$var` is the most common token class in
10411        // PHP, that roughly doubled N2 for real files.
10412        //
10413        // Source: the issue's reproducer plus a re-reference of `$a` and
10414        // `$b`, so N2 exceeds n2 and the assertions can tell "counted
10415        // once per occurrence" from "deduplicated into the vocabulary".
10416        //   <?php $a = null; $b = true; $c = NULL; $a = $b;
10417        //
10418        // Operands by text key: `$a` × 2, `$b` × 2, `$c`, `null`, `true`,
10419        // `NULL` ⇒ n2 = 6, N2 = 8. Before the fix the `a` / `b` / `c`
10420        // leaves added 3 unique and 5 occurrences ⇒ 9 / 13.
10421        check_metrics::<PhpParser>(
10422            "<?php\n$a = null;\n$b = true;\n$c = NULL;\n$a = $b;\n",
10423            "foo.php",
10424            |metric| {
10425                assert_eq!(metric.halstead.unique_operands(), 6);
10426                assert_eq!(metric.halstead.total_operands(), 8);
10427            },
10428        );
10429    }
10430
10431    #[cfg(feature = "php")]
10432    #[test]
10433    fn php_dynamic_variable_name_counts_once_at_any_depth() {
10434        // Regression: issue #1259. Variable-variable syntax nests the
10435        // wrappers, so the double count compounds: `$$a` is a
10436        // `dynamic_variable_name` → `variable_name` → `name` chain that
10437        // scored 3 for one reference, and `$$$b` scored 4. Only the
10438        // outermost wrapper may count.
10439        //
10440        // Source: <?php $$a = 1; $$$b = 2; ${$c} = 3; $$a = 4;
10441        // The trailing re-assignment repeats `$$a` so N2 exceeds n2 and
10442        // the assertions can tell "counted once per occurrence" from
10443        // "deduplicated into the vocabulary".
10444        //
10445        // Operands: `$$a` × 2, `$$$b`, `${$c}`, `1`, `2`, `3`, `4`
10446        // ⇒ n2 = 7, N2 = 8. Before the fix: 14 / 17 (measured), each
10447        // target contributing its whole nesting chain — `$$a` → `$a` →
10448        // `a` is 3 (twice over), `$$$b` → `$$b` → `$b` → `b` is 4, and
10449        // `${$c}` → `$c` → `c` is 3, plus the four integers.
10450        check_metrics::<PhpParser>(
10451            "<?php $$a = 1; $$$b = 2; ${$c} = 3; $$a = 4;",
10452            "foo.php",
10453            |metric| {
10454                assert_eq!(metric.halstead.unique_operands(), 7);
10455                assert_eq!(metric.halstead.total_operands(), 8);
10456            },
10457        );
10458    }
10459
10460    #[cfg(feature = "php")]
10461    #[test]
10462    fn php_dynamic_variable_name_guard_is_parent_scoped() {
10463        // Companion to the two tests above (#1259): the guards fire on
10464        // the *parent* kind, never on the kind alone, so the two
10465        // positions where a nested node is a reference in its own right
10466        // keep counting.
10467        //
10468        // Source: <?php $y = "brace ${z} end"; $s = ${$a . 'b'};
10469        //
10470        // `"${z}"` is a `dynamic_variable_name` whose `name` child is
10471        // suppressed (the wrapper `${z}` carries the reference), while
10472        // `${$a . 'b'}` reaches its `$a` through a `binary_expression`,
10473        // so that `variable_name`'s parent is not a
10474        // `dynamic_variable_name` and it counts normally.
10475        //
10476        // Operands: `$y`, `${z}`, `$s`, `${$a . 'b'}`, `$a`, `'b'` — one
10477        // each ⇒ n2 = 6, N2 = 6. A guard written as a blanket kind
10478        // exclusion instead of a parent check would drop `$a` ⇒ 5 / 5.
10479        check_metrics::<PhpParser>(
10480            "<?php $y = \"brace ${z} end\"; $s = ${$a . 'b'};",
10481            "foo.php",
10482            |metric| {
10483                assert_eq!(metric.halstead.unique_operands(), 6);
10484                assert_eq!(metric.halstead.total_operands(), 6);
10485            },
10486        );
10487    }
10488
10489    #[cfg(feature = "php")]
10490    #[test]
10491    fn php_type_wrappers_count_the_type_once() {
10492        // Regression: issue #1293. A parameter type nests wrapper nodes
10493        // whose text spans the node below them — `primitive_type` around
10494        // the `int` keyword token, `named_type` around a `name`,
10495        // `optional_type` around either — and every level was in the
10496        // operand arm, so `int` scored 2 and `?int` scored 3.
10497        //
10498        // Source: the issue's first reproducer.
10499        //   <?php
10500        //   function f(int $a, bool $b, float $c, string $d, array $e,
10501        //              Foo $g): ?int { return 0; }
10502        //
10503        // Operands by text key: `f`, the five `primitive_type` parameter
10504        // types, `Foo` (the `name` under its `named_type`), `$a`..`$g`,
10505        // the return `int`, and `0`. `int` occurs twice (parameter and
10506        // return) ⇒ n2 = 14, N2 = 15. Before the fix: 15 / 23 — the
10507        // extra vocabulary entry being `?int`, which the `?` operator
10508        // already accounts for.
10509        check_metrics::<PhpParser>(
10510            "<?php\nfunction f(int $a, bool $b, float $c, string $d, \
10511             array $e, Foo $g): ?int { return 0; }\n",
10512            "foo.php",
10513            |metric| {
10514                assert_eq!(metric.halstead.unique_operands(), 14);
10515                assert_eq!(metric.halstead.total_operands(), 15);
10516            },
10517        );
10518    }
10519
10520    #[cfg(feature = "php")]
10521    #[test]
10522    fn php_qualified_name_counts_its_components_once() {
10523        // Regression: issue #1293. `Foo\Bar\Baz` parses as
10524        // `qualified_name` → `namespace_name` → `name` × N, and all
10525        // three kinds were operands, so one three-part path scored 5 and
10526        // planted `Foo`, `Foo\Bar` and `Foo\Bar\Baz` in the vocabulary.
10527        // The components carry the operand and `\` stays an operator,
10528        // matching how PHP's own `::` and `->` already read here.
10529        //
10530        // Source: the issue's second reproducer.
10531        //   <?php
10532        //   namespace App\Sub;
10533        //   use Foo\Bar\Baz;
10534        //   $o = new \Vendor\Pkg\Thing();
10535        //
10536        // Operands: `App`, `Sub`, `Foo`, `Bar`, `Baz`, `$o`, `Vendor`,
10537        // `Pkg`, `Thing` — one each ⇒ n2 = 9, N2 = 9. Before the fix:
10538        // 14 / 14.
10539        check_metrics::<PhpParser>(
10540            "<?php\nnamespace App\\Sub;\nuse Foo\\Bar\\Baz;\n\
10541             $o = new \\Vendor\\Pkg\\Thing();\n",
10542            "foo.php",
10543            |metric| {
10544                assert_eq!(metric.halstead.unique_operands(), 9);
10545                assert_eq!(metric.halstead.total_operands(), 9);
10546            },
10547        );
10548    }
10549
10550    #[cfg(feature = "php")]
10551    #[test]
10552    fn php_nested_type_wrappers_count_once_at_any_depth() {
10553        // Companion to the two tests above (#1293): the type and
10554        // qualified-name wrappers compose, so a single annotation can
10555        // stack five levels — `?A\B` is `optional_type` → `named_type` →
10556        // `qualified_name` → `namespace_name` → `name`, which scored 6
10557        // operands for two identifiers. `union_type` and
10558        // `intersection_type` stack the same way over their members;
10559        // their `|` and `&` are already operators.
10560        //
10561        // Source:
10562        //   <?php function k(?A\B $p, int|string $q, C&D $r): ?A\B
10563        //   { return 0; }
10564        // The return type repeats `?A\B` so N2 exceeds n2 and the
10565        // assertions can tell "counted once per occurrence" from
10566        // "deduplicated into the vocabulary".
10567        //
10568        // Operands: `k`, `A` × 2, `B` × 2, `$p`, `int`, `string`, `$q`,
10569        // `C`, `D`, `$r`, `0` ⇒ n2 = 11, N2 = 13.
10570        check_metrics::<PhpParser>(
10571            "<?php function k(?A\\B $p, int|string $q, C&D $r): ?A\\B { return 0; }",
10572            "foo.php",
10573            |metric| {
10574                assert_eq!(metric.halstead.unique_operands(), 11);
10575                assert_eq!(metric.halstead.total_operands(), 13);
10576            },
10577        );
10578    }
10579
10580    #[cfg(feature = "php")]
10581    #[test]
10582    fn php_childless_primitive_types_still_count() {
10583        // Guards the direction of the #1293 fix for `primitive_type`,
10584        // where — unlike the qualified-name wrappers — the *wrapper*
10585        // carries the operand and the keyword leaf is suppressed. The
10586        // grammar emits no token node under `primitive_type` for
10587        // `callable`, `iterable`, `mixed`, `void`, `false` or `true`
10588        // (verified with `bca dump`), so the other direction would score
10589        // those six types zero — grammar-dispatch §6.
10590        //
10591        // Source:
10592        //   <?php function q(callable $a, iterable $b, mixed $c,
10593        //                    false $d, true $e): void { }
10594        //
10595        // Operands: `q`, `callable`, `iterable`, `mixed`, `false`,
10596        // `true`, `void`, `$a`..`$e` ⇒ n2 = 12, N2 = 12. Dropping
10597        // `PrimitiveType` from the operand arm instead of gating its
10598        // leaf gives 6 / 6.
10599        check_metrics::<PhpParser>(
10600            "<?php function q(callable $a, iterable $b, mixed $c, \
10601             false $d, true $e): void { }",
10602            "foo.php",
10603            |metric| {
10604                assert_eq!(metric.halstead.unique_operands(), 12);
10605                assert_eq!(metric.halstead.total_operands(), 12);
10606            },
10607        );
10608    }
10609
10610    #[cfg(feature = "php")]
10611    #[test]
10612    fn php_primitive_type_keyword_guard_is_parent_scoped() {
10613        // Companion to the test above (#1293): the keyword suppression
10614        // fires on the *parent* kind, never on the kind alone.
10615        // `array` is also the head token of an `array(…)` literal, where
10616        // it is the construct's only operand and must keep counting; a
10617        // `(int)` / `(string)` cast is a childless `cast_type` that
10618        // never reaches the guard at all.
10619        //
10620        // Source: <?php $x = array(1, 2); $y = (int) $x; $z = (string) $x;
10621        //
10622        // Operands: `$x` × 3, `array`, `1`, `2`, `$y`, `int`, `$z`,
10623        // `string` ⇒ n2 = 8, N2 = 10. A blanket kind exclusion instead
10624        // of a parent check would drop the `array` head ⇒ 7 / 9.
10625        check_metrics::<PhpParser>(
10626            "<?php $x = array(1, 2); $y = (int) $x; $z = (string) $x;",
10627            "foo.php",
10628            |metric| {
10629                assert_eq!(metric.halstead.unique_operands(), 8);
10630                assert_eq!(metric.halstead.total_operands(), 10);
10631            },
10632        );
10633    }
10634
10635    #[cfg(feature = "php")]
10636    #[test]
10637    fn php_encapsed_string_interpolation_no_double_count() {
10638        // Regression: issue #184. A PHP `"Hello $name!"` used to be
10639        // classified as a Halstead operand (the wrapping
10640        // `encapsed_string`) AND have its inner `variable_name`
10641        // (`$name`) plus the inner `name` token classified as
10642        // operands too. With the fix, the wrapping literal drops to
10643        // `Unknown` when it carries any `$var` / `${name}` / `{$expr}`
10644        // child, so `$name` is counted exactly once at each text
10645        // occurrence.
10646        //
10647        // Source:
10648        //   <?php $name = "world"; echo "Hello $name!";
10649        //
10650        // Inert operand: `"world"` (no interpolation, still operand).
10651        // Operands by text key (`get_id` keys by source bytes):
10652        //   `$name` × 2 (assignment LHS and `$name` inside the
10653        //   interpolated string), `"world"` × 1.
10654        // u_operands = 2, N2 = 3.
10655        // Without the #184 fix the wrapping `"Hello $name!"` would also
10656        // count → 3 / 4. This test additionally pinned the *inner* `name`
10657        // leaf of each `variable_name` (a further 2 occurrences, 1 unique
10658        // ⇒ the historical 3 / 5) until #1259 recognised that as the same
10659        // double count one level down.
10660        check_metrics::<PhpParser>(
10661            "<?php $name = \"world\"; echo \"Hello $name!\";",
10662            "foo.php",
10663            |metric| {
10664                assert_eq!(metric.halstead.unique_operands(), 2);
10665                assert_eq!(metric.halstead.total_operands(), 3);
10666            },
10667        );
10668    }
10669
10670    #[cfg(feature = "php")]
10671    #[test]
10672    fn php_encapsed_string_no_interpolation_still_operand() {
10673        // The fix for #184 only drops `EncapsedString`/`Heredoc` from
10674        // the operand arm when interpolation is present. An inert
10675        // double-quoted string must still count as exactly one
10676        // operand, identical to the single-quoted equivalent.
10677        //
10678        // Source: `<?php echo "Hello world!";`
10679        // Operands: `"Hello world!"` × 1 → u_operands = 1, N2 = 1.
10680        check_metrics::<PhpParser>("<?php echo \"Hello world!\";", "foo.php", |metric| {
10681            assert_eq!(metric.halstead.unique_operands(), 1);
10682            assert_eq!(metric.halstead.total_operands(), 1);
10683        });
10684    }
10685
10686    #[cfg(feature = "php")]
10687    #[test]
10688    fn php_heredoc_interpolation_no_double_count() {
10689        // Regression: issue #184. A PHP heredoc whose body
10690        // interpolates `$name` previously counted both the wrapping
10691        // `heredoc` node and the inner `$name` as operands; the fix
10692        // drops the wrapper when its `heredoc_body` carries any
10693        // interpolation child.
10694        //
10695        // Source:
10696        //   <?php $name = "x"; echo <<<EOT
10697        //   hi $name
10698        //   EOT;
10699        //
10700        // Operands by text key: `$name` × 2, `"x"` × 1 (inert encapsed
10701        // string, still an operand). With the fix u_operands = 2,
10702        // N2 = 3. Without it the wrapping heredoc text would add one
10703        // more unique operand. The sigil-less `name` leaf inside each
10704        // `variable_name` was counted too until #1259.
10705        check_metrics::<PhpParser>(
10706            "<?php $name = \"x\"; echo <<<EOT\nhi $name\nEOT;\n",
10707            "foo.php",
10708            |metric| {
10709                assert_eq!(metric.halstead.unique_operands(), 2);
10710                assert_eq!(metric.halstead.total_operands(), 3);
10711            },
10712        );
10713    }
10714
10715    #[cfg(feature = "php")]
10716    #[test]
10717    fn php_nowdoc_unaffected() {
10718        // `Nowdoc` (single-quoted heredoc) never interpolates and is
10719        // never matched by `php_string_has_interpolation`. It must
10720        // continue counting as exactly one operand regardless of the
10721        // text inside, mirroring single-quoted `String`.
10722        //
10723        // Source:
10724        //   <?php echo <<<'EOT'
10725        //   plain $name not interpolated
10726        //   EOT;
10727        //
10728        // Operands: the nowdoc literal × 1 → u_operands = 1, N2 = 1.
10729        check_metrics::<PhpParser>(
10730            "<?php echo <<<'EOT'\nplain $name not interpolated\nEOT;\n",
10731            "foo.php",
10732            |metric| {
10733                assert_eq!(metric.halstead.unique_operands(), 1);
10734                assert_eq!(metric.halstead.total_operands(), 1);
10735            },
10736        );
10737    }
10738
10739    #[cfg(feature = "php")]
10740    #[test]
10741    fn php_encapsed_string_bare_member_access_no_double_count() {
10742        // Regression: issue #184 follow-up. The PHP grammar allows
10743        // bare `$obj->prop` interpolation inside `"…"` without
10744        // surrounding `{ … }`; tree-sitter-php emits this as a
10745        // direct `member_access_expression` child of
10746        // `encapsed_string` (kind_id 329 in the current grammar).
10747        // The wrapper must drop to `Unknown` for that form too —
10748        // otherwise the inner `$obj` and `prop` `name` tokens are
10749        // walked as operands while the wrapper also counts,
10750        // double-counting `N2`.
10751        //
10752        // Source:
10753        //   <?php $obj = new stdClass; $obj->prop = "x"; echo "Hi $obj->prop!";
10754        //
10755        // Operands tallied by `get_id` (keyed on source bytes):
10756        //   `$obj`        × 3 (LHS assignment, member-access target,
10757        //                      inside the interpolated string)
10758        //   `prop` (name) × 2 (member-access RHS twice — a bare `name`
10759        //                      outside any `variable_name`, so #1259's
10760        //                      guard leaves it an operand)
10761        //   `stdClass`    × 1
10762        //   `"x"`         × 1
10763        // ⇒ u_operands = 4, N2 = 7.
10764        // With the bug the wrapping `"Hi $obj->prop!"` text adds one
10765        // more unique operand and one more occurrence ⇒ 5 / 8.
10766        check_metrics::<PhpParser>(
10767            "<?php $obj = new stdClass; $obj->prop = \"x\"; echo \"Hi $obj->prop!\";",
10768            "foo.php",
10769            |metric| {
10770                assert_eq!(metric.halstead.unique_operands(), 4);
10771                assert_eq!(metric.halstead.total_operands(), 7);
10772            },
10773        );
10774    }
10775
10776    #[cfg(feature = "php")]
10777    #[test]
10778    fn php_encapsed_string_bare_subscript_no_double_count() {
10779        // Regression: issue #184 follow-up. Bare `$arr[0]` inside
10780        // `"…"` produces a `subscript_expression` child of
10781        // `encapsed_string` (kind_id 351). The wrapper must drop to
10782        // `Unknown` for that form.
10783        //
10784        // Source:
10785        //   <?php $arr = [1]; echo "Hi $arr[0]!";
10786        //
10787        // Operands tallied by `get_id`:
10788        //   `$arr` × 2, `1` × 1, `0` × 1.
10789        // ⇒ u_operands = 3, N2 = 4.
10790        // With the bug the wrapping `"Hi $arr[0]!"` text adds 1 / 1.
10791        // The inner `arr` leaf of each `variable_name` added a further
10792        // 1 / 2 until #1259.
10793        check_metrics::<PhpParser>(
10794            "<?php $arr = [1]; echo \"Hi $arr[0]!\";",
10795            "foo.php",
10796            |metric| {
10797                assert_eq!(metric.halstead.unique_operands(), 3);
10798                assert_eq!(metric.halstead.total_operands(), 4);
10799            },
10800        );
10801    }
10802
10803    #[cfg(feature = "php")]
10804    #[test]
10805    fn php_shell_command_expression_inert_is_operand() {
10806        // Regression: issue #288. Backtick command literals (PHP's
10807        // `shell_command_expression`) were filtered as strings by
10808        // `Checker::is_string` and `Alterator::alterate`, but never
10809        // classified as Halstead operands — so they contributed
10810        // nothing to N2 / eta2. An inert backtick literal must now
10811        // count as exactly one operand, matching `EncapsedString`
10812        // and `Heredoc`.
10813        //
10814        // Source: `<?php $out = ` + backtick `ls` + backtick + `;`
10815        // Operands tallied by `get_id`:
10816        //   `$out` × 1, backtick literal × 1.
10817        // ⇒ u_operands = 2, N2 = 2.
10818        // Before the fix the backtick literal vanished from the count
10819        // ⇒ u_operands = 1, N2 = 1. (The inner `out` leaf of the
10820        // `variable_name` added another 1 / 1 until #1259.)
10821        check_metrics::<PhpParser>("<?php $out = `ls`;", "foo.php", |metric| {
10822            assert_eq!(metric.halstead.unique_operands(), 2);
10823            assert_eq!(metric.halstead.total_operands(), 2);
10824        });
10825    }
10826
10827    #[cfg(feature = "php")]
10828    #[test]
10829    fn php_shell_command_expression_interpolation_no_double_count() {
10830        // Regression: issue #288. PHP backtick literals DO support
10831        // `$var` interpolation (see tree-sitter-php node-types.json:
10832        // `shell_command_expression` children include `variable_name`,
10833        // `dynamic_variable_name`, `member_access_expression`,
10834        // `subscript_expression`). With the fix the wrapper drops to
10835        // `Unknown` when it carries any interpolation child, exactly
10836        // as `EncapsedString` does.
10837        //
10838        // Source: `<?php $dir = "/tmp"; $out = ` + backtick `ls $dir` +
10839        //   backtick + `;`
10840        //
10841        // Operands tallied by `get_id`:
10842        //   `$dir` × 2 (assignment LHS, inside backticks),
10843        //   `$out` × 1, `"/tmp"` × 1.
10844        // ⇒ u_operands = 3, N2 = 4.
10845        // Without the interpolation guard the wrapping backtick literal
10846        // would also count ⇒ u_operands = 4, N2 = 5. The sigil-less
10847        // `dir` / `out` leaves added a further 2 / 3 until #1259.
10848        check_metrics::<PhpParser>(
10849            "<?php $dir = \"/tmp\"; $out = `ls $dir`;",
10850            "foo.php",
10851            |metric| {
10852                assert_eq!(metric.halstead.unique_operands(), 3);
10853                assert_eq!(metric.halstead.total_operands(), 4);
10854            },
10855        );
10856    }
10857
10858    #[cfg(feature = "php")]
10859    #[test]
10860    fn php_interpolation_opener_is_not_an_operator() {
10861        // Regression: issue #1314. `Php::LBRACE` is *both* the
10862        // compound-statement brace and the complex-interpolation
10863        // opener, so `"dq {$y} end"` reported a `{}` operator — and
10864        // reported it against the same vocabulary entry a real block
10865        // uses, which no other language does.
10866        //
10867        // expected: operators `=` × 2, `;` × 2 → n1 = 2, N1 = 4.
10868        // Operands `$s`, `$t`, `$y` × 2 → n2 = 3, N2 = 4. Before the
10869        // guard the two openers added `{}` → n1 = 3, N1 = 6.
10870        check_metrics::<PhpParser>(
10871            "<?php\n$s = \"dq {$y} end\";\n$t = \"dq {$y} end\";\n",
10872            "foo.php",
10873            |metric| {
10874                assert_eq!(metric.halstead.unique_operators(), 2);
10875                assert_eq!(metric.halstead.total_operators(), 4);
10876                assert_eq!(metric.halstead.unique_operands(), 3);
10877                assert_eq!(metric.halstead.total_operands(), 4);
10878            },
10879        );
10880    }
10881
10882    #[cfg(feature = "php")]
10883    #[test]
10884    fn php_interpolation_opener_guard_covers_every_wrapper() {
10885        // The opener is a direct child of four distinct parents, and
10886        // each is an independent leg of the guard (grammar-dispatch
10887        // section 11) — a fixture covering only `encapsed_string`
10888        // leaves the other three dead.
10889        //
10890        // One row per parent, so a failure names the leg that broke.
10891        // The heredoc's brace hangs off `heredoc_body` rather than
10892        // `heredoc`; the backtick form is `shell_command_expression`;
10893        // and the bare `${$y}` variable-variable is a
10894        // `dynamic_variable_name`, the one position that is not inside
10895        // a string at all.
10896        //
10897        // expected per row: operators `=` × 2, `;` × 2 → n1 = 2,
10898        // N1 = 4; three distinct operands with one repeated → n2 = 3,
10899        // N2 = 4.
10900        for (label, source) in [
10901            (
10902                "encapsed_string",
10903                "<?php\n$s = \"dq {$y} end\";\n$t = \"dq {$y} end\";\n",
10904            ),
10905            (
10906                "heredoc_body",
10907                "<?php\n$h = <<<EOT\na {$y} b\nEOT;\n$i = <<<EOT\na {$y} b\nEOT;\n",
10908            ),
10909            (
10910                "shell_command_expression",
10911                "<?php\n$b = `ls {$y}`;\n$c = `ls {$y}`;\n",
10912            ),
10913            ("dynamic_variable_name", "<?php\n$q = ${$y};\n$r = ${$y};\n"),
10914        ] {
10915            assert_halstead_counts::<PhpParser>(source, "foo.php", [2, 4, 3, 4], label);
10916        }
10917    }
10918
10919    #[cfg(feature = "php")]
10920    #[test]
10921    fn php_every_interpolation_spelling_scores_alike() {
10922        // The policy stated as a test (#1314). PHP writes one
10923        // interpolation three ways; the choice is spelling, so all
10924        // three must score identically. Before the guard the two
10925        // braced forms reported a `{}` the bare `$y` form did not.
10926        //
10927        // `"${y}"` is deprecated as of PHP 8.2 and removed in 9.0, but
10928        // the pinned grammar still parses it and it is still in the
10929        // wild, so it stays a row here.
10930        //
10931        // The fixture deliberately omits a `$y = …` declaration: with
10932        // one, the bare and `{$y}` forms key their operand as `$y` and
10933        // collapse into the declaration's entry while `${y}` keys as
10934        // `${y}` and does not, so n2 would differ for a reason that has
10935        // nothing to do with this guard.
10936        //
10937        // expected per spelling: operators `=` × 2, `;` × 2 → n1 = 2,
10938        // N1 = 4; operands `$s`, `$t`, the interpolated reference × 2
10939        // → n2 = 3, N2 = 4.
10940        for literal in ["\"a $y b\"", "\"a {$y} b\"", "\"a ${y} b\""] {
10941            assert_halstead_counts::<PhpParser>(
10942                &format!("<?php\n$s = {literal};\n$t = {literal};\n"),
10943                "foo.php",
10944                [2, 4, 3, 4],
10945                &format!("interpolation {literal}"),
10946            );
10947        }
10948    }
10949
10950    #[cfg(feature = "php")]
10951    #[test]
10952    fn php_compound_statement_brace_still_counts() {
10953        // Control for #1314: the guard is scoped to the four
10954        // interpolating wrappers, so a real block keeps its `{}`. A
10955        // guard widened to every `LBRACE` would take `{}` out of the
10956        // operator set entirely and fail here.
10957        //
10958        // expected: operators `function`, `()`, `{}` × 2, `if`,
10959        // `return`, `;` → n1 = 6, N1 = 8. Operands `f`, `1`, `2` →
10960        // n2 = N2 = 3.
10961        check_metrics::<PhpParser>(
10962            "<?php\nfunction f() { if (1) { return 2; } }\n",
10963            "foo.php",
10964            |metric| {
10965                assert_eq!(metric.halstead.unique_operators(), 6);
10966                assert_eq!(metric.halstead.total_operators(), 8);
10967                assert_eq!(metric.halstead.unique_operands(), 3);
10968                assert_eq!(metric.halstead.total_operands(), 3);
10969            },
10970        );
10971    }
10972
10973    #[cfg(feature = "php")]
10974    #[test]
10975    fn php_interpolation_guard_is_parent_scoped_not_ancestor_scoped() {
10976        // The input that separates the parent-scoped guard from the
10977        // ancestor-scanning mutant — the mutant #1256's post-mortem
10978        // says survives every ordinary fixture. PHP is one of only two
10979        // languages in #1314 where such an input exists: a closure
10980        // inside a complex interpolation puts a *compound-statement*
10981        // brace under an `encapsed_string` ancestor while its parent is
10982        // the `compound_statement`. An ancestor scan swallows it.
10983        //
10984        // expected: operators `=`, `;` × 2, `->`, `()` × 2, `function`,
10985        // `{}`, `return` → n1 = 7, N1 = 9. Operands `$s`, `$o`, `m`,
10986        // `1` → n2 = N2 = 4. Under the ancestor-scoped mutant the
10987        // closure's brace vanishes: n1 = 6, N1 = 8.
10988        check_metrics::<PhpParser>(
10989            "<?php\n$s = \"{$o->m(function() { return 1; })}\";\n",
10990            "foo.php",
10991            |metric| {
10992                assert_eq!(metric.halstead.unique_operators(), 7);
10993                assert_eq!(metric.halstead.total_operators(), 9);
10994                assert_eq!(metric.halstead.unique_operands(), 4);
10995                assert_eq!(metric.halstead.total_operands(), 4);
10996            },
10997        );
10998    }
10999
11000    #[cfg(feature = "php")]
11001    #[test]
11002    fn php_promoted_asymmetric_visibility_scores_like_a_property() {
11003        // tree-sitter-php 0.24.2 parsed `private(set)` on a promoted
11004        // parameter as ERROR and billed `private` and `set` as operands.
11005        // 0.25.1 parses it as the `visibility_modifier` a property
11006        // declaration already gets, so it now scores the same way.
11007        //
11008        // expected: operators `class`, `{}` × 2, `public` × 2, `function`,
11009        // `()` × 2 (parameters, `(set)`), `private` → n1 = 6, N1 = 9.
11010        // Operands `A`, `__construct`, `int`, `$x` → n2 = N2 = 4.
11011        check_metrics::<PhpParser>(
11012            "<?php\nclass A {\n    public function __construct(public private(set) int $x) {}\n}\n",
11013            "foo.php",
11014            |metric| {
11015                assert_eq!(metric.halstead.unique_operators(), 6);
11016                assert_eq!(metric.halstead.total_operators(), 9);
11017                assert_eq!(metric.halstead.unique_operands(), 4);
11018                assert_eq!(metric.halstead.total_operands(), 4);
11019            },
11020        );
11021    }
11022
11023    #[cfg(feature = "elixir")]
11024    #[test]
11025    fn elixir_operators_and_operands() {
11026        // Exercises every Halstead family classified in Elixir's
11027        // `get_op_type`: control-flow keywords (`do`, `end`, `fn`),
11028        // structural punctuation — only the *opening* delimiters `(`,
11029        // `[` count after #695 (the `)`/`]` closers were dropped), plus
11030        // `,`, `.`, `@`,
11031        // arithmetic (`+`, `-`, `*`, `/`), comparison (`==`, `>`),
11032        // logical (`&&`, `||`, `and`, `or`, `!`), pipe (`|>`), capture
11033        // (`&`), assignment/match (`=`), and the stab arrow (`->`).
11034        // The body mixes identifiers, integers, atoms, and a string.
11035        check_metrics::<ElixirParser>(
11036            "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",
11037            "foo.ex",
11038            |metric| {
11039                // Positive headline assertions on integer counts. After
11040                // #695 only opening delimiters count: the `)`/`]` closers
11041                // no longer add operators (was 15 unique / 23 total).
11042                assert_eq!(metric.halstead.unique_operators(), 13);
11043                assert_eq!(metric.halstead.total_operators(), 21);
11044                assert_eq!(metric.halstead.unique_operands(), 16);
11045                assert_eq!(metric.halstead.total_operands(), 27);
11046                insta::assert_json_snapshot!(
11047                    metric.halstead,
11048                    @r#"
11049                {
11050                  "unique_operators": 13,
11051                  "total_operators": 21,
11052                  "unique_operands": 16,
11053                  "total_operands": 27,
11054                  "length": 48,
11055                  "estimated_program_length": 112.10571633583419,
11056                  "purity_ratio": 2.3355357569965456,
11057                  "vocabulary": 29,
11058                  "volume": 233.18308776612344,
11059                  "difficulty": 10.96875,
11060                  "level": 0.09116809116809117,
11061                  "effort": 2557.7269939346666,
11062                  "time": 142.09594410748147,
11063                  "bugs": 0.062342115670886794
11064                }
11065                "#
11066                );
11067            },
11068        );
11069    }
11070
11071    #[cfg(feature = "ruby")]
11072    #[test]
11073    fn ruby_operators_and_operands() {
11074        // A small Ruby method exercising operators (def/if/end keyword
11075        // tokens, `+`, `==`, `<=`, structural punctuation) and operands
11076        // (`n`, `1`, `factorial`). Anchors the unique/total counts on
11077        // both sides and snapshots the full Halstead derivation.
11078        //
11079        // Lesson 4 invariants: u_operators / u_operands here equal the
11080        // dedupe lengths the `--ops` accessor would emit on the same
11081        // source. Any future grammar bump that adds an aliased kind_id
11082        // to either side will trip this without snapshot drift.
11083        check_metrics::<RubyParser>(
11084            "def factorial(n)\n  return 1 if n <= 1\n  n * factorial(n - 1)\nend\n",
11085            "foo.rb",
11086            |metric| {
11087                // After #695 only the `(` opener counts (folded `()`); the
11088                // `)` closer — which appeared twice across the two calls —
11089                // no longer adds an operator (was 9 unique / 11 total).
11090                assert_eq!(metric.halstead.unique_operators(), 8);
11091                assert_eq!(metric.halstead.total_operators(), 9);
11092                assert_eq!(metric.halstead.unique_operands(), 3);
11093                assert_eq!(metric.halstead.total_operands(), 9);
11094                insta::assert_json_snapshot!(metric.halstead);
11095            },
11096        );
11097    }
11098
11099    #[cfg(feature = "ruby")]
11100    #[test]
11101    fn ruby_halstead_plain_string_operand() {
11102        // A bare string literal contributes exactly one operand. The
11103        // counterpart to `ruby_halstead_interpolated_string_no_double_count`
11104        // — verifies the "no interpolation" branch of the same arm
11105        // (see `get_op_type`'s `R::String | …` case in
11106        // `big-code-analysis-ast/src/getter.rs`).
11107        // expected: operators = {def, end} = 2; operands = {f, "hello"} = 2.
11108        check_metrics::<RubyParser>("def f\n  \"hello\"\nend\n", "foo.rb", |metric| {
11109            assert_eq!(metric.halstead.unique_operators(), 2);
11110            assert_eq!(metric.halstead.total_operators(), 2);
11111            assert_eq!(metric.halstead.unique_operands(), 2);
11112            assert_eq!(metric.halstead.total_operands(), 2);
11113        });
11114    }
11115
11116    #[cfg(feature = "ruby")]
11117    #[test]
11118    fn ruby_halstead_interpolated_string_no_double_count() {
11119        // Regression mirror for #180 (Bash) / #183 (C#): when a Ruby
11120        // string literal carries an `Interpolation` child, the
11121        // wrapping `String` node is intentionally classified as
11122        // `Unknown` so the inner expression's identifiers are not
11123        // double-counted as operands.
11124        //
11125        // expected: for `def f(name)\n  "Hi #{name}"\nend\n` —
11126        //   operators: def, (, ), #{, }, end → u_operators = 6.
11127        //   operands: f, name (param), name (inside `#{name}`). The
11128        //   wrapping `"…#{name}"` literal is skipped by the
11129        //   `is_child(R::Interpolation)` guard; the operand store
11130        //   keys by token text so the two `name` occurrences dedupe
11131        //   into one distinct entry → u_operands = 2, operands = 3
11132        //   (`f` once, `name` twice).
11133        // Without the guard, the wrapping literal would also count,
11134        // inflating u_operands to 3 and operands to 4.
11135        check_metrics::<RubyParser>("def f(name)\n  \"Hi #{name}\"\nend\n", "foo.rb", |metric| {
11136            assert_eq!(metric.halstead.unique_operands(), 2);
11137            assert_eq!(metric.halstead.total_operands(), 3);
11138        });
11139    }
11140
11141    #[cfg(feature = "ruby")]
11142    #[test]
11143    fn ruby_halstead_symbol_literal_operand() {
11144        // `:foo` is a `SimpleSymbol` leaf — counts as a single
11145        // operand, no interpolation guard needed (only
11146        // `DelimitedSymbol` (`:"…#{x}…"`) can interpolate).
11147        // expected: operators = {def, end} = 2; operands = {f, :ok} = 2.
11148        check_metrics::<RubyParser>("def f\n  :ok\nend\n", "foo.rb", |metric| {
11149            assert_eq!(metric.halstead.unique_operators(), 2);
11150            assert_eq!(metric.halstead.unique_operands(), 2);
11151        });
11152    }
11153
11154    #[cfg(feature = "ruby")]
11155    #[test]
11156    fn ruby_halstead_regex_operand() {
11157        // `/foo/` parses as a `Regex` node — one operand. Its two
11158        // `SLASH` delimiters used to fall through to the shared
11159        // arithmetic arm and add a `/` operator that is nowhere in the
11160        // source; #1312 parent-guards them to `Unknown`.
11161        // expected: u_operators = {def, (, =~, end} = 4, N1 = 4 (only
11162        // the `(` opener counts after #695 — the `)` closer was
11163        // dropped; was 5 with the fabricated `/`); u_operands =
11164        // {f, s, /foo/} = 3, N2 = 4 (`s` twice: parameter and use).
11165        check_metrics::<RubyParser>("def f(s)\n  s =~ /foo/\nend\n", "foo.rb", |metric| {
11166            assert_eq!(metric.halstead.unique_operators(), 4);
11167            assert_eq!(metric.halstead.total_operators(), 4);
11168            assert_eq!(metric.halstead.unique_operands(), 3);
11169            assert_eq!(metric.halstead.total_operands(), 4);
11170        });
11171    }
11172
11173    #[cfg(feature = "ruby")]
11174    #[test]
11175    fn ruby_regex_delimiters_are_not_operators() {
11176        // Regression: issue #1312, the Ruby sibling of Elixir #1256.
11177        // Both of a `Regex` literal's delimiter tokens are `SLASH` —
11178        // the same kind id real division uses — so `x = /abc/`
11179        // reported a `/` operator with no division in the source.
11180        //
11181        // expected: operators `=` → n1 = N1 = 1. Operands `x` and the
11182        // `/abc/` literal → n2 = N2 = 2. Before the guard the two
11183        // delimiters added `/` → n1 = 2, N1 = 3.
11184        check_metrics::<RubyParser>("x = /abc/\n", "foo.rb", |metric| {
11185            assert_eq!(metric.halstead.unique_operators(), 1);
11186            assert_eq!(metric.halstead.total_operators(), 1);
11187            assert_eq!(metric.halstead.unique_operands(), 2);
11188            assert_eq!(metric.halstead.total_operands(), 2);
11189        });
11190    }
11191
11192    #[cfg(feature = "ruby")]
11193    #[test]
11194    fn ruby_regex_delimiter_choice_is_invariant() {
11195        // Companion to the test above (#1312): `%r`-form regexes are
11196        // the same literal spelled differently, so every delimiter
11197        // choice must produce identical counts. tree-sitter-ruby
11198        // aliases all of them to `SLASH` — verified with `bca dump`,
11199        // which shows `%r{`/`}`, `%r(`/`)`, `%r[`/`]`, `%r<`/`>`,
11200        // `%r|`/`|` and `%r!`/`!` every one emitting kind `SLASH` —
11201        // so each row here genuinely exercises the guard rather than
11202        // reaching a different, already-clean path.
11203        //
11204        // expected per variant: operator `=` → n1 = N1 = 1; operands
11205        // `x` and the literal → n2 = N2 = 2.
11206        for literal in [
11207            "/abc/", "%r{abc}", "%r(abc)", "%r[abc]", "%r<abc>", "%r|abc|", "%r!abc!",
11208        ] {
11209            assert_halstead_counts::<RubyParser>(
11210                &format!("x = {literal}\n"),
11211                "foo.rb",
11212                [1, 1, 2, 2],
11213                &format!("regex literal {literal}"),
11214            );
11215        }
11216    }
11217
11218    #[cfg(feature = "ruby")]
11219    #[test]
11220    fn ruby_division_survives_the_regex_guard() {
11221        // Control for #1312: the guard is scoped to a `Regex` parent,
11222        // so real division still counts. Two divisions, so a mutant
11223        // that collapsed repeated hits would move `N1` even though
11224        // `n1` held (the #1294 count-only-anchor lesson).
11225        //
11226        // expected: operators `=` and `/` × 2 → n1 = 2, N1 = 3.
11227        // Operands `z`, `a`, `b`, `c` → n2 = N2 = 4.
11228        check_metrics::<RubyParser>("z = a / b / c\n", "foo.rb", |metric| {
11229            assert_eq!(metric.halstead.unique_operators(), 2);
11230            assert_eq!(metric.halstead.total_operators(), 3);
11231            assert_eq!(metric.halstead.unique_operands(), 4);
11232            assert_eq!(metric.halstead.total_operands(), 4);
11233        });
11234    }
11235
11236    #[cfg(feature = "ruby")]
11237    #[test]
11238    fn ruby_regex_guard_is_parent_scoped_not_ancestor_scoped() {
11239        // The one input that separates the correct parent-scoped guard
11240        // from the ancestor-scoped mutant of it (#1312, mirroring
11241        // #1256's Elixir case): a division *inside* a regex's `#{…}`
11242        // interpolation. Its `/` has `Binary` as its parent but the
11243        // `Regex` as a further ancestor, so an ancestor scan would
11244        // swallow it. Every other fixture in this file passes under
11245        // both spellings. Two interpolations, so the mutant moves both
11246        // n1 (2 → 1) and N1 (3 → 1).
11247        //
11248        // expected: operators `=`, `/` × 2 → n1 = 2, N1 = 3. Operands
11249        // `w`, `p`, `q`, `r`, `t` → n2 = N2 = 5; the wrapping `Regex`
11250        // is skipped because it carries an `Interpolation` child (the
11251        // #180 double-count guard).
11252        //
11253        // Was n1 = 3, N1 = 5 until #1314 dropped `#{` from the operator
11254        // arm. The mutant still moves both axes, so this fixture is as
11255        // discriminating as it was — it just no longer counts the two
11256        // interpolation openers alongside the two divisions.
11257        check_metrics::<RubyParser>("w = /a#{p / q}c#{r / t}b/\n", "foo.rb", |metric| {
11258            assert_eq!(metric.halstead.unique_operators(), 2);
11259            assert_eq!(metric.halstead.total_operators(), 3);
11260            assert_eq!(metric.halstead.unique_operands(), 5);
11261            assert_eq!(metric.halstead.total_operands(), 5);
11262        });
11263    }
11264
11265    #[cfg(feature = "ruby")]
11266    #[test]
11267    fn ruby_regex_start_alias_never_reaches_kind_id() {
11268        // Drift marker for the `R::SLASH2` half of #1312's guard.
11269        // `SLASH2` is the aliased regex-start token: it sits in the
11270        // enum beside the other literal-start aliases (`DQUOTE`,
11271        // `COLONDQUOTE`, `BQUOTE2`, `PERCENTwLPAREN`) and the runtime
11272        // `public_symbol_map` collapses it to `SLASH` before
11273        // `kind_id()`, exactly like `LPAREN2` in #768. It is listed in
11274        // the guard rather than the arithmetic arm because a regex
11275        // delimiter is the only thing it could ever be; this pins that
11276        // it is currently unreachable, so a grammar bump that starts
11277        // emitting it fails here instead of silently changing a metric.
11278        let path = PathBuf::from("foo.rb");
11279        for source in ["x = /abc/\n", "x = %r{abc}\n"] {
11280            let parser = RubyParser::new(source.as_bytes().to_vec(), &path, None);
11281            assert!(
11282                !ast_has_kind_id(&parser, Ruby::SLASH2 as u16),
11283                "Ruby::SLASH2 must stay collapsed to Ruby::SLASH for `{source}`"
11284            );
11285            // Positive control: the id the guard actually fires on is
11286            // present, so the assertion above cannot pass merely
11287            // because no delimiter was parsed at all.
11288            assert!(
11289                ast_has_kind_id(&parser, Ruby::SLASH as u16),
11290                "Ruby::SLASH must be the delimiter kind for `{source}`"
11291            );
11292        }
11293    }
11294
11295    #[cfg(feature = "ruby")]
11296    #[test]
11297    fn ruby_subshell_delimiters_are_not_operators() {
11298        // Regression: issue #1360, the second delimiter family of the
11299        // fabrication #1312 removed for regexes. tree-sitter-ruby
11300        // aliases both ends of a subshell literal to `BQUOTE` — the
11301        // same kind id the `` def ` `` method-name marker uses — so
11302        // `` x = `echo hi` `` reported a `` ` `` operator with no
11303        // backtick method call in the source.
11304        //
11305        // expected: operator `=` → n1 = N1 = 1. Operands `x` and the
11306        // `` `echo hi` `` literal → n2 = N2 = 2. Before the guard the
11307        // two delimiters added `` ` `` → n1 = 2, N1 = 3.
11308        assert_halstead_counts::<RubyParser>(
11309            "x = `echo hi`\n",
11310            "foo.rb",
11311            [1, 1, 2, 2],
11312            "backtick subshell",
11313        );
11314    }
11315
11316    #[cfg(feature = "ruby")]
11317    #[test]
11318    fn ruby_subshell_delimiter_choice_is_invariant() {
11319        // Companion to the test above (#1360): `%x`-form subshells are
11320        // the same literal spelled differently, so every delimiter
11321        // choice must produce identical counts. tree-sitter-ruby
11322        // aliases all of them to `BQUOTE` — verified with `bca dump`,
11323        // which shows `` ` ``/`` ` ``, `%x{`/`}`, `%x(`/`)`, `%x[`/`]`,
11324        // `%x<`/`>`, `%x|`/`|` and `%x!`/`!` every one emitting kind
11325        // `BQUOTE` — so each row here genuinely exercises the guard
11326        // rather than reaching a different, already-clean path. Note
11327        // the `%x!…!` and `%x|…|` closers in particular: they are
11328        // `BQUOTE`, not the `BANG` / `PIPE` operators they look like.
11329        //
11330        // The empty row records that `` `` `` parses to a `Subshell`
11331        // holding nothing but its two delimiters, and still bills that
11332        // wrapper as one operand once both are suppressed. It is
11333        // documentation, not a discriminator: the guard deletes nothing
11334        // from the operand arm, so grammar-dispatch §6's
11335        // childless-variant-regresses-to-zero hazard cannot arise here
11336        // and this row moves with the first row under every mutant.
11337        //
11338        // expected per variant: operator `=` → n1 = N1 = 1; operands
11339        // `x` and the literal → n2 = N2 = 2.
11340        for literal in [
11341            "`echo`", "%x{echo}", "%x(echo)", "%x[echo]", "%x<echo>", "%x|echo|", "%x!echo!", "``",
11342        ] {
11343            assert_halstead_counts::<RubyParser>(
11344                &format!("x = {literal}\n"),
11345                "foo.rb",
11346                [1, 1, 2, 2],
11347                &format!("subshell literal {literal}"),
11348            );
11349        }
11350    }
11351
11352    #[cfg(feature = "ruby")]
11353    #[test]
11354    fn ruby_backtick_method_name_survives_the_subshell_guard() {
11355        // Control for #1360, and the reason the kind is gated rather
11356        // than dropped from the operator arm (grammar-dispatch §6):
11357        // `` ` `` is a legal Ruby method name. `bca dump` at the pinned
11358        // grammar shows `` def `(cmd) `` emitting a `BQUOTE` wrapped in
11359        // a named `operator` node, so its parent is `Operator` and the
11360        // parent-scoped guard leaves it alone. Deleting `BQUOTE` from
11361        // the operator arm would have scored this construct zero.
11362        //
11363        // expected: operators `def`, `` ` ``, `(` and `end` →
11364        // n1 = N1 = 4 (only the `(` opener counts after #695).
11365        // Operand `cmd` twice, parameter and use → n2 = 1, N2 = 2.
11366        assert_halstead_counts::<RubyParser>(
11367            "def `(cmd)\n  cmd\nend\n",
11368            "foo.rb",
11369            [4, 4, 1, 2],
11370            "backtick method name",
11371        );
11372    }
11373
11374    #[cfg(feature = "ruby")]
11375    #[test]
11376    fn ruby_subshell_guard_is_parent_scoped_not_ancestor_scoped() {
11377        // The one input that separates the correct parent-scoped guard
11378        // from the ancestor-scoped mutant of it (#1360, mirroring
11379        // #1312's regex case): backtick *methods* used inside a
11380        // subshell's `#{…}` interpolation. Their `BQUOTE` has the named
11381        // `operator` node as its parent but the `Subshell` as a further
11382        // ancestor, so an ancestor scan would swallow both. Every other
11383        // fixture in this file passes under either spelling. Two
11384        // markers — a definition and a call — so the mutant moves both
11385        // n1 (7 → 6) and N1 (10 → 8), per the #1294 count-only-anchor
11386        // lesson.
11387        //
11388        // expected: operators `=` (1), `def` (1), `` ` `` (2), `(` (2),
11389        // `;` (2), `end` (1), `.` (1) → n1 = 7, N1 = 10. Operands `n`,
11390        // `c` × 2, `s`, `"x"` → n2 = 4, N2 = 5; the wrapping `Subshell`
11391        // is skipped because it carries `Interpolation` children (the
11392        // #180 double-count guard), and its `a` / `b` string content is
11393        // classified by no arm.
11394        check_metrics::<RubyParser>(
11395            "n = `a#{def `(c); c; end}b#{s.`(\"x\")}`\n",
11396            "foo.rb",
11397            |metric| {
11398                assert_eq!(metric.halstead.unique_operators(), 7);
11399                assert_eq!(metric.halstead.total_operators(), 10);
11400                assert_eq!(metric.halstead.unique_operands(), 4);
11401                assert_eq!(metric.halstead.total_operands(), 5);
11402            },
11403        );
11404    }
11405
11406    #[cfg(feature = "ruby")]
11407    #[test]
11408    fn ruby_subshell_start_alias_never_reaches_kind_id() {
11409        // Drift marker for #1360's guard, the `BQUOTE2` sibling of
11410        // `ruby_regex_start_alias_never_reaches_kind_id`. `BQUOTE2`
11411        // sits in the enum's literal-start alias block beside `DQUOTE`,
11412        // `COLONDQUOTE` and `SLASH2`, and the runtime
11413        // `public_symbol_map` collapses it to `BQUOTE` before
11414        // `kind_id()`, exactly like `LPAREN2` in #768.
11415        //
11416        // `BQUOTE2` is therefore in no dispatch arm at all — not the
11417        // guard and not the operator arm — which is the asymmetry with
11418        // `SLASH2`, a kind #1312 had to *move* out of the arithmetic
11419        // arm. Adding it to the guard would change nothing, since the
11420        // wildcard already answers `Unknown` for it. What that leaves
11421        // unguarded is the reverse direction: a bump that started
11422        // emitting `BQUOTE2` would drop a subshell opener silently
11423        // (already the wanted answer) but would also stop counting a
11424        // `` def ` `` marker spelled that way. This pins the
11425        // reachability so such a bump fails here rather than moving a
11426        // metric unobserved.
11427        let path = PathBuf::from("foo.rb");
11428        for source in ["x = `echo`\n", "x = %x{echo}\n"] {
11429            let parser = RubyParser::new(source.as_bytes().to_vec(), &path, None);
11430            assert!(
11431                !ast_has_kind_id(&parser, Ruby::BQUOTE2 as u16),
11432                "Ruby::BQUOTE2 must stay collapsed to Ruby::BQUOTE for `{source}`"
11433            );
11434            // Positive control: the id the guard actually fires on is
11435            // present, so the assertion above cannot pass merely
11436            // because no delimiter was parsed at all.
11437            assert!(
11438                ast_has_kind_id(&parser, Ruby::BQUOTE as u16),
11439                "Ruby::BQUOTE must be the delimiter kind for `{source}`"
11440            );
11441        }
11442    }
11443
11444    #[cfg(feature = "ruby")]
11445    #[test]
11446    fn ruby_interpolation_opener_is_not_an_operator() {
11447        // Behaviour change, not a fabrication fix: #1314 drops
11448        // `HASHLBRACE` from Ruby's operator arm. `#{` is a token of its
11449        // own here — unlike PHP's `{`, which aliases the
11450        // compound-statement brace — so nothing was being miscounted;
11451        // the question was whether an interpolation opener is an
11452        // operation at all, and across the five interpolating languages
11453        // three already said no. Ruby Halstead operator counts drop for
11454        // interpolated literals as a result.
11455        //
11456        // Asserted as an invariance: the interpolated and plain
11457        // spellings of one string must now score identically, which is
11458        // the policy rather than a magic number. Before the change the
11459        // interpolated row was n1 = 2, N1 = 3.
11460        //
11461        // expected per row: operator `=` × 2 → n1 = 1, N1 = 2; operands
11462        // `s`, `t`, and the literal's content contribution × 2 →
11463        // n2 = 3, N2 = 4.
11464        for literal in ["\"a #{y} b\"", "\"a b\""] {
11465            assert_halstead_counts::<RubyParser>(
11466                &format!("s = {literal}\nt = {literal}\n"),
11467                "foo.rb",
11468                [1, 2, 3, 4],
11469                &format!("literal {literal}"),
11470            );
11471        }
11472    }
11473
11474    #[cfg(feature = "ruby")]
11475    #[test]
11476    fn ruby_interpolation_opener_drop_covers_every_literal() {
11477        // `HASHLBRACE` is one arm, but it fires under every Ruby
11478        // literal that interpolates, so the drop is not specific to
11479        // double-quoted strings. A symbol and a regex — two literals
11480        // whose `#{…}` reaches the same token — must contribute no
11481        // operator for the opener either.
11482        //
11483        // expected: operator `=` × 3 → n1 = 1, N1 = 3. Operands `y`,
11484        // `1`, `a`, `b`, plus the two interpolated `y` references →
11485        // n2 = 4, N2 = 6. Before the change each `#{` added one →
11486        // n1 = 2, N1 = 5.
11487        check_metrics::<RubyParser>("y = 1\na = :\"s#{y}\"\nb = /r#{y}/\n", "foo.rb", |metric| {
11488            assert_eq!(metric.halstead.unique_operators(), 1);
11489            assert_eq!(metric.halstead.total_operators(), 3);
11490            assert_eq!(metric.halstead.unique_operands(), 4);
11491            assert_eq!(metric.halstead.total_operands(), 6);
11492        });
11493    }
11494
11495    #[cfg(feature = "ruby")]
11496    #[test]
11497    fn ruby_element_containers_count_elements_not_the_composite_1353() {
11498        // #1353. `chained_string`, `string_array` and `symbol_array`
11499        // hold *classified operand* children instead of the raw
11500        // `string_content` every other string-like literal wraps, so
11501        // the arm's shared `Interpolation` guard never fired and the
11502        // wrapper was billed alongside each element. Every row below
11503        // scored n2 4 / N2 4 for three operands, the extra entry being
11504        // the wrapper's whole-span text — which made the vocabulary
11505        // depend on how the author grouped the literals.
11506        //
11507        // `assert_ops_operands` pins the operand *text*, not just the
11508        // count: a wrapper coming back would show up as `"one" "two"` /
11509        // `%w[x y]` / `%i[p q]` rather than as an off-by-one number.
11510        //
11511        // The kind assertions are the grammar-dispatch §1 / §2 drift
11512        // marker — a bump that renumbers a wrapper or an element would
11513        // otherwise leave these counts passing while measuring a
11514        // construct the arm no longer names.
11515        //
11516        // Two of the three rows also pin the arm's *membership*:
11517        // dropping `StringArray` or `SymbolArray` from it fails
11518        // `ruby_empty_word_and_symbol_arrays_still_bill_one_operand_1353`.
11519        // `ChainedString`'s membership is unobservable and no test can
11520        // pin it — `repeat1($.string)` guarantees the guard always
11521        // fires, so `Unknown` and "not in the arm at all" are the same
11522        // answer for every input. It is listed for symmetry with its
11523        // siblings, and correct-by-construction is the only defence
11524        // available there.
11525        for (source, wrapper, element, operands) in [
11526            (
11527                "a = \"one\" \"two\"\n",
11528                Ruby::ChainedString,
11529                Ruby::String,
11530                vec!["a", "\"one\"", "\"two\""],
11531            ),
11532            (
11533                "b = %w[x y]\n",
11534                Ruby::StringArray,
11535                Ruby::BareString,
11536                vec!["b", "x", "y"],
11537            ),
11538            (
11539                "c = %i[p q]\n",
11540                Ruby::SymbolArray,
11541                Ruby::BareSymbol,
11542                vec!["c", "p", "q"],
11543            ),
11544        ] {
11545            let parser =
11546                RubyParser::new(source.as_bytes().to_vec(), &PathBuf::from("foo.rb"), None);
11547            assert!(
11548                ast_has_kind_id(&parser, wrapper as u16),
11549                "the container kind this arm gates on is unreachable for `{source}`"
11550            );
11551            assert!(
11552                ast_has_kind_id(&parser, element as u16),
11553                "the element kind this arm gates on is unreachable for `{source}`"
11554            );
11555
11556            // expected: operator `=` → n1 = N1 = 1; operands are the
11557            // assignment target and the two elements → n2 = N2 = 3.
11558            assert_halstead_counts::<RubyParser>(source, "foo.rb", [1, 1, 3, 3], source);
11559            assert_ops_operands::<RubyParser>(source, "foo.rb", 3, operands);
11560        }
11561    }
11562
11563    #[cfg(feature = "ruby")]
11564    #[test]
11565    fn ruby_empty_word_and_symbol_arrays_still_bill_one_operand_1353() {
11566        // The childless spelling, and the whole reason #1353 gates the
11567        // three container kinds instead of dropping them from the arm
11568        // (grammar-dispatch §6). `%w[]` / `%i[]` parse to a wrapper
11569        // holding nothing but its two delimiter tokens, so the tempting
11570        // "just delete the wrapper" fix — the one #1351 was right to
11571        // take for Bash's `command_name` — would score an empty literal
11572        // zero operands where the source plainly has a literal. This is
11573        // the assertion to watch fail against that alternative.
11574        //
11575        // expected: operator `=` → n1 = N1 = 1; operands the assignment
11576        // target and the empty literal itself → n2 = N2 = 2.
11577        for (source, operands) in [
11578            ("d = %w[]\n", vec!["d", "%w[]"]),
11579            ("e = %i[]\n", vec!["e", "%i[]"]),
11580        ] {
11581            assert_halstead_counts::<RubyParser>(source, "foo.rb", [1, 1, 2, 2], source);
11582            assert_ops_operands::<RubyParser>(source, "foo.rb", 2, operands);
11583        }
11584    }
11585
11586    #[cfg(feature = "ruby")]
11587    #[test]
11588    fn ruby_interpolated_array_elements_are_not_double_counted_1353() {
11589        // `bare_string` and `bare_symbol` are two aliases of a single
11590        // grammar production (`_literal_contents`), one per array form,
11591        // so `%W[…]` and `%I[…]` must score identically. Until #1353
11592        // only `bare_string` carried the interpolation guard and
11593        // `bare_symbol` sat in the plain operand arm, so `%I[a#{n}b c]`
11594        // billed the element `a#{n}b` *and* the `n` inside it (n2 4)
11595        // where `%W[a#{n}b c]` billed 3.
11596        //
11597        // Asserted as an invariance over the two spellings, the way
11598        // `ruby_interpolation_opener_is_not_an_operator` is — the
11599        // policy is the claim, not the magic number. The third row is
11600        // the composed case: a `chained_string` whose own guard defers
11601        // to an element that is itself interpolated.
11602        //
11603        // expected per row: operator `=` → n1 = N1 = 1; operands the
11604        // assignment target, the interpolated expression `n`, and the
11605        // one inert element → n2 = N2 = 3. The operand *text* is pinned
11606        // too, because 3 is also the count an implementation that
11607        // suppressed the inert element instead of the wrapper would
11608        // report.
11609        for (source, operands) in [
11610            ("w = %W[a#{n}b c]\n", vec!["w", "n", "c"]),
11611            ("w = %I[a#{n}b c]\n", vec!["w", "n", "c"]),
11612            ("a = \"x#{n}\" \"y\"\n", vec!["a", "n", "\"y\""]),
11613        ] {
11614            assert_halstead_counts::<RubyParser>(source, "foo.rb", [1, 1, 3, 3], source);
11615            assert_ops_operands::<RubyParser>(source, "foo.rb", 3, operands);
11616        }
11617    }
11618
11619    #[cfg(feature = "ruby")]
11620    #[test]
11621    fn ruby_suffixed_numeric_literals_bill_one_operand_1359() {
11622        // #1359, the wrapper/leaf double count one arm below #1353's.
11623        // `rational` and `complex` wrap the numeral they suffix, and
11624        // `get_op_type` billed every level: `a = 1r` scored n2 3 / N2 3
11625        // for two operands, and `c = 3ri` — a `complex` over a
11626        // `rational` over an `integer` — scored n2 3 / N2 4 for the
11627        // same two.
11628        //
11629        // The rows are ordered by nesting depth on purpose, because the
11630        // half-fix is a plausible one to write. Guard the two leaf
11631        // kinds and not the intermediate `rational` — `R::Integer |
11632        // R::Float` where the arm says `R::Integer | R::Float |
11633        // R::Rational` — and the `1r` and `2i` rows still pass; the
11634        // `ri` rows, whose middle `rational` must be suppressed as well
11635        // as the leaf beneath it, are the two that fail (measured:
11636        // `c = 3ri` reports n2 3 / N2 3).
11637        //
11638        // The trailing rows are the ones that must *not* move. `g = 4`
11639        // has no wrapper at all and pins that the guard is scoped to
11640        // the numerals inside one (grammar-dispatch §6 — nothing
11641        // regresses to zero). `"#{4r}"` and `-3r` put the wrapper
11642        // somewhere other than an assignment right-hand side, under a
11643        // node that guards its own children (`Interpolation`, whose
11644        // enclosing `String` is suppressed by the arm above) and under
11645        // one that does not (`unary`); both must still bill the
11646        // wrapper once. They do *not* pin parent-versus-ancestor
11647        // scoping and nothing can: `complex` and `rational` admit
11648        // numeral children only, so an ancestor-scoped guard would
11649        // classify every Ruby input identically. And `1r + 1r` is the
11650        // row where N2 and n2 come apart, so the `N2` column is
11651        // load-bearing on its own rather than tracking `n2`.
11652        //
11653        // The kind assertions are the grammar-dispatch §1 / §2 drift
11654        // marker: a bump that renumbered `complex` or `rational`, or
11655        // stopped nesting them, would leave the counts passing while
11656        // measuring a construct the guard no longer names. `IntOrFloat`
11657        // is the §2 half — the hidden `_int_or_float` the two wrappers
11658        // are written over, which a bump could promote into the
11659        // numeral's parent and silently stop the guard firing. Not
11660        // every kind a row mentions is one the guard names
11661        // (`Interpolation` is context), and `unary` and `binary` are
11662        // deliberately absent: they carry five and three aliased
11663        // variants here, so naming one would pin the alias rather than
11664        // the shape.
11665        for (source, kinds, counts, operands) in [
11666            (
11667                "a = 1r\n",
11668                &[Ruby::Rational as u16, Ruby::Integer as u16][..],
11669                [1, 1, 2, 2],
11670                vec!["a", "1r"],
11671            ),
11672            (
11673                "b = 2i\n",
11674                &[Ruby::Complex as u16, Ruby::Integer as u16][..],
11675                [1, 1, 2, 2],
11676                vec!["b", "2i"],
11677            ),
11678            (
11679                "c = 3ri\n",
11680                &[
11681                    Ruby::Complex as u16,
11682                    Ruby::Rational as u16,
11683                    Ruby::Integer as u16,
11684                ][..],
11685                [1, 1, 2, 2],
11686                vec!["c", "3ri"],
11687            ),
11688            (
11689                "d = 1.5r\n",
11690                &[Ruby::Rational as u16, Ruby::Float as u16][..],
11691                [1, 1, 2, 2],
11692                vec!["d", "1.5r"],
11693            ),
11694            (
11695                "e = 2.5i\n",
11696                &[Ruby::Complex as u16, Ruby::Float as u16][..],
11697                [1, 1, 2, 2],
11698                vec!["e", "2.5i"],
11699            ),
11700            (
11701                "f = 1.5ri\n",
11702                &[
11703                    Ruby::Complex as u16,
11704                    Ruby::Rational as u16,
11705                    Ruby::Float as u16,
11706                ][..],
11707                [1, 1, 2, 2],
11708                vec!["f", "1.5ri"],
11709            ),
11710            (
11711                "g = 4\n",
11712                &[Ruby::Integer as u16][..],
11713                [1, 1, 2, 2],
11714                vec!["g", "4"],
11715            ),
11716            (
11717                "h = \"#{4r}\"\n",
11718                &[Ruby::Interpolation as u16, Ruby::Rational as u16][..],
11719                [1, 1, 2, 2],
11720                vec!["h", "4r"],
11721            ),
11722            (
11723                "i = -3r\n",
11724                &[Ruby::Rational as u16, Ruby::Integer as u16][..],
11725                [2, 2, 2, 2],
11726                vec!["i", "3r"],
11727            ),
11728            (
11729                "j = 1r + 1r\n",
11730                &[Ruby::Rational as u16, Ruby::Integer as u16][..],
11731                [2, 2, 2, 3],
11732                vec!["j", "1r"],
11733            ),
11734        ] {
11735            let parser =
11736                RubyParser::new(source.as_bytes().to_vec(), &PathBuf::from("foo.rb"), None);
11737            for kind in kinds {
11738                assert!(
11739                    ast_has_kind_id(&parser, *kind),
11740                    "the kind this row measures is unreachable for `{source}`"
11741                );
11742            }
11743            assert!(
11744                !ast_has_kind_id(&parser, Ruby::IntOrFloat as u16),
11745                "`_int_or_float` is hidden; a grammar that emits it would \
11746                 sit between the wrapper and the numeral for `{source}`"
11747            );
11748
11749            assert_halstead_counts::<RubyParser>(source, "foo.rb", counts, source);
11750            assert_ops_operands::<RubyParser>(source, "foo.rb", operands.len(), operands);
11751        }
11752    }
11753
11754    #[cfg(feature = "ruby")]
11755    #[test]
11756    fn ruby_numeric_suffixes_stay_distinct_operands_1359() {
11757        // The reason #1359 keeps the *wrapper* and gates the leaf
11758        // rather than dropping the two wrapper kinds from the arm, as
11759        // #1351 and #1358 did for their Bash wrappers. `1`, `1r`, `1i`
11760        // and `1ri` are four distinct Ruby constants — Integer,
11761        // Rational, Complex and Complex-of-Rational — and only the
11762        // wrapper's span carries the suffix that says which.
11763        //
11764        // This is the assertion to watch fail against the leaf-keeping
11765        // alternative: billing the `integer` would file all four under
11766        // the operand text `1`, reporting n2 5 / N2 8 where the source
11767        // plainly has eight distinct operands. The count alone does not
11768        // say that, so the vocabulary is pinned by text.
11769        //
11770        // expected: operator `=` → n1 1, N1 4; four targets and four
11771        // constants, none repeated → n2 = N2 = 8.
11772        let source = "w = 1\nx = 1r\ny = 1i\nz = 1ri\n";
11773        assert_halstead_counts::<RubyParser>(source, "foo.rb", [1, 4, 8, 8], source);
11774        assert_ops_operands::<RubyParser>(
11775            source,
11776            "foo.rb",
11777            8,
11778            vec!["w", "x", "y", "z", "1", "1r", "1i", "1ri"],
11779        );
11780    }
11781
11782    /// Comprehensive iRules Halstead test exercising every operator family
11783    /// classified in `get_op_type`: declaration/control keywords (`proc`,
11784    /// `set`, `if`, `return`), structural punctuation (`{}` `[]` `()`),
11785    /// arithmetic (`+`), comparison (`>`), the word-form string comparator
11786    /// (`eq`), and short-circuit logical (`&&`). Anchored on the integer
11787    /// `n1`/`N1`/`n2`/`N2` headline values; the float fields are derived and
11788    /// bit-brittle, so they are not pinned.
11789    ///
11790    /// The second half pins the lesson-4 invariant: the independent
11791    /// text-keyed `operands_and_operators` store must dedupe to the same
11792    /// `n1`/`n2`. A classification change that moved one store without the
11793    /// other (e.g. a kind landing in both the operator and operand arms)
11794    /// would break this even though the snapshot stayed green.
11795    #[cfg(feature = "irules")]
11796    #[test]
11797    fn irules_operators_and_operands() {
11798        let source = "proc f { a b } {
11799    set x [expr { $a + $b }]
11800    if { $x > 0 && $a eq \"go\" } {
11801        return $x
11802    }
11803    return 0
11804}
11805";
11806        check_metrics::<IrulesParser>(source, "foo.irule", |metric| {
11807            // After #695 only opening delimiters count: the `}`/`]`
11808            // closers no longer add operators (was 12 unique / 20 total).
11809            assert_eq!(metric.halstead.unique_operators(), 10);
11810            assert_eq!(metric.halstead.total_operators(), 14);
11811            // Operands fell 12 / 16 → 10 / 14 with #1354: the proc body
11812            // and the `if` body are `BracedWord` script kinds and are no
11813            // longer operands beside the commands they contain.
11814            assert_eq!(metric.halstead.unique_operands(), 10);
11815            assert_eq!(metric.halstead.total_operands(), 14);
11816        });
11817
11818        let path = PathBuf::from("foo.irule");
11819        let parser = IrulesParser::new(source.as_bytes().to_vec(), &path, None);
11820        let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
11821        let unique_operators: HashSet<&str> = ops.operators.iter().map(String::as_str).collect();
11822        let unique_operands: HashSet<&str> = ops.operands.iter().map(String::as_str).collect();
11823        assert_eq!(
11824            unique_operators.len(),
11825            10,
11826            "dedupe(ops.operators) must equal n1; operators were {:?}",
11827            ops.operators
11828        );
11829        assert_eq!(
11830            unique_operands.len(),
11831            10,
11832            "dedupe(ops.operands) must equal n2; operands were {:?}",
11833            ops.operands
11834        );
11835    }
11836
11837    /// An inert `"hello world"` double-quoted string (no `$var` / `[cmd]`
11838    /// interpolation child) contributes exactly **one** operand — the
11839    /// wrapping `QuotedWord`. Operands are `f`, `s` and `"hello world"` —
11840    /// n2 = 3, the same as Tcl since #1294 restored its `set` target to
11841    /// the operand count and #1354 dropped the proc-body `braced_word`
11842    /// from both. Mirrors `tcl_inert_quoted_word_counts_as_operand`
11843    /// (#277).
11844    #[cfg(feature = "irules")]
11845    #[test]
11846    fn irules_inert_quoted_word_counts_as_operand() {
11847        let source = "proc f {} {\n    set s \"hello world\"\n}\n";
11848        check_metrics::<IrulesParser>(source, "foo.irule", |metric| {
11849            // After #695 only the `{` opener counts; the `}` closer no
11850            // longer adds an operator (was 4 unique / 6 total).
11851            assert_eq!(metric.halstead.unique_operators(), 3);
11852            assert_eq!(metric.halstead.total_operators(), 4);
11853            assert_eq!(metric.halstead.unique_operands(), 3);
11854            assert_eq!(metric.halstead.total_operands(), 3);
11855        });
11856
11857        let path = PathBuf::from("foo.irule");
11858        let parser = IrulesParser::new(source.as_bytes().to_vec(), &path, None);
11859        let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
11860        // The inert quoted word is present as exactly one operand (not
11861        // dropped, not split): dropping it would mean the inert branch was
11862        // over-guarded.
11863        let quoted = ops
11864            .operands
11865            .iter()
11866            .filter(|o| o.as_str() == "\"hello world\"")
11867            .count();
11868        assert_eq!(quoted, 1, "inert quoted word must be one operand");
11869        let unique_operands: HashSet<&str> = ops.operands.iter().map(String::as_str).collect();
11870        assert_eq!(unique_operands.len(), 3, "operands were {:?}", ops.operands);
11871    }
11872
11873    /// Regression for the `QuotedWord` interpolation guard (the #277 /
11874    /// Bash-#180 / C#-#183 / PHP-#184 pattern). An interpolated
11875    /// `"$x is $y"` must contribute **zero** operands for the wrapping
11876    /// `QuotedWord`; the inner `$x` / `$y` `variable_substitution` nodes are
11877    /// walked separately and count on their own. Operands are `f`, `x`, `y`,
11878    /// `s`, `$x`, `$y` = 6 (7 before #1354 dropped the proc-body
11879    /// `braced_word`). If the guard regressed (wrapper classified
11880    /// `Operand`), the wrapper string would add a 7th operand. This is the
11881    /// branch that had no test before.
11882    #[cfg(feature = "irules")]
11883    #[test]
11884    fn irules_interpolated_quoted_word_no_double_count() {
11885        let source = "proc f {x y} {\n    set s \"$x is $y\"\n}\n";
11886        check_metrics::<IrulesParser>(source, "foo.irule", |metric| {
11887            // After #695 only the `{` opener counts; the `}` closer no
11888            // longer adds an operator (was 4 unique / 6 total).
11889            assert_eq!(metric.halstead.unique_operators(), 3);
11890            assert_eq!(metric.halstead.total_operators(), 4);
11891            assert_eq!(metric.halstead.unique_operands(), 6);
11892            assert_eq!(metric.halstead.total_operands(), 6);
11893        });
11894
11895        let path = PathBuf::from("foo.irule");
11896        let parser = IrulesParser::new(source.as_bytes().to_vec(), &path, None);
11897        let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
11898        // The wrapping interpolated string must NOT appear as an operand;
11899        // its inner substitutions must. The wrapper, if wrongly counted,
11900        // would surface as the quoted literal `"$x is $y"` (with quotes,
11901        // like the inert `"hello world"` operand). Match that exact token —
11902        // a substring check would false-match the proc-body `braced_word`
11903        // operand, which legitimately contains the source text.
11904        assert!(
11905            !ops.operands.iter().any(|o| o.as_str() == "\"$x is $y\""),
11906            "interpolated wrapper must not be an operand; operands were {:?}",
11907            ops.operands
11908        );
11909        assert!(
11910            ops.operands.iter().any(|o| o.as_str() == "$x")
11911                && ops.operands.iter().any(|o| o.as_str() == "$y"),
11912            "inner $x / $y substitutions must each be operands; operands were {:?}",
11913            ops.operands
11914        );
11915        let unique_operands: HashSet<&str> = ops.operands.iter().map(String::as_str).collect();
11916        assert_eq!(unique_operands.len(), 6, "operands were {:?}", ops.operands);
11917    }
11918
11919    /// Exercises the operator families not covered by
11920    /// `irules_operators_and_operands`: bitwise (`& | ^ ~ << >>`), ternary
11921    /// (`? :`), the keyword string comparators (`starts_with`, `ends_with`,
11922    /// `contains`, `matches`, `eq`, `ne`), and the keyword logical operator
11923    /// (`and`). Pins every operator-family arm in `get_op_type` plus the
11924    /// lesson-4 dedupe invariant.
11925    #[cfg(feature = "irules")]
11926    #[test]
11927    fn irules_bitwise_ternary_string_ops() {
11928        let source = "proc f { a b } {
11929    set bits [expr { $a & $b | $a ^ ~$b }]
11930    set sh [expr { $a << 2 | $b >> 1 }]
11931    set t [expr { $a > 0 ? $a : $b }]
11932    if { $a starts_with \"x\" && $b ends_with \"y\" } { return 1 }
11933    if { $a contains \"z\" || $b matches \"q\" } { return 2 }
11934    if { $a eq \"m\" and $b ne \"n\" } { return 3 }
11935    return $b
11936}
11937";
11938        check_metrics::<IrulesParser>(source, "foo.irule", |metric| {
11939            // After #695 only opening delimiters count: the `}`/`]`
11940            // closers no longer add operators (was 26 unique / 57 total).
11941            assert_eq!(metric.halstead.unique_operators(), 24);
11942            assert_eq!(metric.halstead.total_operators(), 43);
11943            // Operands fell 23 / 42 → 19 / 38 with #1354: the proc body
11944            // and the three single-statement `if` bodies are
11945            // `BracedWord` script kinds and no longer count as operands.
11946            assert_eq!(metric.halstead.unique_operands(), 19);
11947            assert_eq!(metric.halstead.total_operands(), 38);
11948        });
11949
11950        let path = PathBuf::from("foo.irule");
11951        let parser = IrulesParser::new(source.as_bytes().to_vec(), &path, None);
11952        let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
11953        let unique_operators: HashSet<&str> = ops.operators.iter().map(String::as_str).collect();
11954        let unique_operands: HashSet<&str> = ops.operands.iter().map(String::as_str).collect();
11955        assert_eq!(
11956            unique_operators.len(),
11957            24,
11958            "dedupe(ops.operators) must equal n1; operators were {:?}",
11959            ops.operators
11960        );
11961        assert_eq!(
11962            unique_operands.len(),
11963            19,
11964            "dedupe(ops.operands) must equal n2; operands were {:?}",
11965            ops.operands
11966        );
11967    }
11968
11969    /// A bare `$x` produces one `variable_substitution` operand. Its inner
11970    /// `id` leaf (the *named* `Id` node — not the anonymous `Id2` token Tcl
11971    /// has there) must NOT be counted separately, or every variable
11972    /// reference double-counts. `get_op_type` excludes `Id` whose parent is
11973    /// a `VariableSubstitution`. Operands: `f`, the proc arg `x`, `return`
11974    /// and `$x` — four, with no duplicate (`total_operands()` == 4; the
11975    /// proc-body `braced_word` was a fifth before #1354). If the guard
11976    /// regressed, the inner `id` "x" would add a fifth operand occurrence
11977    /// (it text-collides with the proc arg `x`, so `u_operands` would stay
11978    /// 4 but `total_operands()` would rise to 5 — hence the total, not just
11979    /// the unique count, is asserted).
11980    #[cfg(feature = "irules")]
11981    #[test]
11982    fn irules_array_reference_bills_the_reference_and_the_index() {
11983        // The iRules twin of
11984        // `tcl_array_reference_bills_the_reference_and_the_index`. Until
11985        // `ArrayIndex` left the operand arm this fixture billed `(k)` and
11986        // `($i)` beside the six operands below — the grammar-dispatch §5
11987        // wrapper-plus-leaf count, and a divergence from Tcl.
11988        assert_ops_operands::<IrulesParser>(
11989            "set arr(k) 1\nset z \"$arr($i)\"\n",
11990            "foo.irule",
11991            6,
11992            vec!["arr", "k", "1", "z", "$arr($i)", "$i"],
11993        );
11994    }
11995
11996    #[cfg(feature = "irules")]
11997    #[test]
11998    fn irules_bare_variable_operand() {
11999        let source = "proc f {x} {\n    return $x\n}\n";
12000        check_metrics::<IrulesParser>(source, "foo.irule", |metric| {
12001            // After #695 only the `{` opener counts (folded `{}`); the
12002            // `}` closer no longer adds an operator (was 3 unique / 5 total).
12003            assert_eq!(metric.halstead.unique_operators(), 2);
12004            assert_eq!(metric.halstead.total_operators(), 3);
12005            assert_eq!(metric.halstead.unique_operands(), 4);
12006            assert_eq!(metric.halstead.total_operands(), 4);
12007        });
12008
12009        let path = PathBuf::from("foo.irule");
12010        let parser = IrulesParser::new(source.as_bytes().to_vec(), &path, None);
12011        let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
12012        let bare_var = ops.operands.iter().filter(|o| o.as_str() == "$x").count();
12013        assert_eq!(
12014            bare_var, 1,
12015            "bare $x must be exactly one operand (inner id leaf not double-counted); operands were {:?}",
12016            ops.operands
12017        );
12018    }
12019
12020    #[cfg(feature = "irules")]
12021    #[test]
12022    fn irules_braced_word_delimiter_is_not_an_operator() {
12023        // Regression: issue #1314. The iRules twin of
12024        // `tcl_braced_word_delimiter_is_not_an_operator` — the two
12025        // getters are deliberate clones, so the guard lands in both and
12026        // is asserted in both.
12027        //
12028        // One fixture covers the guard and its control: `{braced word}`
12029        // and `{y}` are values whose openers must not count, while the
12030        // handler body, the `if` condition and the `if` body are
12031        // `BracedWord` / `Expr` and must keep theirs. A guard keyed on
12032        // the brace alone would take `{}` out of the operator set
12033        // entirely and fail here.
12034        //
12035        // expected: operators `when`, `set` × 2, `if`, `contains`,
12036        // `[]`, `{}` × 3 → n1 = 6, N1 = 9. Every operand is distinct →
12037        // n2 = N2 = 7: `HTTP_REQUEST`, `a`, `b`, `HTTP::uri`, `"x"` and
12038        // the two braced words `{braced word}` and `{y}`. Before the
12039        // guard the two value openers added two more `{}` occurrences →
12040        // N1 = 11; before #1354 the handler and `if` bodies and the
12041        // inner `braced` / `word` / `y` were operands too → n2 = 12.
12042        check_metrics::<IrulesParser>(
12043            "when HTTP_REQUEST {\n  set a {braced word}\n  if {[HTTP::uri] contains \"x\"} { set b {y} }\n}\n",
12044            "foo.irule",
12045            |metric| {
12046                assert_eq!(metric.halstead.unique_operators(), 6);
12047                assert_eq!(metric.halstead.total_operators(), 9);
12048                assert_eq!(metric.halstead.unique_operands(), 7);
12049                assert_eq!(metric.halstead.total_operands(), 7);
12050            },
12051        );
12052    }
12053
12054    /// Regression for #563: the two Halstead `Display` labels must use the
12055    /// underscore key that matches the JSON/CSV field name, so a user can grep
12056    /// the same token across `Display` and JSON. The space-separated forms
12057    /// (`estimated program length` / `purity ratio`) were the only outliers,
12058    /// mirroring the `dump` fix in #562.
12059    #[cfg(feature = "cpp")]
12060    #[test]
12061    fn display_halstead_labels_use_underscore_keys() {
12062        check_metrics::<CppParser>("int a = 42;", "foo.cpp", |metric| {
12063            let out = metric.halstead.to_string();
12064            assert!(
12065                out.contains("estimated_program_length: "),
12066                "Display must use the underscore key `estimated_program_length`:\n{out}"
12067            );
12068            assert!(
12069                out.contains("purity_ratio: "),
12070                "Display must use the underscore key `purity_ratio`:\n{out}"
12071            );
12072            assert!(
12073                !out.contains("estimated program length"),
12074                "Display must not emit the space-separated `estimated program length`:\n{out}"
12075            );
12076            assert!(
12077                !out.contains("purity ratio"),
12078                "Display must not emit the space-separated `purity ratio`:\n{out}"
12079            );
12080        });
12081    }
12082
12083    /// `@"…"` is one `string_literal` holding its `@` as a child, so the
12084    /// literal is the operand, keyed by its whole text, and the marker is
12085    /// not an operator on top — it was, which planted a phantom `@` in
12086    /// n1 for a file whose only `@` was in NSString literals and billed
12087    /// the same byte in both streams. Boxing (`@42`) keeps its `@`: there
12088    /// the token is a child of the `at_expression`, not of the literal.
12089    #[cfg(feature = "objc")]
12090    #[test]
12091    fn objc_nsstring_literal_is_one_operand() {
12092        // expected: [n1, N1, n2, N2], each operator count including the
12093        // wrapper's `@implementation` (#1552). Before the guard the first
12094        // two rows read [9, 9, 5, 5] and [9, 10, 6, 6] — one `@` operator
12095        // per literal; the boxing control is unchanged.
12096        let cases = [
12097            ("NSString *s = @\"str\";", [8, 8, 5, 5]),
12098            ("NSString *t = @\"x\" @\"y\";", [8, 8, 6, 6]),
12099            ("NSNumber *n = @42;", [9, 9, 5, 5]),
12100        ];
12101        for (body, counts) in cases {
12102            let source = format!("@implementation Foo\n- (void)m {{\n    {body}\n}}\n@end\n");
12103            assert_halstead_counts::<ObjcParser>(&source, "foo.m", counts, body);
12104        }
12105        assert_ops_operands::<ObjcParser>(
12106            "@implementation Foo\n- (void)m {\n    NSString *s = @\"str\";\n}\n@end\n",
12107            "foo.m",
12108            5,
12109            vec!["Foo", "m", "NSString", "s", "@\"str\""],
12110        );
12111    }
12112
12113    /// Comprehensive Objective-C Halstead fixture exercising a message
12114    /// send (`[self log:@"hi"]`), an ObjC string literal (`@"hi"`), an
12115    /// `if`, a short-circuit `&&`, arithmetic (`+`), comparisons, and
12116    /// assignment. Pins every field and enforces the lesson-4 invariants
12117    /// `unique_operators == n1` / `unique_operands == n2` via the
12118    /// independent `--ops` store.
12119    #[cfg(feature = "objc")]
12120    #[test]
12121    fn objc_operators_and_operands() {
12122        let source = "@implementation Foo
12123- (int)bar:(int)x {
12124    int y = x + 1;
12125    if (x > 0 && y < 10) {
12126        [self log:@\"hi\"];
12127    }
12128    return y;
12129}
12130@end
12131";
12132        check_metrics::<ObjcParser>(source, "foo.m", |metric| {
12133            // n1 = 15 unique operators:
12134            //   `&&`, `()`, `+`, `-`, `:`, `;`, `<`, `=`, `>`,
12135            //   `[]` (message send), `if`, `int`, `return`, `{}`,
12136            //   `@implementation` (#1552).
12137            //   The `@` of `@"hi"` is part of the literal's operand
12138            //   key, not an operator (see `objc_nsstring_literal_is_one_operand`).
12139            // n2 = 10 unique operands:
12140            //   `Foo`, `bar`, `log`, `self`, `x`, `y`, `0`, `1`, `10`,
12141            //   `@"hi"` (the ObjC string literal).
12142            assert_eq!(metric.halstead.unique_operators(), 15);
12143            assert_eq!(metric.halstead.unique_operands(), 10);
12144            insta::assert_json_snapshot!(metric.halstead, @r#"
12145            {
12146              "unique_operators": 15,
12147              "total_operators": 23,
12148              "unique_operands": 10,
12149              "total_operands": 14,
12150              "length": 37,
12151              "estimated_program_length": 91.82263988300141,
12152              "purity_ratio": 2.481692969810849,
12153              "vocabulary": 25,
12154              "volume": 171.8226790216648,
12155              "difficulty": 10.5,
12156              "level": 0.09523809523809523,
12157              "effort": 1804.1381297274804,
12158              "time": 100.22989609597113,
12159              "bugs": 0.049399808887691035
12160            }
12161            "#);
12162        });
12163        // Lesson-4 invariant: dedupe(ops.operands) == n2 (10), via the
12164        // independent text-keyed `--ops` store.
12165        assert_ops_operands::<ObjcParser>(
12166            source,
12167            "foo.m",
12168            10,
12169            vec![
12170                "Foo", "bar", "log", "self", "x", "y", "0", "1", "10", "@\"hi\"",
12171            ],
12172        );
12173    }
12174
12175    /// #1316 fixture separating the vocabulary and occurrence axes
12176    /// (#1294): `'x'` appears twice, so a correct fix adds four `n2`
12177    /// entries and five `N2` hits. The `'ab'` multi-character constant
12178    /// is also the grammar-dispatch section 5 pin — that literal carries
12179    /// *two* `character` children and must still bill one operand.
12180    ///
12181    /// Both #1316 fixtures are plain C, which every C-family grammar
12182    /// parses to the same shape, so one source proves the same thing
12183    /// about each of the four clones.
12184    #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12185    const C_FAMILY_CHAR_REPEATS: &str =
12186        "char a = 'x';\nchar b = 'x';\nchar c = 'y';\nchar d = '\\n';\nint e = 'ab';\n";
12187
12188    /// #1316 fixture holding all five spellings the grammars admit. Each
12189    /// opens on a distinct delimiter kind (`'`, `L'`, `u'`, `U'`, `u8'`),
12190    /// and operands key on source text, so the five are five vocabulary
12191    /// entries rather than one.
12192    #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12193    const C_FAMILY_CHAR_PREFIXES: &str =
12194        "char a = 'x';\nchar b = L'x';\nchar c = u'x';\nchar d = U'x';\nchar e = u8'x';\n";
12195
12196    /// Asserts both #1316 fixtures for one C-family language, through
12197    /// the metrics store *and* the text-keyed `--ops` store (the
12198    /// lesson-4 invariant `n2 == len(dedupe(ops.operands))`).
12199    ///
12200    /// The `--ops` half pins *which text* each literal is billed under,
12201    /// which the counts cannot see: billing a literal's `character`
12202    /// payload instead of the whole literal keeps `n2` at 9 while the
12203    /// vocabulary silently becomes `x` rather than `'x'`. (Billing
12204    /// *both* is caught earlier, by the counts.) It is a second *walk*
12205    /// rather than a second classification — `ops_inner` reads the keys
12206    /// of the same `HalsteadMaps` — which is worth knowing before
12207    /// reading it as independent corroboration of the count.
12208    #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12209    #[track_caller]
12210    fn assert_char_literal_operands<T: crate::MetricSuite>(file: &str, label: &str) {
12211        // `char` x4 and `int` are text-keyed primitive operators, so
12212        // n1 = 4 (`;`, `=`, `char`, `int`) and N1 = 5 + 5 + 4 + 1.
12213        // Operands: `a`..`e`, plus `'x'` (twice), `'y'`, `'\n'`, `'ab'`.
12214        let repeats = format!("{label}: repeated / escaped / multi-char literals");
12215        assert_halstead_counts::<T>(C_FAMILY_CHAR_REPEATS, file, [4, 15, 9, 10], &repeats);
12216        assert_ops_operands::<T>(
12217            C_FAMILY_CHAR_REPEATS,
12218            file,
12219            9,
12220            vec!["a", "b", "c", "d", "e", "'x'", "'y'", "'\\n'", "'ab'"],
12221        );
12222
12223        // Every declaration is `char` here, so the `int` primitive
12224        // operator of the other fixture is gone and n1 drops to 3. Ten
12225        // distinct operands, each seen once.
12226        let prefixes = format!("{label}: L / u / U / u8 prefixed literals");
12227        assert_halstead_counts::<T>(C_FAMILY_CHAR_PREFIXES, file, [3, 15, 10, 10], &prefixes);
12228        assert_ops_operands::<T>(
12229            C_FAMILY_CHAR_PREFIXES,
12230            file,
12231            10,
12232            vec![
12233                "a", "b", "c", "d", "e", "'x'", "L'x'", "u'x'", "U'x'", "u8'x'",
12234            ],
12235        );
12236    }
12237
12238    /// Regression for #1316: a C-family character literal is a Halstead
12239    /// operand.
12240    ///
12241    /// `char_literal` was in no arm of `CCode` / `CppCode` /
12242    /// `MozcppCode` / `ObjcCode`'s `get_op_type`, so a character literal
12243    /// contributed *nothing* — not an operator (correct) and not an
12244    /// operand (wrong) — while Rust, Java, Kotlin, C#, Go and Elixir all
12245    /// counted theirs. Both fixtures measured `n2` 5, `N2` 5 before the
12246    /// fix: the five declared identifiers and not one literal.
12247    ///
12248    /// Each language asserts separately over the same source rather than
12249    /// sharing one call, so reverting one clone's arm fails that row
12250    /// alone (grammar-dispatch section 11) — verified by perturbing each
12251    /// of the four arms in turn. A single shared assertion would be
12252    /// satisfied by whichever clone still had the arm.
12253    ///
12254    /// Mozcpp owns no file extension, so no integration snapshot ever
12255    /// reaches its clone; its row is the whole coverage that arm has.
12256    #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12257    #[test]
12258    fn c_family_char_literals_are_operands() {
12259        assert_char_literal_operands::<CParser>("chars.c", "c");
12260        assert_char_literal_operands::<CppParser>("chars.cpp", "cpp");
12261        assert_char_literal_operands::<MozcppParser>("chars.cpp", "mozcpp");
12262        assert_char_literal_operands::<ObjcParser>("chars.m", "objc");
12263    }
12264
12265    /// ObjC boxes a character literal as `@'y'` — an `at_expression`
12266    /// wrapping the same `char_literal`, with the `@` counted as its own
12267    /// operator. The wrapper is in no operand arm, so the boxed form
12268    /// bills exactly the literal it wraps and stays distinct from a bare
12269    /// one (#1316).
12270    #[cfg(feature = "objc")]
12271    #[test]
12272    fn objc_boxed_char_literal_is_one_operand() {
12273        let source = "char a = 'x';\nid b = @'y';\n";
12274        // n1: `char`, `=`, `;`, `@`. N1: 1 + 2 + 2 + 1.
12275        // n2 / N2: `a`, `b`, `'x'`, `'y'` — `id` is a `typedefed_specifier`
12276        // and is classified by neither arm. Before #1316 this was n2 2,
12277        // N2 2.
12278        assert_halstead_counts::<ObjcParser>(source, "boxed.m", [4, 6, 4, 4], "objc @'y'");
12279        assert_ops_operands::<ObjcParser>(source, "boxed.m", 4, vec!["a", "b", "'x'", "'y'"]);
12280    }
12281
12282    /// Walks both #1316 fixtures under all four C-family `Getter`s and
12283    /// pins the two grammar facts the new operand arm rests on.
12284    ///
12285    /// * **Grammar-dispatch section 1.** In the positions these
12286    ///   fixtures exercise, every node the grammar spells `char_literal`
12287    ///   carries the one `kind_id` the arm lists. That is the weaker
12288    ///   half of the alias evidence — an alias arises in a *different*
12289    ///   syntactic position, which no fixture can enumerate. The strong
12290    ///   half is that these generated enums do carry numeric-suffix
12291    ///   aliases in quantity (`language_c.rs` alone has ninety) and none
12292    ///   of the four spells a `CharLiteral2`, so its absence is a
12293    ///   measurement rather than a silence. This loop is what notices if
12294    ///   a grammar bump changes that under an existing fixture.
12295    /// * **Grammar-dispatch section 5.** No child of a `char_literal` is
12296    ///   classified. That is what makes listing the wrapper safe rather
12297    ///   than a wrapper/leaf double count: the opening delimiter, the
12298    ///   closing `'`, and the `character` / `escape_sequence` payload
12299    ///   must all stay `Unknown`, or every literal would bill two
12300    ///   operands and a prefixed one three.
12301    ///
12302    /// Both loops are non-vacuous by assertion, since a fixture that
12303    /// stopped containing a character literal would otherwise make this
12304    /// test pass having checked nothing.
12305    #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12306    #[test]
12307    fn c_family_char_literal_internals_stay_unclassified() {
12308        #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12309        fn check<L: LanguageInfo + Getter>(char_literal: u16, label: &str) {
12310            let mut literals = 0_usize;
12311            let mut children = 0_usize;
12312            for source in [C_FAMILY_CHAR_REPEATS, C_FAMILY_CHAR_PREFIXES] {
12313                for_each_node_with_chain::<L>(source.as_bytes(), |node, chain| {
12314                    if node.kind() == "char_literal" {
12315                        assert_eq!(
12316                            node.kind_id(),
12317                            char_literal,
12318                            "{label}: a `char_literal` carries kind_id {} rather than the \
12319                             {char_literal} the operand arm lists — an alias the arm cannot see",
12320                            node.kind_id()
12321                        );
12322                        literals += 1;
12323                    }
12324                    if chain
12325                        .last()
12326                        .is_none_or(|parent| parent.kind_id() != char_literal)
12327                    {
12328                        return;
12329                    }
12330                    children += 1;
12331                    // `_with_code` is the spelling `compute_halstead`
12332                    // calls. The default forwards to the byte-less form,
12333                    // so today the two agree for every C-family
12334                    // language — which is exactly why asking the wrong
12335                    // one would read as correct right up until one of
12336                    // these four grew an override (grammar-dispatch
12337                    // section 7).
12338                    assert!(
12339                        matches!(
12340                            L::get_op_type_with_code(
12341                                node,
12342                                source.as_bytes(),
12343                                Ancestors::known(chain)
12344                            ),
12345                            TokenRole::Unknown
12346                        ),
12347                        "{label}: `{}` inside a character literal is classified, so the \
12348                         literal now double-counts against its wrapper",
12349                        node.kind()
12350                    );
12351                });
12352            }
12353            // Ten literals across the two fixtures; each holds two
12354            // delimiters plus at least one payload leaf, and `'ab'` two.
12355            assert_eq!(literals, 10, "{label}: fixtures lost a character literal");
12356            assert_eq!(children, 31, "{label}: fixtures lost a literal's internals");
12357        }
12358
12359        check::<CCode>(C::CharLiteral as u16, "c");
12360        check::<CppCode>(Cpp::CharLiteral as u16, "cpp");
12361        check::<MozcppCode>(Mozcpp::CharLiteral as u16, "mozcpp");
12362        check::<ObjcCode>(Objc::CharLiteral as u16, "objc");
12363    }
12364
12365    /// #1361 fixture pairing the two spellings of a member access whose
12366    /// receiver is the enclosing object: `this->x` and `p->x`.
12367    ///
12368    /// The pairing is the point, not decoration. `field_expression` is
12369    /// `<receiver> -> <field>`, so the two lines are the same AST shape
12370    /// and must bill the same *kinds* of thing — a receiver operand, the
12371    /// `->` operator, a field operand. Before the fix `this` was in
12372    /// neither arm, so the two shapes scored differently and `m1` billed
12373    /// a `->` with only one operand around it.
12374    ///
12375    /// Plain C++ that the mozcpp fork parses identically, so one source
12376    /// proves the same thing about both clones.
12377    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
12378    const CPP_THIS_RECEIVER_PARITY: &str = "struct S {
12379         int x;
12380         int m1() { return this->x; }
12381         int m2(S* p) { return p->x; }
12382     };";
12383
12384    /// #1361 fixture holding one representative position per kind of
12385    /// container the pinned `tree-sitter-cpp` can put a `this` in: the
12386    /// `->` receiver, a dereference inside a parenthesised expression, a
12387    /// bare return value, both lambda-capture spellings (`[this]` and
12388    /// `[=, this]`), a `decltype` trailing return type, a constructor
12389    /// body following a member-initialiser list, and a call argument.
12390    ///
12391    /// Representative, not exhaustive — `delete this;`, `throw this;`,
12392    /// `[&, this]`, a default member initialiser and a `this->template
12393    /// f<int>()` all parse cleanly too. Every one of them is the same
12394    /// childless `{this:215}` leaf under an unclassified wrapper, which
12395    /// is precisely why the arm needs no guard and why enumerating them
12396    /// would add rows without adding a distinct path (grammar-dispatch
12397    /// section 11).
12398    ///
12399    /// C++23's explicit object parameter (`int g(this S&& self)`) is
12400    /// deliberately absent: the pinned grammar cannot parse it and emits
12401    /// `type_identifier "this"` plus an `ERROR`, which
12402    /// `for_each_node_with_chain` rejects outright. That spelling is
12403    /// already an operand through `TypeIdentifier`, so the construct is
12404    /// unaffected by this arm either way.
12405    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
12406    const CPP_THIS_POSITIONS: &str = "struct S {
12407         int x;
12408         void g(S*);
12409         int arrow() { return this->x; }
12410         int deref() { return (*this).x; }
12411         S* ret() { return this; }
12412         void cap() { auto l = [this]() { return x; }; }
12413         void cap2() { auto l = [=, this]() { return x; }; }
12414         auto trail() -> decltype(this->x) { return x; }
12415         S(int v) : x(v) { this->x = v; }
12416         void call() { g(this); }
12417     };";
12418
12419    /// Asserts `CPP_THIS_RECEIVER_PARITY` for one C++-family language,
12420    /// through the metrics store *and* the text-keyed `--ops` store
12421    /// (the lesson-4 invariant `n2 == len(dedupe(ops.operands))`).
12422    ///
12423    /// The `--ops` half pins *which text* the new operand is billed
12424    /// under, which the counts alone cannot see: billing the enclosing
12425    /// `field_expression` instead of the `this` leaf would hold `n2` at
12426    /// 6 while the vocabulary silently became `this->x`.
12427    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
12428    #[track_caller]
12429    fn assert_this_receiver_parity<T: crate::MetricSuite>(file: &str, label: &str) {
12430        // Operators, keyed by kind_id except the text-keyed primitives:
12431        // `{` x3 (class body, `m1`, `m2`), `int` x3, `;` x4 (the field,
12432        // the two returns, the struct terminator), `(` x2, `return` x2,
12433        // `->` x2, the one `*` of `S* p`, and `struct` (#1552). #695
12434        // dropped every closing delimiter. n1 = 8, N1 = 18.
12435        //
12436        // Operands, keyed by source text: `S` x2 (the name and the
12437        // parameter type), `x` x3, `m1`, `m2`, `p` x2, and — the fix —
12438        // `this` x1. n2 = 6, N2 = 10. Before #1361: n2 5, N2 9, with
12439        // every operator count identical, which is the whole claim.
12440        let counts = format!("{label}: this->x and p->x are the same shape");
12441        assert_halstead_counts::<T>(CPP_THIS_RECEIVER_PARITY, file, [8, 18, 6, 10], &counts);
12442        assert_ops_operands::<T>(
12443            CPP_THIS_RECEIVER_PARITY,
12444            file,
12445            6,
12446            vec!["S", "x", "m1", "m2", "p", "this"],
12447        );
12448    }
12449
12450    /// Every C++ overloaded-operator name the grammar spells as a token
12451    /// of its own, plus the `co_await` expression and the conversion
12452    /// operator's `operator` keyword (#1296).
12453    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
12454    const CPP_OPERATOR_NAMES: &str =
12455        "struct S { int operator[](int); void operator()(); bool operator co_await(); };
12456long operator\"\"_x(unsigned long long);
12457S::operator bool() const;
12458S s; int a = s[1]; auto t = co_await s;
12459";
12460
12461    /// Asserts `CPP_OPERATOR_NAMES` bills each operator name, keyword and
12462    /// `co_await` as an operator under parser `T`.
12463    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
12464    #[track_caller]
12465    fn assert_cpp_operator_names<T: crate::MetricSuite>(label: &str) {
12466        // Operators (n1 = 18, N1 = 41): `operator` 5 (four
12467        // `operator_name`s and the `operator_cast`); the names `[]` 1,
12468        // `()` 1, `""` 1; `co_await` 2 (the name and the expression);
12469        // the `(` of a parameter list 5; `;` 9; `int` 3; `long` 3;
12470        // `bool` 2; `=` 2; `{` 1; `void` 1; `unsigned` 1; `::` 1; the
12471        // subscript `[` 1; `struct` 1 (#1552); the trailing `const` 1
12472        // (#1578). `auto` is in no arm.
12473        //
12474        // Operands (n2 = 6, N2 = 9): `s` 3, `S` 2 (the qualifier in
12475        // `S::operator bool` is a namespace identifier, which #1096
12476        // leaves unbilled), `_x`, `a`, `1`, `t`.
12477        //
12478        // Before #1296: (12, 30, 6, 9) with `struct`, the five operator
12479        // kinds billed nowhere.
12480        assert_halstead_counts::<T>(CPP_OPERATOR_NAMES, "ops.cpp", [18, 41, 6, 9], label);
12481
12482        // `operator[]` / `operator()` are kinds of their own, so they sit
12483        // beside the subscript `[` and the parameter-list `(` as two more
12484        // operators, each rendered as the operator it names so that every
12485        // listed entry is distinct.
12486        let ops = ops_of::<T>(CPP_OPERATOR_NAMES, "ops.cpp");
12487        for (name, entries) in [
12488            ("operator", 1),
12489            ("co_await", 1),
12490            ("\"\"", 1),
12491            ("operator[]", 1),
12492            ("operator()", 1),
12493            ("[]", 1),
12494            ("()", 1),
12495        ] {
12496            assert_eq!(
12497                ops.operators.iter().filter(|o| *o == name).count(),
12498                entries,
12499                "{label}: `{name}` must be {entries} operator entry; operators were {:?}",
12500                ops.operators
12501            );
12502            assert!(
12503                !ops.operands.iter().any(|o| o == name),
12504                "{label}: `{name}` must not be an operand; operands were {:?}",
12505                ops.operands
12506            );
12507        }
12508    }
12509
12510    /// Regression for #1296: a C++ operator name is a Halstead operator,
12511    /// as `operator +`'s `+` already was. One row per clone, so reverting
12512    /// either arm fails its own row (grammar-dispatch section 11); Mozcpp
12513    /// owns no file extension, so this row is its only coverage.
12514    #[cfg(all(feature = "cpp", feature = "mozcpp"))]
12515    #[test]
12516    fn cpp_operator_names_are_operators() {
12517        assert_cpp_operator_names::<CppParser>("cpp");
12518        assert_cpp_operator_names::<MozcppParser>("mozcpp");
12519    }
12520
12521    /// Asserts a coroutine's `co_yield` and `co_return` keywords bill as
12522    /// operators under parser `T`, as `return` does (#1547).
12523    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
12524    #[track_caller]
12525    fn assert_cpp_coroutine_keywords<T: crate::MetricSuite>(label: &str) {
12526        let source = "T gen() { co_yield 1; co_yield 2; co_return 3; }";
12527        // Operators (n1 = 5, N1 = 8): `()` 1, `{}` 1, `co_yield` 2,
12528        // `co_return` 1, `;` 3. Operands (n2 = 5, N2 = 5): `T`, `gen`,
12529        // `1`, `2`, `3`. Before #1547: (3, 5, 5, 5), both keywords
12530        // billed nowhere.
12531        assert_halstead_counts::<T>(source, "foo.cpp", [5, 8, 5, 5], label);
12532        let ops = ops_of::<T>(source, "foo.cpp");
12533        for keyword in ["co_yield", "co_return"] {
12534            assert!(
12535                ops.operators.iter().any(|o| o == keyword),
12536                "{label}: `{keyword}` must be an operator; operators were {:?}",
12537                ops.operators
12538            );
12539        }
12540    }
12541
12542    /// Regression for #1546: a zero-width node is billed in neither
12543    /// Halstead half. Error recovery inserts one for a token the source
12544    /// lacks, usually flagged MISSING, and every getter that classifies
12545    /// the kind billed it — an identifier as the empty-string operand.
12546    /// One fixture per language, each a parse on which recovery inserts
12547    /// an operand-classified zero-width node; before the fix every row
12548    /// billed `""`. C#'s and Elixir's are not flagged MISSING, which is
12549    /// why the guard keys on width: an `is_missing()` guard leaves those
12550    /// two rows billing `""`. Each row names its zero-width operand's
12551    /// kind and flag, so the two unflagged rows keep discriminating. The
12552    /// rows pin no counts, since the recovery trees are the grammars' to
12553    /// change (grammar-dispatch §6); the operator half, an inserted `;` or
12554    /// `)`, is anchored by `cpp_missing_tokens_in_valid_code_are_not_billed`.
12555    #[cfg(any(
12556        feature = "bash",
12557        feature = "c",
12558        feature = "cpp",
12559        feature = "csharp",
12560        feature = "elixir",
12561        feature = "go",
12562        feature = "groovy",
12563        feature = "irules",
12564        feature = "java",
12565        feature = "javascript",
12566        feature = "kotlin",
12567        feature = "lua",
12568        feature = "mozcpp",
12569        feature = "mozjs",
12570        feature = "objc",
12571        feature = "perl",
12572        feature = "php",
12573        feature = "python",
12574        feature = "ruby",
12575        feature = "rust",
12576        feature = "tcl",
12577        feature = "typescript",
12578    ))]
12579    #[test]
12580    fn zero_width_nodes_bill_neither_halstead_half() {
12581        // `(language, file, source, kind, is_missing)`: the last two name
12582        // the zero-width operand each fixture's recovery inserts, so a
12583        // grammar bump that recovers some other way fails the row rather
12584        // than passing it on an unrelated zero-width node.
12585        let cases: &[(LANG, &str, &str, &str, bool)] = &[
12586            #[cfg(feature = "bash")]
12587            (LANG::Bash, "foo.sh", "echo a $( )\n", "word", true),
12588            #[cfg(feature = "c")]
12589            (
12590                LANG::C,
12591                "foo.c",
12592                "int x; void () { x = 1; }\n",
12593                "identifier",
12594                true,
12595            ),
12596            #[cfg(feature = "cpp")]
12597            (
12598                LANG::Cpp,
12599                "foo.cpp",
12600                "void f(S *s) {\n  s->~D<F>();\n}\n",
12601                "identifier",
12602                true,
12603            ),
12604            #[cfg(feature = "csharp")]
12605            (
12606                LANG::Csharp,
12607                "foo.cs",
12608                "class A { int = 1; }\n",
12609                "identifier",
12610                false,
12611            ),
12612            #[cfg(feature = "elixir")]
12613            (LANG::Elixir, "foo.ex", "x = a.\n", "identifier", false),
12614            #[cfg(feature = "go")]
12615            (
12616                LANG::Go,
12617                "foo.go",
12618                "package m\nfunc f() { a.() }\n",
12619                "type_identifier",
12620                true,
12621            ),
12622            #[cfg(feature = "groovy")]
12623            (
12624                LANG::Groovy,
12625                "foo.groovy",
12626                "def x = a.\n",
12627                "identifier",
12628                true,
12629            ),
12630            #[cfg(feature = "irules")]
12631            (
12632                LANG::Irules,
12633                "foo.irule",
12634                "when HTTP_REQUEST { set x [expr {1 + }] }\n",
12635                "number",
12636                true,
12637            ),
12638            #[cfg(feature = "java")]
12639            (
12640                LANG::Java,
12641                "Foo.java",
12642                "class A { int = 1; }\n",
12643                "identifier",
12644                true,
12645            ),
12646            #[cfg(feature = "javascript")]
12647            (
12648                LANG::Javascript,
12649                "foo.js",
12650                "let {a: } = b;\n",
12651                "identifier",
12652                true,
12653            ),
12654            #[cfg(feature = "kotlin")]
12655            (
12656                LANG::Kotlin,
12657                "foo.kt",
12658                "fun f(: Int) {}\n",
12659                "identifier",
12660                true,
12661            ),
12662            #[cfg(feature = "lua")]
12663            (LANG::Lua, "foo.lua", "x = a.\n", "identifier", true),
12664            #[cfg(feature = "mozcpp")]
12665            (
12666                LANG::Mozcpp,
12667                "foo.cpp",
12668                "void f(S *s) {\n  s->~D<F>();\n}\n",
12669                "identifier",
12670                true,
12671            ),
12672            #[cfg(feature = "mozjs")]
12673            (
12674                LANG::Mozjs,
12675                "foo.jsm",
12676                "let {a: } = b;\n",
12677                "identifier",
12678                true,
12679            ),
12680            #[cfg(feature = "objc")]
12681            (
12682                LANG::Objc,
12683                "foo.m",
12684                "@interface A\n- (void)f:;\n@end\n",
12685                "identifier",
12686                true,
12687            ),
12688            #[cfg(feature = "perl")]
12689            (LANG::Perl, "foo.pl", "sub { $x = ; }\n", "identifier", true),
12690            #[cfg(feature = "php")]
12691            (LANG::Php, "foo.php", "<?php $x = $a->;\n", "name", true),
12692            #[cfg(feature = "python")]
12693            (
12694                LANG::Python,
12695                "foo.py",
12696                "for in x:\n  pass\n",
12697                "identifier",
12698                true,
12699            ),
12700            #[cfg(feature = "ruby")]
12701            (LANG::Ruby, "foo.rb", "x = a.\n", "identifier", true),
12702            #[cfg(feature = "rust")]
12703            (
12704                LANG::Rust,
12705                "foo.rs",
12706                "fn f() { let x = a.; }\n",
12707                "field_identifier",
12708                true,
12709            ),
12710            #[cfg(feature = "tcl")]
12711            (LANG::Tcl, "foo.tcl", "puts [expr {1 + }]\n", "number", true),
12712            #[cfg(feature = "typescript")]
12713            (
12714                LANG::Typescript,
12715                "foo.ts",
12716                "let {a: } = b;\n",
12717                "identifier",
12718                true,
12719            ),
12720            #[cfg(feature = "typescript")]
12721            (LANG::Tsx, "foo.tsx", "let {a: } = b;\n", "identifier", true),
12722        ];
12723        crate::test_support::assert_fixtures_present(cases);
12724
12725        let mut billed = Vec::new();
12726        for (lang, file, source, kind, missing) in cases {
12727            let ast = crate::test_support::parse_named(*lang, file, source);
12728            assert!(
12729                ast.root_node().preorder().any(|node| {
12730                    node.start_byte() == node.end_byte()
12731                        && node.kind() == *kind
12732                        && node.is_missing() == *missing
12733                }),
12734                "{lang:?}: `{source}` no longer recovers with a zero-width `{kind}` \
12735                 (is_missing: {missing}); find another fixture"
12736            );
12737            let ops = ast.ops().expect("ops walk must yield a top-level Ops");
12738            assert!(
12739                !ops.operands.is_empty(),
12740                "{lang:?}: `{source}` billed no operand at all"
12741            );
12742            billed.extend(ops.operands.iter().any(String::is_empty).then_some(*lang));
12743        }
12744        assert!(
12745            billed.is_empty(),
12746            "a zero-width node was billed as the empty operand in {billed:?}"
12747        );
12748    }
12749
12750    /// Asserts two valid C++ programs that tree-sitter-cpp 0.23.4 can only
12751    /// parse by inserting MISSING tokens bill only the tokens written
12752    /// (#1546), under parser `T`. Both come from the DeepSpeech corpus.
12753    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
12754    #[track_caller]
12755    fn assert_cpp_missing_tokens_unbilled<T: crate::MetricSuite>(label: &str) {
12756        // `openfst/dfs-visit.h:75`: an explicit destructor call through a
12757        // template name. Recovery reads `s->~D<F` as comparisons and
12758        // inserts a MISSING identifier inside the trailing `()`.
12759        // Operators (n1 = 9, N1 = 10): `void`, `()` 2, `*`, `{}`, `->`,
12760        // `~`, `<`, `>`, `;`. Operands (n2 = 5, N2 = 6): `f`, `S`, `s` 2,
12761        // `D`, `F`. Before #1546: (9, 10, 6, 7), the extra being `""`.
12762        let destructor = "void f(S *s) {\n  s->~D<F>();\n}\n";
12763        assert_halstead_counts::<T>(destructor, "foo.cpp", [9, 10, 5, 6], label);
12764        let ops = ops_of::<T>(destructor, "foo.cpp");
12765        assert_eq!(ops.operands, ["D", "F", "S", "f", "s"], "{label}");
12766
12767        // `kenlm/lm/search_trie.cc:286`: a `typename` functional cast
12768        // used as a statement. Recovery inserts a MISSING `)` after
12769        // `quant_` and a MISSING `;` after `order`. Operators (n1 = 9,
12770        // N1 = 11): `void`, `()` 3, `{}`, `::`, `,`, `-`, `.`, the one
12771        // written `;`, and `typename` (#1557). Operands (n2 = 7, N2 = 7):
12772        // `f`, `MiddlePointer`, `quant_`, `order`, `2`, `Write`, `w`.
12773        // Before #1546 the MISSING `;` was a second `;`.
12774        let cast = "void f() {\n  typename Quant::MiddlePointer(quant_, order - 2).Write(w);\n}\n";
12775        assert_halstead_counts::<T>(cast, "foo.cpp", [9, 11, 7, 7], label);
12776    }
12777
12778    /// Regression for #1546 on the two C++ clones. Mozcpp owns no file
12779    /// extension, so this row is its only coverage.
12780    #[cfg(all(feature = "cpp", feature = "mozcpp"))]
12781    #[test]
12782    fn cpp_missing_tokens_in_valid_code_are_not_billed() {
12783        assert_cpp_missing_tokens_unbilled::<CppParser>("cpp");
12784        assert_cpp_missing_tokens_unbilled::<MozcppParser>("mozcpp");
12785    }
12786
12787    /// Regression for #1547: `co_return` and `co_yield` are Halstead
12788    /// operators. One row per clone, so reverting either arm fails its
12789    /// own row; Mozcpp owns no file extension, so this row is its only
12790    /// coverage.
12791    #[cfg(all(feature = "cpp", feature = "mozcpp"))]
12792    #[test]
12793    fn cpp_coroutine_keywords_are_operators() {
12794        assert_cpp_coroutine_keywords::<CppParser>("cpp");
12795        assert_cpp_coroutine_keywords::<MozcppParser>("mozcpp");
12796    }
12797
12798    /// Regression for #1361: a C++ `this` is a Halstead operand.
12799    ///
12800    /// `Cpp::This` / `Mozcpp::This` were in neither arm of
12801    /// `CppCode::get_op_type` / `MozcppCode::get_op_type`, so a `this`
12802    /// contributed *nothing* — not an operator and not an operand —
12803    /// while eleven of the fourteen languages here bill their
12804    /// self-reference as an operand. Same shape as #1316's character
12805    /// literals, and the inverse of the #1351-#1355 over-counts.
12806    ///
12807    /// Each language asserts separately over the same source rather than
12808    /// sharing one call, so reverting one clone's arm fails that row
12809    /// alone (grammar-dispatch section 11). Mozcpp owns no file
12810    /// extension, so no integration snapshot reaches its clone; this row
12811    /// and `cpp_and_mozcpp_agree_on_this` in `tests/parity/` are the
12812    /// whole coverage that arm has.
12813    #[cfg(all(feature = "cpp", feature = "mozcpp"))]
12814    #[test]
12815    fn cpp_this_is_an_operand() {
12816        assert_this_receiver_parity::<CppParser>("this.cpp", "cpp");
12817        assert_this_receiver_parity::<MozcppParser>("this.cpp", "mozcpp");
12818    }
12819
12820    /// The `This` arm is unconditional: every `this` in
12821    /// `CPP_THIS_POSITIONS` classifies as an operand whatever encloses
12822    /// it (#1361).
12823    ///
12824    /// The counts in `cpp_this_is_an_operand` are measured on one
12825    /// position only. An arm reached through a parent-scoped guard —
12826    /// the shape the neighbouring `RawStringLiteral` `LPAREN` arm uses,
12827    /// and the shape a reviewer might reasonably add here — would pass
12828    /// that test and still drop `this` in a lambda capture or a
12829    /// `decltype`. This walk is what makes the arm's unconditional
12830    /// reach a measurement rather than an assumption.
12831    ///
12832    /// Named for the *property* rather than for the fixture: the fixture
12833    /// samples one position per container kind and is deliberately not
12834    /// exhaustive (see `CPP_THIS_POSITIONS`), so a name promising "every
12835    /// position" would claim more than it checks.
12836    #[cfg(all(feature = "cpp", feature = "mozcpp"))]
12837    #[test]
12838    fn cpp_this_is_an_operand_regardless_of_position() {
12839        #[cfg(any(feature = "cpp", feature = "mozcpp"))]
12840        fn check<L: LanguageInfo + Getter>(label: &str) {
12841            let mut seen = 0_usize;
12842            for_each_node_with_chain::<L>(CPP_THIS_POSITIONS.as_bytes(), |node, chain| {
12843                if node.kind() != "this" {
12844                    return;
12845                }
12846                seen += 1;
12847                // `_with_code` is the spelling `compute_halstead` calls.
12848                // The default forwards to the byte-less form, so the two
12849                // agree for both languages today — which is exactly why
12850                // asking the wrong one would read as correct right up
12851                // until one of them grew an override
12852                // (grammar-dispatch section 7).
12853                assert!(
12854                    matches!(
12855                        L::get_op_type_with_code(
12856                            node,
12857                            CPP_THIS_POSITIONS.as_bytes(),
12858                            Ancestors::known(chain)
12859                        ),
12860                        TokenRole::Operand
12861                    ),
12862                    "{label}: a `this` under `{}` is not an operand",
12863                    chain.last().map_or("<root>", Node::kind)
12864                );
12865            });
12866            // One per position: `->` receiver, `(*this)`, bare return,
12867            // `[this]`, `[=, this]`, `decltype(this->x)`, the
12868            // constructor body, and the call argument.
12869            assert_eq!(seen, 8, "{label}: fixture lost a `this` position");
12870        }
12871
12872        check::<CppCode>("cpp");
12873        check::<MozcppCode>("mozcpp");
12874    }
12875
12876    /// Pins the two grammar facts the #1361 operand arm rests on, across
12877    /// both fixtures and both languages.
12878    ///
12879    /// * **Grammar-dispatch section 1.** Every node the grammar spells
12880    ///   `this` carries the one `kind_id` the arm lists. That is the
12881    ///   weaker half of the alias evidence — an alias arises in a
12882    ///   *different* syntactic position, which no fixture can enumerate.
12883    ///   The strong half is that these generated enums carry
12884    ///   numeric-suffix aliases in quantity (`FunctionDefinition2..4`,
12885    ///   `QualifiedIdentifier2..4`, `LPAREN2`, `Try2`, `GT2` in this one
12886    ///   file) and neither spells a `This2` — `This = 215` is the sole
12887    ///   variant rendering `"this"` in `language_cpp.rs` and
12888    ///   `language_mozcpp.rs`. This loop is what notices if a grammar
12889    ///   bump changes that under an existing fixture.
12890    /// * **Grammar-dispatch section 5.** A `this` is a childless leaf and
12891    ///   no node containing it is classified. That is both halves of the
12892    ///   double-count question: nothing below it can be billed a second
12893    ///   time, and no `field_expression` / `pointer_expression` /
12894    ///   `lambda_capture_specifier` / `argument_list` wrapper bills the
12895    ///   same source text from above.
12896    #[cfg(all(feature = "cpp", feature = "mozcpp"))]
12897    #[test]
12898    fn cpp_this_is_a_childless_unaliased_leaf() {
12899        #[cfg(any(feature = "cpp", feature = "mozcpp"))]
12900        fn check<L: LanguageInfo + Getter>(this: u16, label: &str) {
12901            let mut seen = 0_usize;
12902            for source in [CPP_THIS_RECEIVER_PARITY, CPP_THIS_POSITIONS] {
12903                for_each_node_with_chain::<L>(source.as_bytes(), |node, chain| {
12904                    if node.kind() != "this" {
12905                        return;
12906                    }
12907                    seen += 1;
12908                    assert_eq!(
12909                        node.kind_id(),
12910                        this,
12911                        "{label}: a `this` carries kind_id {} rather than the {this} the \
12912                         operand arm lists — an alias the arm cannot see",
12913                        node.kind_id()
12914                    );
12915                    assert_eq!(
12916                        node.child_count(),
12917                        0,
12918                        "{label}: `this` grew children, which the operand arm would now \
12919                         double-count against"
12920                    );
12921                    assert!(!chain.is_empty(), "{label}: a `this` cannot be the root");
12922                    // Every ancestor, not just the parent. The claim is
12923                    // that *no node containing* a `this` is classified,
12924                    // and `(*this)` alone puts two wrappers between the
12925                    // leaf and the nearest classified node — checking
12926                    // only `chain.last()` would assert something
12927                    // narrower than the comment on the arm promises.
12928                    // `chain` is the *node's* ancestry, so the ancestor
12929                    // at `i` has `chain[..i]` for its own.
12930                    for (i, ancestor) in chain.iter().enumerate() {
12931                        assert!(
12932                            matches!(
12933                                L::get_op_type_with_code(
12934                                    ancestor,
12935                                    source.as_bytes(),
12936                                    Ancestors::known(&chain[..i])
12937                                ),
12938                                TokenRole::Unknown
12939                            ),
12940                            "{label}: `{}` contains a `this` and is itself classified, so the \
12941                             reference now counts twice",
12942                            ancestor.kind()
12943                        );
12944                    }
12945                });
12946            }
12947            // One `this` in the parity fixture, eight in the positions
12948            // fixture. Asserted so a fixture that stopped containing one
12949            // cannot make this test pass having checked nothing.
12950            assert_eq!(seen, 9, "{label}: fixtures lost a `this`");
12951        }
12952
12953        check::<CppCode>(Cpp::This as u16, "cpp");
12954        check::<MozcppCode>(Mozcpp::This as u16, "mozcpp");
12955    }
12956
12957    /// Builds a `HalsteadMaps` from explicit occurrence counts.
12958    ///
12959    /// The per-language tests above reach these maps only through a
12960    /// parse, which cannot produce a *chosen* overlap between a child
12961    /// and its parent — the cases `merge` exists to get right.
12962    fn halstead_maps_of<'a>(
12963        operators: &[(u16, u64)],
12964        primitive_operators: &[(&'a [u8], u64)],
12965        operands: &[(&'a [u8], u64)],
12966    ) -> HalsteadMaps<'a> {
12967        HalsteadMaps {
12968            operators: operators.iter().copied().collect(),
12969            primitive_operators: primitive_operators.iter().copied().collect(),
12970            operands: operands.iter().copied().collect(),
12971        }
12972    }
12973
12974    /// `HalsteadMaps::operators` must stay on the crate's integer hasher.
12975    ///
12976    /// Swapping a hasher moves no metric value, so every other test in
12977    /// this file passes just as well with #1108 reverted. Both halves
12978    /// here are needed: the typed binding stops compiling if the field
12979    /// goes back to a default-hasher `HashMap`, and the `type_name`
12980    /// comparison still fails at runtime if `IntKeyHashMap` itself is
12981    /// ever redefined to wrap `RandomState`.
12982    ///
12983    /// The two text-keyed maps are pinned to SipHash in the same test,
12984    /// because moving *them* would be a regression rather than an
12985    /// optimisation. `crate::int_hash`'s module doc is the single place
12986    /// that argues why analysed source text does not qualify.
12987    #[test]
12988    fn halstead_operator_map_uses_the_int_key_hasher() {
12989        use std::any::{type_name, type_name_of_val};
12990        use std::hash::BuildHasherDefault;
12991
12992        use crate::int_hash::IntKeyHasher;
12993
12994        let maps = HalsteadMaps::new();
12995
12996        let operators: &IntKeyHashMap<u16, u64> = &maps.operators;
12997        assert_eq!(
12998            type_name_of_val(operators.hasher()),
12999            type_name::<BuildHasherDefault<IntKeyHasher>>(),
13000            "the kind_id-keyed operator map must use the int_hash hasher"
13001        );
13002
13003        let siphash = type_name::<std::collections::hash_map::RandomState>();
13004        assert_eq!(
13005            type_name_of_val(maps.operands.hasher()),
13006            siphash,
13007            "operand keys come from the analysed source, so the keyed hash \
13008             is what stops a crafted file from flooding this map"
13009        );
13010        assert_eq!(
13011            type_name_of_val(maps.primitive_operators.hasher()),
13012            siphash,
13013            "primitive-operator keys come from the analysed source, so the \
13014             keyed hash is what stops a crafted file from flooding this map"
13015        );
13016    }
13017
13018    /// `merge` sums overlapping keys and adopts disjoint ones, in all
13019    /// three maps, and `finalize` reads the union back as n1/N1/n2/N2.
13020    ///
13021    /// Every count differs from every other and none is zero, so a
13022    /// dropped key, an overwrite where an addition belongs, or a map
13023    /// crossed with its neighbour all change the totals.
13024    #[test]
13025    fn halstead_maps_merge_sums_overlaps_and_adopts_disjoint_keys() {
13026        let mut parent = halstead_maps_of(
13027            &[(1, 2), (2, 3)],
13028            &[(b"int", 1)],
13029            &[(b"alpha", 4), (b"beta", 7)],
13030        );
13031        let child = halstead_maps_of(
13032            &[(2, 5), (7, 11)],
13033            &[(b"double", 13)],
13034            &[(b"alpha", 17), (b"gamma", 19)],
13035        );
13036
13037        parent.merge(&child);
13038
13039        // expected: operators {1: 2, 2: 3+5, 7: 11}; primitives
13040        // {int: 1, double: 13}; operands {alpha: 4+17, beta: 7,
13041        // gamma: 19}.
13042        assert_eq!(
13043            parent,
13044            halstead_maps_of(
13045                &[(1, 2), (2, 8), (7, 11)],
13046                &[(b"int", 1), (b"double", 13)],
13047                &[(b"alpha", 21), (b"beta", 7), (b"gamma", 19)],
13048            )
13049        );
13050
13051        let mut stats = Stats::default();
13052        parent.finalize(&mut stats);
13053        // expected: n1 = 3 kind ids + 2 primitives; N1 = (2+8+11) +
13054        // (1+13); n2 = 3 texts; N2 = 21+7+19.
13055        assert_eq!(stats.unique_operators(), 5);
13056        assert_eq!(stats.total_operators(), 35);
13057        assert_eq!(stats.unique_operands(), 3);
13058        assert_eq!(stats.total_operands(), 47);
13059    }
13060
13061    /// Merging an empty child leaves the parent untouched.
13062    ///
13063    /// A space with no operators or operands is the common case for a
13064    /// leaf getter or an empty function body, and `finalize` runs on
13065    /// the parent afterwards either way.
13066    #[test]
13067    fn halstead_maps_merge_of_empty_child_is_a_no_op() {
13068        let mut parent = halstead_maps_of(&[(3, 5)], &[(b"char", 2)], &[(b"delta", 9)]);
13069        let before = parent.clone();
13070
13071        parent.merge(&HalsteadMaps::new());
13072
13073        assert_eq!(parent, before);
13074
13075        let mut stats = Stats::default();
13076        parent.finalize(&mut stats);
13077        // expected: n1 = 1 kind id + 1 primitive; N1 = 5 + 2; n2 = 1;
13078        // N2 = 9.
13079        assert_eq!(stats.unique_operators(), 2);
13080        assert_eq!(stats.total_operators(), 7);
13081        assert_eq!(stats.unique_operands(), 1);
13082        assert_eq!(stats.total_operands(), 9);
13083    }
13084
13085    /// Folding a chain of nested spaces bottom-up must reach the union
13086    /// of every level, re-merging already-merged maps on the way up.
13087    ///
13088    /// This is what `spaces.rs` and `ops.rs` actually do: each space is
13089    /// merged into its parent as the walk pops it, so by the time the
13090    /// root sees a grandchild's counts they have already passed through
13091    /// one `merge`. The literal expectation below is what discriminates
13092    /// — the `nested == flat` cross-check on its own does not, because
13093    /// any entry-wise fold over the same levels agrees with itself
13094    /// however it is associated, including a broken one.
13095    #[test]
13096    fn halstead_maps_merge_folds_a_nested_chain() {
13097        let levels = [
13098            halstead_maps_of(&[(1, 1)], &[(b"int", 1)], &[(b"a", 1)]),
13099            halstead_maps_of(&[(1, 2), (2, 3)], &[], &[(b"a", 2), (b"b", 4)]),
13100            halstead_maps_of(&[(2, 5)], &[(b"long", 6)], &[(b"b", 7)]),
13101            halstead_maps_of(&[(3, 8)], &[(b"int", 9)], &[(b"c", 10)]),
13102        ];
13103
13104        // Bottom-up: the deepest level folds into its parent, that
13105        // result into *its* parent, and so on up to the root.
13106        let mut nested = levels[levels.len() - 1].clone();
13107        for level in levels.iter().rev().skip(1) {
13108            let mut outer = level.clone();
13109            outer.merge(&nested);
13110            nested = outer;
13111        }
13112
13113        // Flat: every level merged directly into the root.
13114        let mut flat = levels[0].clone();
13115        for level in &levels[1..] {
13116            flat.merge(level);
13117        }
13118
13119        // expected: every key summed across the four levels — operators
13120        // {1: 1+2, 2: 3+5, 3: 8}, primitives {int: 1+9, long: 6},
13121        // operands {a: 1+2, b: 4+7, c: 10}.
13122        assert_eq!(
13123            nested,
13124            halstead_maps_of(
13125                &[(1, 3), (2, 8), (3, 8)],
13126                &[(b"int", 10), (b"long", 6)],
13127                &[(b"a", 3), (b"b", 11), (b"c", 10)],
13128            )
13129        );
13130        assert_eq!(nested, flat);
13131
13132        let mut stats = Stats::default();
13133        nested.finalize(&mut stats);
13134        // expected: n1 = 3 kind ids + 2 primitives; N1 = (3+8+8) +
13135        // (10+6); n2 = 3 texts; N2 = 3+11+10.
13136        assert_eq!(stats.unique_operators(), 5);
13137        assert_eq!(stats.total_operators(), 35);
13138        assert_eq!(stats.unique_operands(), 3);
13139        assert_eq!(stats.total_operands(), 24);
13140    }
13141
13142    /// A `kind_id` at the top of the `u16` range must behave like any
13143    /// other key.
13144    ///
13145    /// The largest grammar in the workspace (`mozcpp`) tops out around
13146    /// 640 symbols, so nothing near `u16::MAX` occurs today — but the
13147    /// map is keyed by the raw id, and a dense-array representation
13148    /// (the shape #1108 considered and rejected) is exactly what such a
13149    /// key would break. Pinning it keeps that trade-off honest if the
13150    /// representation is ever revisited.
13151    #[test]
13152    fn halstead_maps_handle_the_full_kind_id_range() {
13153        let mut parent = halstead_maps_of(&[(0, 3), (u16::MAX, 5)], &[], &[]);
13154        parent.merge(&halstead_maps_of(&[(u16::MAX, 7)], &[], &[]));
13155
13156        let mut stats = Stats::default();
13157        parent.finalize(&mut stats);
13158        // expected: two distinct kind ids, occurrences 3 and 5+7.
13159        assert_eq!(stats.unique_operators(), 2);
13160        assert_eq!(stats.total_operators(), 15);
13161    }
13162}