big_code_analysis/metrics/halstead.rs
1// Per-language metric and AST modules deliberately consume the macro-
2// generated tree-sitter token enums via `use crate::*` and `use Foo::*`
3// inside match expressions — explicit imports would list dozens of
4// variants per arm and obscure the per-language token sets that are the
5// point of these files. Allowed at the module level rather than per
6// function so the per-language impl blocks stay readable.
7#![allow(
8 clippy::doc_markdown,
9 clippy::enum_glob_use,
10 clippy::match_wildcard_for_single_variants,
11 clippy::similar_names,
12 clippy::unused_self,
13 clippy::wildcard_imports
14)]
15// Metric counts (token, function, branch, argument, etc.) are stored as
16// `usize` and crossed with `f64` averages, ratios, and Halstead scores
17// across the cyclomatic / MI / Halstead computations. The `usize as f64`
18// and `f64 as usize` casts are intentional and snapshot-anchored — every
19// site is bounded by the count it came from. Allowing the lints at the
20// module level keeps the metric arithmetic legible.
21#![allow(
22 clippy::cast_precision_loss,
23 clippy::cast_possible_truncation,
24 clippy::cast_sign_loss
25)]
26
27use std::collections::HashMap;
28
29use std::fmt;
30
31// The operator / operand classification is syntactic and lives beside
32// the per-language `Getter` tables that produce it; re-exported here
33// because this metric is what consumes it.
34pub use crate::token_role::TokenRole;
35
36/// The former name of [`TokenRole`], kept so the
37/// `metrics::halstead::HalsteadType` path still resolves.
38///
39/// The classification never was Halstead-specific — it answers whether
40/// a node acts as an operator or an operand, which the grammar decides
41/// and other consumers can use (#1376). Renaming it outright would be a
42/// SemVer break, so the old spelling stays until the next major.
43#[deprecated(
44 since = "2.3.0",
45 note = "renamed to `TokenRole`: the classification is syntactic, not Halstead-specific"
46)]
47pub type HalsteadType = TokenRole;
48
49use crate::checker::Checker;
50use crate::getter::Getter;
51use crate::int_hash::IntKeyHashMap;
52use crate::macros::implement_metric_trait;
53
54use crate::*;
55
56/// The `Halstead` metric suite.
57#[derive(Default, Clone, Debug, PartialEq)]
58#[non_exhaustive]
59pub struct Stats {
60 u_operators: u64,
61 operators: u64,
62 u_operands: u64,
63 operands: u64,
64}
65
66/// Per-space operator / operand occurrence maps used to compute the
67/// Halstead `Stats` struct. One map per distinct operator (`kind_id`)
68/// and one per distinct operand (`text`); merged across nested spaces.
69#[derive(Debug, Default, Clone, PartialEq)]
70pub struct HalsteadMaps<'a> {
71 /// Keyed by `kind_id`, so it is hashed with [`crate::int_hash`]'s
72 /// integer hasher rather than SipHash: the key is a grammar symbol
73 /// this crate generated, drawn from an alphabet of at most a few
74 /// hundred values, so there is nothing for a keyed hash to defend.
75 pub(crate) operators: IntKeyHashMap<u16, u64>,
76 /// Primitive-type operators stored by text so each distinct primitive
77 /// (e.g. `int` vs `double`) counts as a separate distinct operator,
78 /// even when the grammar maps them all to a single kind_id. Also
79 /// holds the operators a getter keys by a fixed spelling
80 /// ([`Getter::get_operator_spelling`]), such as the generic `<>`
81 /// pair (#1559).
82 ///
83 /// Text-keyed, so it keeps SipHash — see the module doc on
84 /// [`crate::int_hash`] for why analysed source text does not qualify
85 /// for the fast hasher.
86 pub(crate) primitive_operators: HashMap<&'a [u8], u64>,
87 /// Text-keyed, and on SipHash for the same reason as
88 /// `primitive_operators`.
89 pub(crate) operands: HashMap<&'a [u8], u64>,
90}
91
92impl<'a> HalsteadMaps<'a> {
93 pub(crate) fn new() -> Self {
94 Self::default()
95 }
96
97 pub(crate) fn merge(&mut self, other: &HalsteadMaps<'a>) {
98 for (k, v) in &other.operators {
99 *self.operators.entry(*k).or_insert(0) += v;
100 }
101 for (k, v) in &other.primitive_operators {
102 *self.primitive_operators.entry(*k).or_insert(0) += v;
103 }
104 for (k, v) in &other.operands {
105 *self.operands.entry(*k).or_insert(0) += v;
106 }
107 }
108
109 pub(crate) fn finalize(&self, stats: &mut Stats) {
110 stats.u_operators = (self.operators.len() + self.primitive_operators.len()) as u64;
111 stats.operators =
112 self.operators.values().sum::<u64>() + self.primitive_operators.values().sum::<u64>();
113 stats.u_operands = self.operands.len() as u64;
114 stats.operands = self.operands.values().sum::<u64>();
115 }
116}
117
118impl fmt::Display for Stats {
119 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
120 write!(
121 f,
122 "unique_operators: {}, \
123 total_operators: {}, \
124 unique_operands: {}, \
125 total_operands: {}, \
126 length: {}, \
127 estimated_program_length: {}, \
128 purity_ratio: {}, \
129 size: {}, \
130 volume: {}, \
131 difficulty: {}, \
132 level: {}, \
133 effort: {}, \
134 time: {}, \
135 bugs: {}",
136 self.unique_operators(),
137 self.total_operators(),
138 self.unique_operands(),
139 self.total_operands(),
140 self.length(),
141 self.estimated_program_length(),
142 self.purity_ratio(),
143 self.vocabulary(),
144 self.volume(),
145 self.difficulty(),
146 self.level(),
147 self.effort(),
148 self.time(),
149 self.bugs(),
150 )
151 }
152}
153
154impl Stats {
155 // Intentionally a no-op. Halstead distinct-counts (`u_operators` /
156 // `u_operands`) cannot be summed across sibling spaces without
157 // double-counting operators/operands they share. Cross-space
158 // aggregation is instead done by unioning the occurrence maps
159 // (`HalsteadMaps::merge`) and re-running `finalize` on the parent
160 // (see `spaces/compute.rs`). Summing the finalized fields here —
161 // mirroring the sibling metrics' `merge` — would silently inflate
162 // every parent space's n1/n2/N1/N2.
163 pub(crate) fn merge(&mut self, _other: &Stats) {}
164
165 /// Returns `η1`, the number of distinct operators
166 #[inline]
167 #[must_use]
168 pub fn unique_operators(&self) -> u64 {
169 self.u_operators
170 }
171
172 /// Returns `N1`, the number of total operators
173 #[inline]
174 #[must_use]
175 pub fn total_operators(&self) -> u64 {
176 self.operators
177 }
178
179 /// Returns `η2`, the number of distinct operands
180 #[inline]
181 #[must_use]
182 pub fn unique_operands(&self) -> u64 {
183 self.u_operands
184 }
185
186 /// Returns `N2`, the number of total operands
187 #[inline]
188 #[must_use]
189 pub fn total_operands(&self) -> u64 {
190 self.operands
191 }
192
193 /// Returns the program length
194 ///
195 /// Computed as `N = N1 + N2`, the sum of [`Self::total_operators`] and
196 /// [`Self::total_operands`].
197 #[inline]
198 #[must_use]
199 pub fn length(&self) -> u64 {
200 self.total_operands() + self.total_operators()
201 }
202
203 /// Returns the calculated estimated program length
204 ///
205 /// Computed as `N^ = n1 * log2(n1) + n2 * log2(n2)`, where `n1` is
206 /// [`Self::unique_operators`] and `n2` is [`Self::unique_operands`]. Each term is
207 /// treated as `0` when its unique count is `0`.
208 #[inline]
209 #[must_use]
210 pub fn estimated_program_length(&self) -> f64 {
211 let uo = self.unique_operators() as f64;
212 let ud = self.unique_operands() as f64;
213 let uo_term = if uo == 0.0 { 0.0 } else { uo * uo.log2() };
214 let ud_term = if ud == 0.0 { 0.0 } else { ud * ud.log2() };
215 uo_term + ud_term
216 }
217
218 /// Returns the purity ratio
219 ///
220 /// Computed as `PR = N^ / N`, the ratio of
221 /// [`Self::estimated_program_length`] to [`Self::length`].
222 #[inline]
223 #[must_use]
224 pub fn purity_ratio(&self) -> f64 {
225 let len = self.length() as f64;
226 if len == 0.0 {
227 0.0
228 } else {
229 self.estimated_program_length() / len
230 }
231 }
232
233 /// Returns the program vocabulary
234 ///
235 /// Computed as `n = n1 + n2`, the sum of [`Self::unique_operators`] and
236 /// [`Self::unique_operands`].
237 #[inline]
238 #[must_use]
239 pub fn vocabulary(&self) -> u64 {
240 self.unique_operands() + self.unique_operators()
241 }
242
243 /// Returns the program volume.
244 ///
245 /// Computed as `V = N * log2(n)`, where `N` is [`Self::length`] and `n`
246 /// is [`Self::vocabulary`]. Returns `0` when the vocabulary is `<= 1`,
247 /// since `log2` would be non-positive.
248 ///
249 /// Unit of measurement: bits
250 #[inline]
251 #[must_use]
252 pub fn volume(&self) -> f64 {
253 // Assumes a uniform binary encoding for the vocabulary is used.
254 let vocab = self.vocabulary() as f64;
255 if vocab <= 1.0 {
256 0.0
257 } else {
258 self.length() as f64 * vocab.log2()
259 }
260 }
261
262 /// Returns the estimated difficulty required to program
263 ///
264 /// Computed as `D = (n1 / 2) * (N2 / n2)`, where `n1` is
265 /// [`Self::unique_operators`], `N2` is [`Self::total_operands`], and `n2` is
266 /// [`Self::unique_operands`].
267 #[inline]
268 #[must_use]
269 pub fn difficulty(&self) -> f64 {
270 let ud = self.unique_operands() as f64;
271 if ud == 0.0 {
272 0.0
273 } else {
274 self.unique_operators() as f64 / 2. * self.total_operands() as f64 / ud
275 }
276 }
277
278 /// Returns the estimated level of difficulty required to program
279 ///
280 /// Computed as `L = 1 / D`, the reciprocal of [`Self::difficulty`].
281 #[inline]
282 #[must_use]
283 pub fn level(&self) -> f64 {
284 let d = self.difficulty();
285 if d == 0.0 { 0.0 } else { 1. / d }
286 }
287
288 /// Returns the estimated effort required to program
289 ///
290 /// Computed as `E = D * V`, the product of [`Self::difficulty`] and
291 /// [`Self::volume`].
292 #[inline]
293 #[must_use]
294 pub fn effort(&self) -> f64 {
295 self.difficulty() * self.volume()
296 }
297
298 /// Returns the estimated time required to program.
299 ///
300 /// Computed as `T = E / 18`, where `E` is [`Self::effort`] and `18` is
301 /// the Stroud number (see the divisor rationale below).
302 ///
303 /// Unit of measurement: seconds
304 #[inline]
305 #[must_use]
306 pub fn time(&self) -> f64 {
307 // The floating point `18.` aims to describe the processing rate of the
308 // human brain. It is called Stoud number, S, and its
309 // unit of measurement is moments/seconds.
310 // A moment is the time required by the human brain to carry out the
311 // most elementary decision.
312 // 5 <= S <= 20. Halstead uses 18.
313 // The value of S has been empirically developed from psychological
314 // reasoning, and its recommended value for
315 // programming applications is 18.
316 //
317 // Source: https://www.geeksforgeeks.org/software-engineering-halsteads-software-metrics/
318 self.effort() / 18.
319 }
320
321 /// Returns the estimated number of delivered bugs.
322 ///
323 /// This metric represents the average amount of work a programmer can do
324 /// without introducing an error.
325 ///
326 /// Computed as `B = E^(2/3) / 3000`, where `E` is [`Self::effort`]. This
327 /// is the effort-based variant of Halstead's delivered-bugs estimate
328 /// rather than the more commonly cited volume-based form `B = V / 3000`;
329 /// it matches the formula used by upstream `rust-code-analysis`.
330 #[inline]
331 #[must_use]
332 pub fn bugs(&self) -> f64 {
333 // The floating point `3000.` represents the number of elementary
334 // mental discriminations.
335 // A mental discrimination, in psychology, is the ability to perceive
336 // and respond to differences among stimuli.
337 //
338 // The value above is obtained starting from a constant that
339 // is different for every language and assumes that natural language is
340 // the language of the brain.
341 // For programming languages, the English language constant
342 // has been considered.
343 //
344 // After every 3000 mental discriminations a result is produced.
345 // This result, whether correct or incorrect, is more than likely
346 // either used as an input for the next operation or is output to the
347 // environment.
348 // If incorrect the error should become apparent.
349 // Thus, an opportunity for error occurs every 3000
350 // mental discriminations.
351 //
352 // Source: https://docs.lib.purdue.edu/cgi/viewcontent.cgi?article=1145&context=cstech
353 self.effort().powf(2. / 3.) / 3000.
354 }
355}
356
357#[doc(hidden)]
358/// Per-language extraction of Halstead operator/operand maps.
359pub(crate) trait Halstead
360where
361 Self: Checker + Getter,
362{
363 /// Walk `node` and update `stats` with this metric for the language
364 /// implementing the trait.
365 ///
366 /// `ancestors` is the chain the walker descended through; it is
367 /// handed to [`Getter::get_op_type`], six of whose impls classify a
368 /// token by what encloses it (#1096).
369 fn compute<'a>(
370 node: &Node<'a>,
371 code: &'a [u8],
372 ancestors: Ancestors<'a, '_>,
373 halstead_maps: &mut HalsteadMaps<'a>,
374 );
375}
376
377#[inline]
378fn get_id<'a>(node: &Node<'a>, code: &'a [u8]) -> &'a [u8] {
379 &code[node.start_byte()..node.end_byte()]
380}
381
382#[inline]
383fn compute_halstead<'a, T: Getter + Checker>(
384 node: &Node<'a>,
385 code: &'a [u8],
386 ancestors: Ancestors<'a, '_>,
387 halstead_maps: &mut HalsteadMaps<'a>,
388) {
389 // Halstead counts the tokens written, and a zero-width node spells
390 // none, so it bills neither half (#1546). Error recovery is where
391 // they come from: every MISSING node tree-sitter inserts for a token
392 // the source lacks (a `;`, a `)`, an identifier) is zero-width, and
393 // so is the identifier C#'s and Elixir's recovery builds without
394 // flagging it MISSING. Checked here rather than per getter because
395 // every getter that classifies the kind would otherwise bill it, an
396 // identifier as the empty-string operand.
397 if node.start_byte() == node.end_byte() {
398 return;
399 }
400 match T::get_op_type_with_code(node, code, ancestors) {
401 TokenRole::Operator => {
402 if let Some(spelling) = T::get_operator_spelling(node, ancestors) {
403 *halstead_maps
404 .primitive_operators
405 .entry(spelling.as_bytes())
406 .or_insert(0) += 1;
407 } else if T::is_primitive(node) {
408 // Store primitive-type operators by text so distinct
409 // primitives (e.g. `int` vs `double`) that share a
410 // single kind_id are counted separately in n1/N1.
411 *halstead_maps
412 .primitive_operators
413 .entry(get_id(node, code))
414 .or_insert(0) += 1;
415 } else {
416 *halstead_maps.operators.entry(node.kind_id()).or_insert(0) += 1;
417 }
418 }
419 TokenRole::Operand => {
420 *halstead_maps
421 .operands
422 .entry(T::get_operand_id(node, code, ancestors))
423 .or_insert(0) += 1;
424 }
425 _ => {}
426 }
427}
428
429// Every language's `Halstead::compute` is the same forward to
430// `compute_halstead`, which classifies each node through the language's
431// own `Getter` / `Checker`. Nothing per-language lives here — it lives
432// in `big-code-analysis-ast/src/getter/<lang>.rs` — so writing the
433// impls out was 23 copies of one signature. (This is the only metric
434// whose per-language impls are all identical; every other trait has
435// real per-language bodies.)
436macro_rules! impl_halstead_forwarding {
437 ($($code:ty),+ $(,)?) => {
438 $(
439 impl Halstead for $code {
440 fn compute<'a>(
441 node: &Node<'a>,
442 code: &'a [u8],
443 ancestors: Ancestors<'a, '_>,
444 halstead_maps: &mut HalsteadMaps<'a>,
445 ) {
446 compute_halstead::<Self>(node, code, ancestors, halstead_maps);
447 }
448 }
449 )+
450 };
451}
452
453impl_halstead_forwarding!(
454 PythonCode,
455 MozjsCode,
456 JavascriptCode,
457 TypescriptCode,
458 TsxCode,
459 RustCode,
460 CppCode,
461 CCode,
462 ObjcCode,
463 MozcppCode,
464 JavaCode,
465 GroovyCode,
466 CsharpCode,
467 GoCode,
468 PerlCode,
469 KotlinCode,
470 LuaCode,
471 PhpCode,
472 RubyCode,
473 ElixirCode,
474 BashCode,
475 TclCode,
476 IrulesCode,
477);
478
479// Real defaults — no operators / operands to count. Audited in #188.
480implement_metric_trait!(Halstead, PreprocCode, CcommentCode);
481
482#[cfg(test)]
483#[allow(
484 clippy::float_cmp,
485 clippy::cast_precision_loss,
486 clippy::cast_possible_truncation,
487 clippy::cast_sign_loss,
488 clippy::similar_names,
489 clippy::doc_markdown,
490 clippy::needless_raw_string_hashes,
491 clippy::too_many_lines
492)]
493mod tests {
494 use std::collections::{BTreeSet, HashSet};
495 use std::path::PathBuf;
496
497 use crate::test_support::{ast_has_kind_id, check_metrics_only_shim, for_each_node_with_chain};
498
499 use super::*;
500
501 check_metrics_only_shim!(check_metrics, Halstead);
502
503 // `HalsteadType` is the pre-#1376 name for `TokenRole`, kept as a
504 // deprecated alias because removing it before `3.0` would be a
505 // SemVer break (STABILITY.md). Nothing in this repository uses the
506 // old spelling any more, so only this guard would notice it being
507 // dropped — and dropping it silently is exactly what the promise
508 // rules out.
509 //
510 // The `matches!` arms are the assertion: a value typed through the
511 // alias can only be matched against `TokenRole`'s variants if the
512 // alias still names that enum, so this fails to *compile* if the
513 // alias is removed or re-pointed, rather than failing at runtime.
514 // `TokenRole` derives nothing, so there is no `assert_eq!` to reach
515 // for here.
516 #[test]
517 #[allow(deprecated)]
518 fn halstead_type_alias_still_names_token_role() {
519 let via_alias: HalsteadType = HalsteadType::Operand;
520 assert!(matches!(via_alias, TokenRole::Operand));
521 assert!(!matches!(via_alias, TokenRole::Operator));
522 }
523
524 /// Runs the `--ops` walk over `source` and returns the root space's
525 /// merged vocabulary, with every nested space still reachable
526 /// through [`crate::ops::Ops::spaces`].
527 #[cfg(any(
528 feature = "c",
529 feature = "cpp",
530 feature = "csharp",
531 feature = "groovy",
532 feature = "irules",
533 feature = "java",
534 feature = "javascript",
535 feature = "kotlin",
536 feature = "mozcpp",
537 feature = "mozjs",
538 feature = "objc",
539 feature = "perl",
540 feature = "ruby",
541 feature = "tcl",
542 feature = "typescript",
543 ))]
544 fn ops_of<T: crate::MetricSuite>(source: &str, file: &str) -> crate::ops::Ops {
545 let path = PathBuf::from(file);
546 let parser = T::new(source.as_bytes().to_vec(), &path, None);
547 crate::ops::ops_inner(&parser, None).expect("ops walk succeeds")
548 }
549
550 // Pins the lesson-4 invariant `n2 == len(dedupe(ops.operands))` by
551 // running `operands_and_operators` (the text-keyed `--ops` store)
552 // on the same source and comparing its deduplicated operand count
553 // to the expected `n2`. The metrics store and the ops store are
554 // independent (lesson 4); this catches a classification change that
555 // moves one without the other.
556 // `#[track_caller]` so a failure reports the *caller's* line rather
557 // than this helper's. Callers that wrap it in a per-language helper
558 // (`assert_char_literal_operands`, #1316) are tracked too, so the
559 // reported location names the language row instead of a shared line
560 // no assertion message distinguishes.
561 #[cfg(any(
562 feature = "c",
563 feature = "cpp",
564 feature = "groovy",
565 feature = "irules",
566 feature = "kotlin",
567 feature = "mozcpp",
568 feature = "objc",
569 feature = "perl",
570 feature = "ruby",
571 feature = "tcl",
572 ))]
573 #[track_caller]
574 fn assert_ops_operands<T: crate::MetricSuite>(
575 source: &str,
576 file: &str,
577 expected_n2: usize,
578 mut expected_operands: Vec<&str>,
579 ) {
580 let ops = ops_of::<T>(source, file);
581
582 let unique: HashSet<&str> = ops.operands.iter().map(String::as_str).collect();
583 assert_eq!(
584 unique.len(),
585 expected_n2,
586 "dedupe(ops.operands) must equal n2; operands were {:?}",
587 ops.operands
588 );
589
590 let mut got: Vec<&str> = unique.into_iter().collect();
591 got.sort_unstable();
592 expected_operands.sort_unstable();
593 assert_eq!(got, expected_operands, "operand vocabulary for {file}");
594 }
595
596 /// Asserts each keyword in `keywords` reaches the operand vocabulary
597 /// and reaches the operator vocabulary nowhere.
598 ///
599 /// Both halves are load-bearing for #1380. A keyword listed in two
600 /// arms is billed twice — once into `n1`/`N1` and once into
601 /// `n2`/`N2` — and an assertion that only looked for its arrival
602 /// among the operands would pass on that. The operator side is what
603 /// pins the removal.
604 #[cfg(any(
605 feature = "csharp",
606 feature = "groovy",
607 feature = "java",
608 feature = "kotlin"
609 ))]
610 #[track_caller]
611 fn assert_keywords_are_operands_only<T: crate::MetricSuite>(
612 source: &str,
613 file: &str,
614 keywords: &[&str],
615 ) {
616 let ops = ops_of::<T>(source, file);
617
618 for keyword in keywords {
619 assert!(
620 ops.operands.iter().any(|o| o == keyword),
621 "{file}: `{keyword}` must be an operand; operands were {:?}",
622 ops.operands
623 );
624 assert!(
625 !ops.operators.iter().any(|o| o == keyword),
626 "{file}: `{keyword}` must not be an operator; operators were {:?}",
627 ops.operators
628 );
629 }
630 }
631
632 /// How one space's vocabularies classify a single keyword.
633 #[derive(Debug, PartialEq, Eq)]
634 enum Role {
635 OperatorOnly,
636 OperandOnly,
637 Both,
638 }
639
640 /// Records `keyword`'s role in `ops` and in every space below it.
641 ///
642 /// A space that bills the keyword in neither vocabulary contributes
643 /// no row, so the result is the set of places the keyword actually
644 /// reaches — which is what distinguishes a parent gate from its own
645 /// inverse, and what the merged root vocabulary cannot show.
646 #[cfg(feature = "csharp")]
647 fn collect_keyword_roles(ops: &crate::ops::Ops, keyword: &str, out: &mut Vec<Role>) {
648 let operator = ops.operators.iter().any(|o| o == keyword);
649 let operand = ops.operands.iter().any(|o| o == keyword);
650 match (operator, operand) {
651 (true, true) => out.push(Role::Both),
652 (true, false) => out.push(Role::OperatorOnly),
653 (false, true) => out.push(Role::OperandOnly),
654 (false, false) => {}
655 }
656 for space in &ops.spaces {
657 collect_keyword_roles(space, keyword, out);
658 }
659 }
660
661 /// Pins the three grammar facts every #1380 operand arm rests on,
662 /// over a fixture that samples one self- or super-reference per
663 /// syntactic position the language allows.
664 ///
665 /// Each `keywords` row is `(spelling, kind_id, occurrences)`.
666 ///
667 /// * **Grammar-dispatch section 1.** Every node the grammar spells
668 /// `this` / `super` / `base` / `this@` / `super@` carries the one
669 /// `kind_id` its arm lists. An alias arises in a *different*
670 /// syntactic position, which no fixture can enumerate, so this is
671 /// the weaker half of the alias evidence — the strong half is that
672 /// these generated enums carry numeric-suffix aliases in quantity
673 /// and none spells a `This2`. What this loop catches is a grammar
674 /// bump introducing one under an existing fixture.
675 /// * **Grammar-dispatch section 5.** Each is a childless leaf and no
676 /// node *containing* it is classified, so the reference cannot be
677 /// billed twice. Every ancestor is checked, not just the parent:
678 /// Kotlin puts `this_expression` *and* `navigation_expression`
679 /// between the leaf and the nearest classified node, and the claim
680 /// the arms make is about containment, not parentage.
681 /// * **Grammar-dispatch section 11.** The arm is unconditional. A
682 /// parent-scoped guard added later — the shape the two deliberate
683 /// carve-outs use, and so the shape a reviewer might reasonably
684 /// add — would satisfy the single-position count tests and still
685 /// drop `this` in a method reference or a constructor delegation.
686 ///
687 /// The fixtures deliberately exclude the two gated positions (C#'s
688 /// indexer declarator, Java's wildcard bound); those are operators,
689 /// and their own tests pin them.
690 #[cfg(any(feature = "csharp", feature = "java", feature = "kotlin"))]
691 #[track_caller]
692 fn assert_self_reference_leaves<L: LanguageInfo + Getter>(
693 source: &str,
694 keywords: &[(&str, u16, usize)],
695 label: &str,
696 ) {
697 let code = source.as_bytes();
698 let mut seen = vec![0_usize; keywords.len()];
699
700 for_each_node_with_chain::<L>(code, |node, chain| {
701 let Some(index) = keywords.iter().position(|(kw, _, _)| *kw == node.kind()) else {
702 return;
703 };
704 let (keyword, kind_id, _) = keywords[index];
705 seen[index] += 1;
706
707 assert_eq!(
708 node.kind_id(),
709 kind_id,
710 "{label}: a `{keyword}` carries kind_id {} rather than the {kind_id} its arm \
711 lists — an alias the arm cannot see",
712 node.kind_id()
713 );
714 assert_eq!(
715 node.child_count(),
716 0,
717 "{label}: `{keyword}` grew children, which the operand arm would now \
718 double-count against"
719 );
720 for (depth, ancestor) in chain.iter().enumerate() {
721 assert!(
722 matches!(
723 L::get_op_type_with_code(ancestor, code, Ancestors::known(&chain[..depth])),
724 TokenRole::Unknown
725 ),
726 "{label}: `{}` contains a `{keyword}` and is itself classified, so the \
727 reference now counts twice",
728 ancestor.kind()
729 );
730 }
731 // `_with_code` is the spelling `compute_halstead` calls. The
732 // default forwards to the byte-less form, so asking the wrong
733 // one reads as correct right up until a language grows an
734 // override (grammar-dispatch section 7).
735 assert!(
736 matches!(
737 L::get_op_type_with_code(node, code, Ancestors::known(chain)),
738 TokenRole::Operand
739 ),
740 "{label}: a `{keyword}` under `{}` is not an operand",
741 chain.last().map_or("<root>", Node::kind)
742 );
743 });
744
745 for ((keyword, _, expected), got) in keywords.iter().zip(&seen) {
746 assert_eq!(
747 got, expected,
748 "{label}: fixture holds {got} `{keyword}` rather than {expected}"
749 );
750 }
751 }
752
753 /// Asserts the root space's `[n1, N1, n2, N2]`, naming `label` when
754 /// it does not hold.
755 ///
756 /// The delimiter-invariance tests (#1256 Elixir, #1312 Ruby and
757 /// Perl) each loop over spellings of one literal and need the
758 /// spelling in the failure message; `check_metrics` expands to a
759 /// plain `fn` that cannot capture a loop variable, so they reach
760 /// for the closure-taking helper it wraps. Three copies of that
761 /// dance is two too many.
762 #[cfg(any(
763 feature = "bash",
764 feature = "c",
765 feature = "cpp",
766 feature = "csharp",
767 feature = "elixir",
768 feature = "groovy",
769 feature = "irules",
770 feature = "java",
771 feature = "javascript",
772 feature = "kotlin",
773 feature = "mozcpp",
774 feature = "mozjs",
775 feature = "objc",
776 feature = "perl",
777 feature = "php",
778 feature = "ruby",
779 feature = "tcl",
780 feature = "typescript",
781 ))]
782 fn assert_halstead_counts<T: crate::MetricSuite>(
783 source: &str,
784 file: &str,
785 expected: [u64; 4],
786 label: &str,
787 ) {
788 crate::test_support::check_func_space_only::<T, _>(
789 source,
790 file,
791 &[crate::Metric::Halstead],
792 |space| {
793 let halstead = &space.metrics.halstead;
794 assert_eq!(
795 [
796 halstead.unique_operators(),
797 halstead.total_operators(),
798 halstead.unique_operands(),
799 halstead.total_operands(),
800 ],
801 expected,
802 "{label}"
803 );
804 },
805 );
806 }
807
808 #[cfg(feature = "python")]
809 #[test]
810 fn python_operators_and_operands() {
811 check_metrics::<PythonParser>(
812 "def foo():
813 def bar():
814 def toto():
815 a = 1 + 1
816 b = 2 + a
817 c = 3 + 3",
818 "foo.py",
819 |metric| {
820 // unique operators: def, (), :, =, +
821 // operators: def, (), : for each of the three headers,
822 // then =, =, =, +, +, + (the `()` and `:` since #1486)
823 // unique operands: foo, bar, toto, a, b, c, 1, 2, 3
824 // operands: foo, bar, toto, a, b, c, 1, 1, 2, a, 3, 3
825 assert_eq!(metric.halstead.unique_operators(), 5);
826 assert_eq!(metric.halstead.total_operators(), 15);
827 insta::assert_json_snapshot!(
828 metric.halstead,
829 @r#"
830 {
831 "unique_operators": 5,
832 "total_operators": 15,
833 "unique_operands": 9,
834 "total_operands": 12,
835 "length": 27,
836 "estimated_program_length": 40.13896548741762,
837 "purity_ratio": 1.4866283513858378,
838 "vocabulary": 14,
839 "volume": 102.79858289555531,
840 "difficulty": 3.3333333333333335,
841 "level": 0.3,
842 "effort": 342.6619429851844,
843 "time": 19.03677461028802,
844 "bugs": 0.01632259960095138
845 }
846 "#
847 );
848 },
849 );
850 }
851
852 /// Pointer-arithmetic operators: `*` (dereference), `&` (address-of),
853 /// `->` (member-of-pointer), `+` (pointer + offset). Each is counted
854 /// once in `n1`; multiple uses bump `N1`. The headline integer values
855 /// (`u_operators`, `u_operands`) anchor the snapshot per the
856 /// snapshot-anchor policy.
857 #[cfg(feature = "c")]
858 #[test]
859 fn c_pointer_arithmetic_operators() {
860 check_metrics::<CParser>(
861 "int g(int* p, int* q) {
862 return *(p + 1) + *q;
863 }",
864 "foo.c",
865 |metric| {
866 // Unique operators: int, *, (), {, }, +, ;, return (= 8)
867 // `*` covers both pointer-type and dereference; the grammar
868 // does NOT split them. `,` does not appear (only one
869 // parameter on each side of the body).
870 // Unique operands: g, p, q, 1 (= 4)
871 assert_eq!(metric.halstead.unique_operators(), 8);
872 assert_eq!(metric.halstead.unique_operands(), 4);
873 insta::assert_json_snapshot!(metric.halstead);
874 },
875 );
876 }
877
878 /// Bitwise (`&`, `|`, `^`, `~`, `<<`, `>>`) and logical (`&&`, `||`,
879 /// `!`) operators are distinct kind_ids and count as separate unique
880 /// operators in Halstead. `&` (bitwise-and) and `&&` (logical-and)
881 /// must NOT collapse, even though both render as ampersands.
882 #[cfg(feature = "c")]
883 #[test]
884 fn c_bitwise_and_logical_operators() {
885 check_metrics::<CParser>(
886 "int f(int a, int b) {
887 int x = (a & b) | (a ^ b);
888 int y = ~a;
889 int z = (a << 1) >> 2;
890 return (a && b) || !x;
891 }",
892 "foo.c",
893 |metric| {
894 // Expect: 6 bitwise op kinds (& | ^ ~ << >>), 3 logical (&& || !).
895 // Plus int, (), {, }, =, ;, return, , — 8 syntactic / arithmetic
896 // operator kinds. Six bitwise + three logical + eight = 17 unique
897 // operators is the upper bound; actuals depend on grammar collapse,
898 // so we assert a lower-bound and anchor via snapshot below.
899 let s = &metric.halstead;
900 assert!(
901 s.unique_operators() >= 14,
902 "expected >= 14 unique operators (bitwise + logical + syntax), got {}",
903 s.unique_operators(),
904 );
905 assert_eq!(s.unique_operands(), 8); // f, a, b, x, y, z, 1, 2
906 insta::assert_json_snapshot!(metric.halstead);
907 },
908 );
909 }
910
911 /// Increment / decrement (`++`, `--`) and `sizeof` / cast operators
912 /// each contribute distinct unique operators. C-style casts in the
913 /// tree-sitter grammar surface as `cast_expression` with the type
914 /// token classified as a primitive_type operator.
915 #[cfg(feature = "c")]
916 #[test]
917 fn c_increment_decrement_and_sizeof() {
918 check_metrics::<CParser>(
919 "void f(int* p) {
920 int n = sizeof(int);
921 ++p;
922 --n;
923 long w = (long) n;
924 }",
925 "foo.c",
926 |metric| {
927 // Unique operators include: void, int, long, *, =, sizeof, ++, --, (), {, }, ;
928 // Unique operands: f, p, n, w
929 let s = &metric.halstead;
930 assert!(
931 s.unique_operators() >= 10,
932 "expected >= 10 unique operators including ++ / -- / sizeof / cast, got {}",
933 s.unique_operators(),
934 );
935 assert_eq!(s.unique_operands(), 4);
936 insta::assert_json_snapshot!(metric.halstead);
937 },
938 );
939 }
940
941 #[cfg(feature = "cpp")]
942 #[test]
943 fn cpp_operators_and_operands() {
944 // Define operators and operands for C/C++ grammar according to this specification:
945 // https://www.verifysoft.com/en_halstead_metrics.html
946 // The only difference with the specification above is that
947 // primitive types are treated as operators, since the definition of a
948 // primitive type can be seen as the creation of a slot of a certain size.
949 // i.e. The `int a;` definition creates a n-bytes slot.
950 check_metrics::<CppParser>(
951 "main()
952 {
953 int a, b, c, avg;
954 scanf(\"%d %d %d\", &a, &b, &c);
955 avg = (a + b + c) / 3;
956 printf(\"avg = %d\", avg);
957 }",
958 "foo.c",
959 |metric| {
960 // unique operators: (), {}, int, &, =, +, /, ,, ;
961 // unique operands: main, a, b, c, avg, scanf, "%d %d %d", 3, printf, "avg = %d"
962 insta::assert_json_snapshot!(
963 metric.halstead,
964 @r#"
965 {
966 "unique_operators": 9,
967 "total_operators": 24,
968 "unique_operands": 10,
969 "total_operands": 18,
970 "length": 42,
971 "estimated_program_length": 61.74860596185444,
972 "purity_ratio": 1.470204903853677,
973 "vocabulary": 19,
974 "volume": 178.41295556463058,
975 "difficulty": 8.1,
976 "level": 0.1234567901234568,
977 "effort": 1445.1449400735075,
978 "time": 80.28583000408375,
979 "bugs": 0.04260752914034329
980 }
981 "#
982 );
983 },
984 );
985 }
986
987 /// A `sized_type_specifier` carries its `unsigned`/`signed`/`long`/
988 /// `short` modifiers as bare keyword tokens (distinct kind_ids), not
989 /// as `primitive_type` children. Prior to issue #466 those tokens
990 /// fell through to the `Unknown` arm and were dropped from `n1`/`N1`,
991 /// so `unsigned int` collapsed to just `int` and `signed long`
992 /// contributed nothing. They must each count as a distinct operator,
993 /// while `long long`'s two `long` tokens fold to one `n1` entry but
994 /// two `N1` hits. Regression test for issue #466.
995 #[cfg(feature = "cpp")]
996 #[test]
997 fn cpp_sized_type_specifier_operators() {
998 let source = "unsigned int u = 3; signed long b = 4; long long c = 5;";
999 check_metrics::<CppParser>(source, "foo.cpp", |metric| {
1000 // Distinct operators (n1): unsigned, signed, long, int, =, ; = 6
1001 // Total operators (N1):
1002 // unsigned(1) + int(1) + =(3) + ;(3) + signed(1) + long(3) = 12
1003 // (`long` appears once in `signed long` and twice in `long long`)
1004 // Distinct/total operands: u, b, c, 3, 4, 5 = 6 / 6
1005 assert_eq!(metric.halstead.unique_operators(), 6);
1006 assert_eq!(metric.halstead.total_operators(), 12);
1007 assert_eq!(metric.halstead.unique_operands(), 6);
1008 assert_eq!(metric.halstead.total_operands(), 6);
1009 });
1010
1011 // Pin the lesson-4 `n1 == dedupe(ops.operators)` invariant: the
1012 // kind_id-keyed metrics store and the text-keyed `--ops` store are
1013 // independent, so a modifier classified in one but not the other
1014 // would diverge here.
1015 let path = PathBuf::from("foo.cpp");
1016 let parser = CppParser::new(source.as_bytes().to_vec(), &path, None);
1017 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
1018 let unique_operators: HashSet<&str> = ops.operators.iter().map(String::as_str).collect();
1019 assert_eq!(
1020 unique_operators.len(),
1021 6,
1022 "dedupe(ops.operators) must equal n1; operators were {:?}",
1023 ops.operators
1024 );
1025 for modifier in ["unsigned", "signed", "long"] {
1026 assert!(
1027 unique_operators.contains(modifier),
1028 "sized_type_specifier modifier {modifier:?} missing from ops.operators: {:?}",
1029 ops.operators
1030 );
1031 }
1032 }
1033
1034 /// C++20 spaceship operator `<=>` (`Cpp::LTEQGT`) is a comparison
1035 /// operator and must be counted in Halstead, like its sibling
1036 /// comparison operators `<`, `>`, `<=`, `>=`, `==`, `!=`. Prior to
1037 /// this fix it fell through to the `Unknown` arm and was silently
1038 /// dropped from `n1` / `N1`, under-reporting volume / effort on any
1039 /// C++20+ codebase that defines `operator<=>`. Regression test for
1040 /// issue #197.
1041 #[cfg(feature = "cpp")]
1042 #[test]
1043 fn cpp_spaceship_operator_is_halstead_operator() {
1044 check_metrics::<CppParser>(
1045 "int f(int a, int b) {
1046 return (a <=> b) != 0;
1047 }",
1048 "foo.cpp",
1049 |metric| {
1050 // Unique operators (grammar collapses matched delimiters
1051 // to a single kind_id): int, (), {}, <=>, !=, return, ;, ,
1052 // `<=>` is the regression target — without the fix it
1053 // would be Unknown and `u_operators` would be 7.
1054 // Unique operands: f, a, b, 0
1055 let s = &metric.halstead;
1056 assert_eq!(s.unique_operators(), 8);
1057 assert_eq!(s.unique_operands(), 4);
1058 insta::assert_json_snapshot!(
1059 s,
1060 @r#"
1061 {
1062 "unique_operators": 8,
1063 "total_operators": 11,
1064 "unique_operands": 4,
1065 "total_operands": 6,
1066 "length": 17,
1067 "estimated_program_length": 32.0,
1068 "purity_ratio": 1.8823529411764706,
1069 "vocabulary": 12,
1070 "volume": 60.94436251225965,
1071 "difficulty": 6.0,
1072 "level": 0.16666666666666666,
1073 "effort": 365.6661750735579,
1074 "time": 20.31478750408655,
1075 "bugs": 0.01704519358507665
1076 }
1077 "#
1078 );
1079 },
1080 );
1081 }
1082
1083 /// C++ compound subtract-assign `-=` (`Cpp::DASHEQ`) must be counted
1084 /// in Halstead like every other compound assignment (`+=`, `*=`,
1085 /// `/=`, etc.). Prior to the fix it fell through to the `Unknown`
1086 /// arm and was silently dropped from `n1` / `N1` — under-reporting
1087 /// volume / effort wherever C++ code subtracts in place. Regression
1088 /// test for issue #198.
1089 #[cfg(feature = "cpp")]
1090 #[test]
1091 fn cpp_dash_eq_is_halstead_operator() {
1092 check_metrics::<CppParser>("void f(int a, int b) { a -= b; }", "foo.cpp", |metric| {
1093 // Unique operators: void, (), {}, int, ,, -=, ;
1094 // `-=` is the regression target — without the fix it
1095 // would be Unknown and `u_operators` would be 6.
1096 // Unique operands: f, a, b
1097 let s = &metric.halstead;
1098 assert_eq!(s.unique_operators(), 7);
1099 assert_eq!(s.unique_operands(), 3);
1100 });
1101 }
1102
1103 /// C++ pointer-to-member access `.*` (`Cpp::DOTSTAR`) must be
1104 /// counted in Halstead. Prior to the fix it fell through to the
1105 /// `Unknown` arm and was silently dropped from `n1` / `N1`.
1106 /// Regression test for issue #198.
1107 ///
1108 /// The snippet uses an `operator.*` declaration because that is
1109 /// where the C++ tree-sitter grammar reliably emits a single
1110 /// `DOTSTAR` leaf; in expression position (`a.*b`) some grammar
1111 /// versions split the token into `DOT` + `STAR` and the regression
1112 /// would be masked.
1113 #[cfg(feature = "cpp")]
1114 #[test]
1115 fn cpp_dot_star_is_halstead_operator() {
1116 check_metrics::<CppParser>("struct S { void operator.*(int); };", "foo.cpp", |metric| {
1117 // Unique operators with fix: {}, ;, (), int, void, .*, the
1118 // `operator` keyword (#1296) and `struct` (#1552).
1119 // `.*` is the regression target — without the fix it
1120 // falls through to `Unknown` and `u_operators` is 7.
1121 // Unique operands: S
1122 let s = &metric.halstead;
1123 assert_eq!(s.unique_operators(), 8);
1124 assert_eq!(s.unique_operands(), 1);
1125 });
1126 }
1127
1128 /// C++ pointer-to-member access through pointer `->*`
1129 /// (`Cpp::DASHGTSTAR`) must be counted in Halstead. Prior to the
1130 /// fix it fell through to the `Unknown` arm and was silently
1131 /// dropped from `n1` / `N1`. Regression test for issue #198.
1132 ///
1133 /// The snippet uses an `operator->*` declaration because that is
1134 /// where the C++ tree-sitter grammar reliably emits a single
1135 /// `DASHGTSTAR` leaf; in expression position (`a->*b`) the grammar
1136 /// splits the token into `DASHGT` + `STAR` and the regression would
1137 /// be masked.
1138 #[cfg(feature = "cpp")]
1139 #[test]
1140 fn cpp_dash_gt_star_is_halstead_operator() {
1141 check_metrics::<CppParser>(
1142 "struct S { void operator->*(int); };",
1143 "foo.cpp",
1144 |metric| {
1145 // Unique operators with fix: {}, ;, (), int, void, ->*,
1146 // the `operator` keyword (#1296) and `struct` (#1552).
1147 // `->*` is the regression target — without the fix it
1148 // falls through to `Unknown` and `u_operators` is 7.
1149 // Unique operands: S
1150 let s = &metric.halstead;
1151 assert_eq!(s.unique_operators(), 8);
1152 assert_eq!(s.unique_operands(), 1);
1153 },
1154 );
1155 }
1156
1157 #[cfg(feature = "cpp")]
1158 #[test]
1159 fn cpp_raw_string_delimiter_is_not_an_operator() {
1160 // Regression: issue #1314, the C++ sibling of Elixir #1256 and
1161 // Ruby/Perl #1312. A `raw_string_literal` carries its `R"(`
1162 // opener as a bare `LPAREN` child — the kind id a call uses —
1163 // so `auto a = R"(raw)";` reported a `()` operator with no call
1164 // in the source.
1165 //
1166 // The fixture holds both sides at once: two raw strings and one
1167 // real call. A guard widened past the literal would drop
1168 // `f(a)`'s parenthesis and fail here rather than silently
1169 // passing.
1170 //
1171 // The second literal uses the custom-delimiter form to pin that
1172 // shape too — it adds a `raw_string_delimiter` child but keeps
1173 // the same `(` — and its distinct text makes n2 differ from N2.
1174 //
1175 // expected: operators `;` × 3, `=` × 3, `int`, `()` × 1 →
1176 // n1 = 4, N1 = 8. Operands the two literals, `a` × 2, `b`, `c`,
1177 // `f` → n2 = 6, N2 = 7. Before the guard the two openers added
1178 // two more `()` → N1 = 10.
1179 check_metrics::<CppParser>(
1180 "auto a = R\"(raw)\";\nauto b = R\"tag(raw)tag\";\nint c = f(a);\n",
1181 "foo.cpp",
1182 |metric| {
1183 assert_eq!(metric.halstead.unique_operators(), 4);
1184 assert_eq!(metric.halstead.total_operators(), 8);
1185 assert_eq!(metric.halstead.unique_operands(), 6);
1186 assert_eq!(metric.halstead.total_operands(), 7);
1187 },
1188 );
1189 }
1190
1191 #[cfg(feature = "rust")]
1192 #[test]
1193 fn rust_operators_and_operands() {
1194 check_metrics::<RustParser>(
1195 "fn main() {
1196 let a = 5; let b = 5; let c = 5;
1197 let avg = (a + b + c) / 3;
1198 println!(\"{}\", avg);
1199 }",
1200 "foo.rs",
1201 |metric| {
1202 // unique operators: fn, (), {}, let, =, +, /, ;, !, ,
1203 // unique operands: main, a, b, c, avg, 5, 3, println, "{}"
1204 insta::assert_json_snapshot!(
1205 metric.halstead,
1206 @r#"
1207 {
1208 "unique_operators": 10,
1209 "total_operators": 23,
1210 "unique_operands": 9,
1211 "total_operands": 15,
1212 "length": 38,
1213 "estimated_program_length": 61.74860596185444,
1214 "purity_ratio": 1.624963314785643,
1215 "vocabulary": 19,
1216 "volume": 161.42124551085624,
1217 "difficulty": 8.333333333333334,
1218 "level": 0.12,
1219 "effort": 1345.177045923802,
1220 "time": 74.7320581068779,
1221 "bugs": 0.040619232256751396
1222 }
1223 "#
1224 );
1225 },
1226 );
1227 }
1228
1229 #[cfg(feature = "rust")]
1230 #[test]
1231 fn rust_aliased_primitive_type_classification() {
1232 // Regression for issue #95 (lesson #2): the Rust grammar emits 17
1233 // distinct `kind_id`s for `primitive_type` (one base plus 16
1234 // numeric-suffixed alias variants). `RustCode::is_primitive` in
1235 // `big-code-analysis-ast/src/checker.rs` must list every variant;
1236 // if a future regression omits one, primitive type names emitted
1237 // in that aliased position silently drop into the kind_id-keyed
1238 // operators bucket instead of the text-keyed primitive_operators
1239 // map, miscounting Halstead n1.
1240 //
1241 // The snippet exercises every primitive scalar type across many
1242 // syntactic positions (function parameter types, return types,
1243 // let-binding annotations, `as` casts, const items, type aliases,
1244 // struct fields, function pointer types, tuple types, array types,
1245 // reference types, generic type arguments). Empirically, ordinary
1246 // Rust source emits the base `Rust::PrimitiveType` variant from
1247 // all of these positions; the 16 suffixed alias variants are
1248 // produced by specific grammar productions not reachable from
1249 // user-written code. Mutation-verified: dropping
1250 // `Rust::PrimitiveType` from `is_primitive` fails this test
1251 // (u_operators 30→15). Dropping any single suffixed variant
1252 // currently leaves the test passing; if a future grammar bump
1253 // makes any suffixed variant reachable from idiomatic source,
1254 // extend the snippet so the test fires for that variant too.
1255 check_metrics::<RustParser>(
1256 "const C: u8 = 0;
1257 type T = i64;
1258 struct S { x: u32, y: u64 }
1259 fn g(p: fn(u8) -> u16) -> bool { let _ = p(0); true }
1260 fn f(a: u8, b: u16, c: u32, d: u64) -> u128 {
1261 let _x: i8 = 0;
1262 let _y: i16 = 0;
1263 let _z: i32 = 0;
1264 let _w: i64 = 0;
1265 let _v: i128 = 0;
1266 let _p: f32 = 1.0;
1267 let _q: f64 = 2.0;
1268 let _r: bool = true;
1269 let _s: char = 'x';
1270 let _t: usize = 0;
1271 let _u: isize = 0;
1272 let _arr: [u32; 4] = [0; 4];
1273 let _ref: &u8 = &0;
1274 let _tup: (u32, u64) = (0, 0);
1275 let _opt: Option<u32> = None;
1276 a as u128 + b as u128 + c as u128 + d
1277 }",
1278 "foo.rs",
1279 |metric| {
1280 // Headline: u_operators is the load-bearing assertion —
1281 // the 16 distinct primitive type names dedupe by text in
1282 // the primitive_operators map. Total operators (N1) and
1283 // operand counts pin the rest of the Halstead state.
1284 // Grew from 30 → 33 with the issue #394 fix: `const`,
1285 // `type`, and `struct` keywords are now classified as
1286 // operators (one occurrence each). #1486 adds the
1287 // annotation `:` — one entry, 23 occurrences: `C`, the
1288 // two fields, `p`, the four params of `f`, and the
1289 // fifteen `let` bindings. #1559 bills `Option<u32>`'s
1290 // brackets as one `<>` pair: one entry and one
1291 // occurrence fewer.
1292 assert_eq!(metric.halstead.unique_operators(), 33);
1293 assert_eq!(metric.halstead.total_operators(), 143);
1294 // u_operands / operands grew (was 31/50 before #390): the
1295 // fix now classifies TypeIdentifier (`T`, `S`, `Option`)
1296 // and FieldIdentifier (struct fields `x`, `y`) as operands
1297 // alongside the existing primitive type names.
1298 assert_eq!(metric.halstead.unique_operands(), 36);
1299 assert_eq!(metric.halstead.total_operands(), 55);
1300 },
1301 );
1302 }
1303
1304 #[cfg(feature = "rust")]
1305 #[test]
1306 fn rust_field_identifier_is_operand() {
1307 // Regression for issue #390: prior to the fix, `FieldIdentifier`
1308 // (e.g. the `x` / `y` in `p.x`, `p.y`) fell through to
1309 // `TokenRole::Unknown`, so the field names were not counted
1310 // as operands. Both C++ and Go already classify FieldIdentifier
1311 // as an operand. After the fix:
1312 // unique operators: fn, (), {}, let, =, +, ;, ., ,, :
1313 // unique operands : main, p, Point, x, y, sum, 0, 1
1314 // Field names `x` and `y` each appear twice (`p.x + p.y` and
1315 // the struct literal `Point { x: 0, y: 1 }`).
1316 check_metrics::<RustParser>(
1317 "fn main() {
1318 let p = Point { x: 0, y: 1 };
1319 let sum = p.x + p.y;
1320 }",
1321 "foo.rs",
1322 |metric| {
1323 // Headline: pre-fix, FieldIdentifier (`x`, `y`) and
1324 // TypeIdentifier (`Point`) fell through to Unknown, so
1325 // u_operands was 5 (main, p, sum, 0, 1). After the
1326 // fix, +Point, +x, +y → 8 distinct names.
1327 assert_eq!(metric.halstead.unique_operands(), 8);
1328 assert_eq!(metric.halstead.total_operands(), 12);
1329 // N1: fn, (), {} ×2, let ×2, = ×2, +, ; ×2, . ×2, `,`,
1330 // and the struct literal's two field `:` (#1486) = 16.
1331 assert_eq!(metric.halstead.unique_operators(), 10);
1332 assert_eq!(metric.halstead.total_operators(), 16);
1333 insta::assert_json_snapshot!(
1334 metric.halstead,
1335 @r#"
1336 {
1337 "unique_operators": 10,
1338 "total_operators": 16,
1339 "unique_operands": 8,
1340 "total_operands": 12,
1341 "length": 28,
1342 "estimated_program_length": 57.219280948873624,
1343 "purity_ratio": 2.043545748174058,
1344 "vocabulary": 18,
1345 "volume": 116.75790004038474,
1346 "difficulty": 7.5,
1347 "level": 0.13333333333333333,
1348 "effort": 875.6842503028855,
1349 "time": 48.64912501682697,
1350 "bugs": 0.03051010983631521
1351 }
1352 "#
1353 );
1354 },
1355 );
1356 }
1357
1358 #[cfg(feature = "rust")]
1359 #[test]
1360 fn rust_type_identifier_is_operand() {
1361 // Regression for issue #390: `TypeIdentifier` (e.g. `Vec`,
1362 // `HashMap`, `String` when used as a path name) was dropped to
1363 // `TokenRole::Unknown` for Rust. C++ and Go classify them as
1364 // operands. After the fix, u_operands = 8:
1365 // main, v, m, Vec, HashMap, new, K, V
1366 // (`i32` is a primitive type, classified as an operator.)
1367 //
1368 // Also covers issue #394: `::` is now an operator. The snippet
1369 // has two `::` tokens (`Vec::new`, `HashMap::new`), so n1 grew
1370 // from 10 → 11 and N1 from 17 → 19. #1486 then billed the two
1371 // annotation `:` tokens as their own entry: 12 and 21. #1559
1372 // billed each generic's brackets as one `<>` pair, replacing the
1373 // `<` and `>` entries and their four occurrences: 11 and 19.
1374 check_metrics::<RustParser>(
1375 "fn main() {
1376 let v: Vec<i32> = Vec::new();
1377 let m: HashMap<K, V> = HashMap::new();
1378 }",
1379 "foo.rs",
1380 |metric| {
1381 // Headline: u_operands includes `Vec`, `HashMap`, `K`,
1382 // `V` (and `i32` as a primitive operator). Without the
1383 // fix, Vec/HashMap/K/V silently dropped to Unknown.
1384 assert_eq!(metric.halstead.unique_operands(), 8);
1385 assert_eq!(metric.halstead.total_operands(), 11);
1386 // `::` appears twice (Vec::new, HashMap::new); without
1387 // the #394 fix u_operators was 10 and operators 17.
1388 // `:` appears twice and stays distinct from `::`.
1389 assert_eq!(metric.halstead.unique_operators(), 11);
1390 assert_eq!(metric.halstead.total_operators(), 19);
1391 insta::assert_json_snapshot!(
1392 metric.halstead,
1393 @r#"
1394 {
1395 "unique_operators": 11,
1396 "total_operators": 19,
1397 "unique_operands": 8,
1398 "total_operands": 11,
1399 "length": 30,
1400 "estimated_program_length": 62.05374780501027,
1401 "purity_ratio": 2.068458260167009,
1402 "vocabulary": 19,
1403 "volume": 127.43782540330756,
1404 "difficulty": 7.5625,
1405 "level": 0.1322314049586777,
1406 "effort": 963.7485546125134,
1407 "time": 53.54158636736186,
1408 "bugs": 0.03252279825177962
1409 }
1410 "#
1411 );
1412 },
1413 );
1414 }
1415
1416 /// #1486: Rust's single `:` is vocabulary in every position it is
1417 /// spelled, and stays a separate entry from `::`.
1418 #[cfg(feature = "rust")]
1419 #[test]
1420 fn rust_colon_is_operator_1486() {
1421 const ROWS: &[(&str, &str, [u64; 2])] = &[
1422 // `let`, `:`, `u8`, `=`, `;`
1423 ("let annotation", "let x: u8 = 1;\n", [5, 5]),
1424 // `struct`, `{}`, `:`, `u8`
1425 ("field declaration", "struct S { a: u8 }\n", [4, 4]),
1426 // `const`, `:` twice, `=`, `{}`, `;`
1427 ("field initializer", "const C: S = S { a: 1 };\n", [5, 6]),
1428 // `fn`, `{}` twice, `()`, `let`, `{}` (pattern), `:`, `=`, `;`
1429 ("field pattern", "fn f() { let S { a: b } = s; }\n", [7, 8]),
1430 // `fn`, `()`, `:`, `u8`, `{}`
1431 ("parameter", "fn f(x: u8) {}\n", [5, 5]),
1432 // `fn`, `<>`, `:`, `()`, `{}`
1433 ("trait bound", "fn f<T: Clone>() {}\n", [5, 5]),
1434 // `fn`, `<>`, `()`, `where`, `:`, `{}`
1435 ("where clause", "fn f<T>() where T: Clone {}\n", [6, 6]),
1436 // `fn`, `()`, `{}` twice, `:`, `loop`, `break`, `;`
1437 ("loop label", "fn f() { 'a: loop { break 'a; } }\n", [7, 8]),
1438 // `fn`, `()`, `{}`, `let`, `=`, `|` twice, `:`, `u8`, `;`
1439 (
1440 "closure parameter",
1441 "fn f() { let c = |q: u8| q; }\n",
1442 [9, 10],
1443 ),
1444 // `fn`, `()` twice, `{}`, `let`, `:`, `<>`, `u8`, `=`, `::`,
1445 // `;` — `:` and `::` are two entries
1446 (
1447 "beside a path",
1448 "fn f() { let v: Vec<u8> = Vec::new(); }\n",
1449 [10, 11],
1450 ),
1451 ];
1452 assert_operator_rows::<RustParser>("foo.rs", ROWS);
1453 }
1454
1455 #[cfg(feature = "rust")]
1456 #[test]
1457 fn rust_path_separator_is_operator() {
1458 // Regression for issue #394: `::` (`COLONCOLON`) was missing
1459 // from the Rust `get_op_type` operator arm even though C++,
1460 // Java, C#, and Kotlin all classify it as an operator. Path-
1461 // heavy code (`std::collections::HashMap`, `Vec::new`,
1462 // `T::method`) had every `::` silently dropped into
1463 // TokenRole::Unknown.
1464 //
1465 // Snippet has three `::` tokens (`std::collections::HashMap`,
1466 // counted as two `::` separators, plus `HashMap::new`).
1467 check_metrics::<RustParser>(
1468 "fn main() {
1469 let m = std::collections::HashMap::new();
1470 }",
1471 "foo.rs",
1472 |metric| {
1473 // `::` appears 3 times across the two path expressions
1474 // (`std::collections::HashMap` contributes two; the
1475 // `HashMap::new` contributes one). Pre-fix all three
1476 // dropped to Unknown: u_operators would be 6 (no `::`
1477 // distinct) and total_operators() would be 7 (minus 3 `::`
1478 // occurrences). With the fix u_operators=7 and
1479 // operators=10.
1480 //
1481 // unique operators (post-fix): fn, LPAREN, LBRACE,
1482 // let, =, ::, ;. unique operands: main, m, std,
1483 // collections, HashMap, new.
1484 assert_eq!(metric.halstead.unique_operators(), 7);
1485 assert_eq!(metric.halstead.total_operators(), 10);
1486 assert_eq!(metric.halstead.unique_operands(), 6);
1487 assert_eq!(metric.halstead.total_operands(), 6);
1488 },
1489 );
1490 }
1491
1492 #[cfg(feature = "rust")]
1493 #[test]
1494 fn rust_declaration_keywords_are_operators() {
1495 // Regression for issue #394: the Rust impl already accepted 17
1496 // keywords as operators (As, Async, Await, …, Fn) but omitted
1497 // 14 declaration / visibility keywords. The fix adds `Const`,
1498 // `Static`, `Enum`, `Struct`, `Trait`, `Impl`, `Use`, `Mod`,
1499 // `Pub`, `Type`, `Union`, `Where`, `Extern`, `Dyn`.
1500 //
1501 // Snippet exercises `use`, `pub`, `struct`, and `impl` (one of
1502 // each); together they account for 4 new operator occurrences
1503 // and 4 new unique operators.
1504 check_metrics::<RustParser>(
1505 "use std::fmt;
1506 pub struct S;
1507 impl S { fn n() -> u8 { 0 } }",
1508 "foo.rs",
1509 |metric| {
1510 // expected: unique operators (11) = use, ::, ;, pub,
1511 // struct, impl, LBRACE, fn, LPAREN, DASHGT, u8. Without
1512 // the #394 fix, `use`, `pub`, `struct`, and `impl`
1513 // would each drop to Unknown and u_operators would be
1514 // 7. unique operands (5): std, fmt, S, n, 0.
1515 assert_eq!(metric.halstead.unique_operators(), 11);
1516 assert_eq!(metric.halstead.total_operators(), 13);
1517 assert_eq!(metric.halstead.unique_operands(), 5);
1518 assert_eq!(metric.halstead.total_operands(), 6);
1519 },
1520 );
1521 }
1522
1523 #[cfg(feature = "javascript")]
1524 #[test]
1525 fn javascript_operators_and_operands() {
1526 check_metrics::<JavascriptParser>(
1527 "function main() {
1528 var a, b, c, avg;
1529 a = 5; b = 5; c = 5;
1530 avg = (a + b + c) / 3;
1531 console.log(\"{}\", avg);
1532 }",
1533 "foo.js",
1534 |metric| {
1535 // unique operators: function, (), {}, var, =, +, /, ,, ., ;
1536 // unique operands: main, a, b, c, avg, 3, 5, console, log, "{}"
1537 // `console.log` is the `.` operator applied to the two
1538 // identifier leaves; the composite `member_expression`
1539 // text is deliberately not a third operand (#1263), so
1540 // n2/N2 are 10/20 rather than the pre-#1263 11/21.
1541 insta::assert_json_snapshot!(
1542 metric.halstead,
1543 @r#"
1544 {
1545 "unique_operators": 10,
1546 "total_operators": 24,
1547 "unique_operands": 10,
1548 "total_operands": 20,
1549 "length": 44,
1550 "estimated_program_length": 66.43856189774725,
1551 "purity_ratio": 1.5099673158578921,
1552 "vocabulary": 20,
1553 "volume": 190.16483617504394,
1554 "difficulty": 10.0,
1555 "level": 0.1,
1556 "effort": 1901.6483617504396,
1557 "time": 105.64713120835775,
1558 "bugs": 0.05116412536051621
1559 }
1560 "#
1561 );
1562 },
1563 );
1564 }
1565
1566 #[cfg(feature = "mozjs")]
1567 #[test]
1568 fn mozjs_operators_and_operands() {
1569 check_metrics::<MozjsParser>(
1570 "function main() {
1571 var a, b, c, avg;
1572 a = 5; b = 5; c = 5;
1573 avg = (a + b + c) / 3;
1574 console.log(\"{}\", avg);
1575 }",
1576 "foo.js",
1577 |metric| {
1578 // unique operators: function, (), {}, var, =, +, /, ,, ., ;
1579 // unique operands: main, a, b, c, avg, 3, 5, console, log, "{}"
1580 // `console.log` is the `.` operator applied to the two
1581 // identifier leaves; the composite `member_expression`
1582 // text is deliberately not a third operand (#1263), so
1583 // n2/N2 are 10/20 rather than the pre-#1263 11/21.
1584 insta::assert_json_snapshot!(
1585 metric.halstead,
1586 @r#"
1587 {
1588 "unique_operators": 10,
1589 "total_operators": 24,
1590 "unique_operands": 10,
1591 "total_operands": 20,
1592 "length": 44,
1593 "estimated_program_length": 66.43856189774725,
1594 "purity_ratio": 1.5099673158578921,
1595 "vocabulary": 20,
1596 "volume": 190.16483617504394,
1597 "difficulty": 10.0,
1598 "level": 0.1,
1599 "effort": 1901.6483617504396,
1600 "time": 105.64713120835775,
1601 "bugs": 0.05116412536051621
1602 }
1603 "#
1604 );
1605 },
1606 );
1607 }
1608
1609 #[cfg(feature = "typescript")]
1610 #[test]
1611 fn typescript_operators_and_operands() {
1612 check_metrics::<TypescriptParser>(
1613 "function main() {
1614 var a, b, c, avg;
1615 a = 5; b = 5; c = 5;
1616 avg = (a + b + c) / 3;
1617 console.log(\"{}\", avg);
1618 }",
1619 "foo.ts",
1620 |metric| {
1621 // unique operators: function, (), {}, var, =, +, /, ,, ., ;
1622 // unique operands: main, a, b, c, avg, 3, 5, console, log, "{}"
1623 // `console.log` is the `.` operator applied to the two
1624 // identifier leaves; the composite `member_expression`
1625 // text is deliberately not a third operand (#1263), so
1626 // n2/N2 are 10/20 rather than the pre-#1263 11/21.
1627 insta::assert_json_snapshot!(
1628 metric.halstead,
1629 @r#"
1630 {
1631 "unique_operators": 10,
1632 "total_operators": 24,
1633 "unique_operands": 10,
1634 "total_operands": 20,
1635 "length": 44,
1636 "estimated_program_length": 66.43856189774725,
1637 "purity_ratio": 1.5099673158578921,
1638 "vocabulary": 20,
1639 "volume": 190.16483617504394,
1640 "difficulty": 10.0,
1641 "level": 0.1,
1642 "effort": 1901.6483617504396,
1643 "time": 105.64713120835775,
1644 "bugs": 0.05116412536051621
1645 }
1646 "#
1647 );
1648 },
1649 );
1650 }
1651
1652 #[cfg(feature = "typescript")]
1653 #[test]
1654 fn tsx_operators_and_operands() {
1655 check_metrics::<TsxParser>(
1656 "function main() {
1657 var a, b, c, avg;
1658 a = 5; b = 5; c = 5;
1659 avg = (a + b + c) / 3;
1660 console.log(\"{}\", avg);
1661 }",
1662 "foo.ts",
1663 |metric| {
1664 // unique operators: function, (), {}, var, =, +, /, ,, ., ;
1665 // unique operands: main, a, b, c, avg, 3, 5, console, log, "{}"
1666 // `console.log` is the `.` operator applied to the two
1667 // identifier leaves; the composite `member_expression`
1668 // text is deliberately not a third operand (#1263), so
1669 // n2/N2 are 10/20 rather than the pre-#1263 11/21.
1670 insta::assert_json_snapshot!(
1671 metric.halstead,
1672 @r#"
1673 {
1674 "unique_operators": 10,
1675 "total_operators": 24,
1676 "unique_operands": 10,
1677 "total_operands": 20,
1678 "length": 44,
1679 "estimated_program_length": 66.43856189774725,
1680 "purity_ratio": 1.5099673158578921,
1681 "vocabulary": 20,
1682 "volume": 190.16483617504394,
1683 "difficulty": 10.0,
1684 "level": 0.1,
1685 "effort": 1901.6483617504396,
1686 "time": 105.64713120835775,
1687 "bugs": 0.05116412536051621
1688 }
1689 "#
1690 );
1691 },
1692 );
1693 }
1694
1695 #[cfg(feature = "javascript")]
1696 #[test]
1697 fn javascript_template_string_plain_is_operand() {
1698 // Regression: issue #192. A backtick-delimited `` `hello` ``
1699 // without `${...}` is semantically identical to `"hello"` /
1700 // `'hello'` and must contribute exactly one operand — before
1701 // the fix `TemplateString` fell through to `TokenRole::Unknown`
1702 // and contributed zero. expected: operands are `f` (function
1703 // name) and the wrapping `` `hello` `` template literal →
1704 // u_operands = 2, N2 = 2 (matches the equivalent
1705 // `function f() { return "hello"; }` baseline).
1706 check_metrics::<JavascriptParser>("function f() { return `hello`; }", "foo.js", |metric| {
1707 assert_eq!(metric.halstead.unique_operands(), 2);
1708 assert_eq!(metric.halstead.total_operands(), 2);
1709 });
1710 }
1711
1712 /// Regression for #695. The `get` / `set` property-accessor keywords
1713 /// are operators, matching the C# getter's `Get | Set | Init | Add |
1714 /// Remove` accessor arm. Before #695 the JS family classified them as
1715 /// operands, so the same accessor keyword landed in opposite Halstead
1716 /// groups across languages. This pins them in the operator store and
1717 /// out of the operand store.
1718 #[cfg(feature = "javascript")]
1719 #[test]
1720 fn js_get_set_accessors_are_operators() {
1721 let source = "class C { get x() { return 1; } set x(v) { this._x = v; } }";
1722 let path = PathBuf::from("foo.js");
1723 let parser = JavascriptParser::new(source.as_bytes().to_vec(), &path, None);
1724 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
1725 assert!(
1726 ops.operators.iter().any(|o| o.as_str() == "get")
1727 && ops.operators.iter().any(|o| o.as_str() == "set"),
1728 "`get`/`set` accessors must be operators; operators were {:?}",
1729 ops.operators
1730 );
1731 assert!(
1732 !ops.operands.iter().any(|o| o.as_str() == "get")
1733 && !ops.operands.iter().any(|o| o.as_str() == "set"),
1734 "`get`/`set` accessors must not be operands; operands were {:?}",
1735 ops.operands
1736 );
1737 }
1738
1739 #[cfg(feature = "javascript")]
1740 #[test]
1741 fn javascript_template_string_interpolation_no_double_count() {
1742 // Regression: issue #192. An interpolated template literal
1743 // `` `Hi ${name}!` `` used to fall through to `Unknown`,
1744 // dropping the wrapper from the count entirely; the inner
1745 // `name` was still walked and counted via the
1746 // `TemplateSubstitution` child. Mirrors #183 (C#), #191
1747 // (Kotlin), #199 (Perl): the wrapper is skipped when a
1748 // `TemplateSubstitution` child is present so the inner
1749 // expression is not double-counted.
1750 //
1751 // expected: for `function f(name) { return ` + "`Hi ${name}!`"
1752 // + `; }`, operands are `f` and `name` (twice — `name` as the
1753 // parameter, then again inside the interpolation), so
1754 // u_operands = 2 and N2 = 3. Without the wrapper-skip guard
1755 // the wrapping literal would also be counted, lifting
1756 // u_operands to 3 and N2 to 4.
1757 check_metrics::<JavascriptParser>(
1758 "function f(name) { return `Hi ${name}!`; }",
1759 "foo.js",
1760 |metric| {
1761 assert_eq!(metric.halstead.unique_operands(), 2);
1762 assert_eq!(metric.halstead.total_operands(), 3);
1763 },
1764 );
1765 }
1766
1767 #[cfg(feature = "mozjs")]
1768 #[test]
1769 fn mozjs_template_string_plain_is_operand() {
1770 // Regression: issue #192. Mirrors
1771 // `javascript_template_string_plain_is_operand` for the
1772 // Firefox-mode dialect — the four JS-family `get_op_type`
1773 // impls share the same template-literal handling.
1774 check_metrics::<MozjsParser>("function f() { return `hello`; }", "foo.js", |metric| {
1775 assert_eq!(metric.halstead.unique_operands(), 2);
1776 assert_eq!(metric.halstead.total_operands(), 2);
1777 });
1778 }
1779
1780 #[cfg(feature = "mozjs")]
1781 #[test]
1782 fn mozjs_template_string_interpolation_no_double_count() {
1783 // Regression: issue #192. Mirrors
1784 // `javascript_template_string_interpolation_no_double_count`
1785 // for the Firefox-mode dialect.
1786 check_metrics::<MozjsParser>(
1787 "function f(name) { return `Hi ${name}!`; }",
1788 "foo.js",
1789 |metric| {
1790 assert_eq!(metric.halstead.unique_operands(), 2);
1791 assert_eq!(metric.halstead.total_operands(), 3);
1792 },
1793 );
1794 }
1795
1796 /// One JSX shape per row of #1483's decision: the source, the
1797 /// expected `n2` / `N2`, and the deduplicated operand vocabulary.
1798 /// Every row is a single element bound to `a`, so the operands
1799 /// outside the element's content are always `a` plus the tag names.
1800 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
1801 const JSX_TEXT_CASES: &[(&str, &str, [u64; 2], &[&str])] = &[
1802 // Element text is one operand. N2: a, p x2 (both tags), hi.
1803 ("text", "const a = <p>hi</p>;\n", [3, 4], &["a", "p", "hi"]),
1804 // The same-line space between `<b/>` and `<i/>` is a jsx_text
1805 // node of its own, and bills nothing. Billing it would key an
1806 // empty operand: (5, 6).
1807 (
1808 "whitespace",
1809 "const a = <p><b/> <i/></p>;\n",
1810 [4, 5],
1811 &["a", "p", "b", "i"],
1812 ),
1813 // `Start` at two indentation depths is one distinct operand,
1814 // because the key is the trimmed text. Untrimmed: (5, 7).
1815 (
1816 "indentation",
1817 "const a = (\n <p>\n Start\n <span>\n Start\n </span>\n </p>\n);\n",
1818 [4, 7],
1819 &["a", "p", "span", "Start"],
1820 ),
1821 // An entity splits the text into three siblings, each an
1822 // operand. Without the entity arm: (4, 5).
1823 (
1824 "entity in text",
1825 "const a = <p>x & y</p>;\n",
1826 [5, 6],
1827 &["a", "p", "x", "&", "y"],
1828 ),
1829 // Inside an attribute string the entity is part of the string
1830 // operand and bills nothing of its own. Without the parent
1831 // guard: (5, 5).
1832 (
1833 "entity in attribute",
1834 "const a = <p title=\"x & y\" />;\n",
1835 [4, 4],
1836 &["a", "p", "title", "\"x & y\""],
1837 ),
1838 // An expression child is unchanged by #1483: its identifier was
1839 // already an operand.
1840 (
1841 "expression",
1842 "const a = <p>{v}</p>;\n",
1843 [3, 4],
1844 &["a", "p", "v"],
1845 ),
1846 ];
1847
1848 /// Asserts every row of [`JSX_TEXT_CASES`] under parser `T`, through
1849 /// the metrics store (`n2` / `N2`) and the `--ops` vocabulary.
1850 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
1851 #[track_caller]
1852 fn assert_jsx_text_operands<T: crate::MetricSuite>(file: &str) {
1853 for (label, source, expected, vocabulary) in JSX_TEXT_CASES {
1854 crate::test_support::check_func_space_only::<T, _>(
1855 source,
1856 file,
1857 &[crate::Metric::Halstead],
1858 |space| {
1859 let halstead = &space.metrics.halstead;
1860 assert_eq!(
1861 [halstead.unique_operands(), halstead.total_operands()],
1862 *expected,
1863 "{file}: {label}"
1864 );
1865 },
1866 );
1867 let ops = ops_of::<T>(source, file);
1868 let got: BTreeSet<&str> = ops.operands.iter().map(String::as_str).collect();
1869 let want: BTreeSet<&str> = vocabulary.iter().copied().collect();
1870 assert_eq!(got, want, "{file}: {label} operand vocabulary");
1871 }
1872 }
1873
1874 /// Regression for #1483: JSX element text and its character
1875 /// references are operands. One row per grammar, so reverting one
1876 /// invocation's `jsx:` argument fails its own row.
1877 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
1878 #[test]
1879 fn jsx_text_is_an_operand() {
1880 // A whitespace-only node bills nothing, so no count can tell the
1881 // "whitespace" row's fixture from one that stopped spelling the
1882 // same-line space. Pin the node's presence directly.
1883 let (_, source, _, _) = JSX_TEXT_CASES
1884 .iter()
1885 .find(|(label, ..)| *label == "whitespace")
1886 .expect("the whitespace row exists");
1887 let parser =
1888 JavascriptParser::new(source.as_bytes().to_vec(), &PathBuf::from("foo.jsx"), None);
1889 assert!(
1890 parser.root().preorder().any(|n| {
1891 n.kind_id() == Javascript::JsxText as u16
1892 && source.as_bytes()[n.start_byte()..n.end_byte()]
1893 .trim_ascii()
1894 .is_empty()
1895 }),
1896 "the whitespace row must carry a whitespace-only jsx_text node"
1897 );
1898
1899 assert_jsx_text_operands::<JavascriptParser>("foo.jsx");
1900 assert_jsx_text_operands::<MozjsParser>("foo.js");
1901 assert_jsx_text_operands::<TsxParser>("foo.tsx");
1902 }
1903
1904 /// The `.ts` grammar's enum carries `JsxText` in the externals it
1905 /// shares with TSX, but no production emits it, which is why the
1906 /// TypeScript invocation passes no `jsx:` argument (#1483). Pinned
1907 /// so a grammar that starts emitting it cannot go unbilled quietly.
1908 #[cfg(feature = "typescript")]
1909 #[test]
1910 fn typescript_never_emits_jsx_text() {
1911 let parser = TypescriptParser::new(
1912 b"const a = <p>hi</p>;\nlet b = <T>(x);\n".to_vec(),
1913 &PathBuf::from("foo.ts"),
1914 None,
1915 );
1916 // The positive half: the parse reached the angle-bracket syntax
1917 // TSX would read as JSX, so the absence below is a property of
1918 // the grammar rather than of a parse that produced nothing.
1919 assert!(
1920 ast_has_kind_id(&parser, Typescript::TypeAssertion as u16),
1921 "fixture lost the `<T>(x)` type assertion"
1922 );
1923 assert!(
1924 !ast_has_kind_id(&parser, Typescript::JsxText as u16),
1925 "the .ts grammar now emits jsx_text; give its invocation a `jsx:` argument"
1926 );
1927 }
1928
1929 #[cfg(feature = "typescript")]
1930 #[test]
1931 fn typescript_template_string_plain_is_operand() {
1932 // Regression: issue #192. Mirrors
1933 // `javascript_template_string_plain_is_operand` for
1934 // TypeScript — the four JS-family `get_op_type` impls share
1935 // the same template-literal handling.
1936 //
1937 // The `: string` annotation contributes no operand — its
1938 // keyword counts once, as the text-keyed operator (#1261) — so
1939 // the operands are `f` and `` `hello` `` (2 each). The headline
1940 // of this test — that the plain template literal contributes
1941 // one operand — is unaffected.
1942 check_metrics::<TypescriptParser>(
1943 "function f(): string { return `hello`; }",
1944 "foo.ts",
1945 |metric| {
1946 assert_eq!(metric.halstead.unique_operands(), 2);
1947 assert_eq!(metric.halstead.total_operands(), 2);
1948 },
1949 );
1950 }
1951
1952 #[cfg(feature = "typescript")]
1953 #[test]
1954 fn typescript_template_string_interpolation_no_double_count() {
1955 // Regression: issue #192. Mirrors
1956 // `javascript_template_string_interpolation_no_double_count`
1957 // for TypeScript.
1958 //
1959 // The `: string` annotations contribute no operands (#1261).
1960 // Unique operands: `f`, `name` (2). Total operands: `f`, `name`
1961 // (param), `name` (in the interpolation) (3). The interpolation
1962 // guard from #192 still holds — the wrapping `` `Hi ${name}!` ``
1963 // is `Unknown`, not double-counted.
1964 check_metrics::<TypescriptParser>(
1965 "function f(name: string): string { return `Hi ${name}!`; }",
1966 "foo.ts",
1967 |metric| {
1968 assert_eq!(metric.halstead.unique_operands(), 2);
1969 assert_eq!(metric.halstead.total_operands(), 3);
1970 },
1971 );
1972 }
1973
1974 #[cfg(feature = "typescript")]
1975 #[test]
1976 fn tsx_template_string_plain_is_operand() {
1977 // Regression: issue #192. Mirrors
1978 // `javascript_template_string_plain_is_operand` for the
1979 // TSX (TypeScript + JSX) variant.
1980 //
1981 // TSX's type-keyword `string` (`String3`) contributes no
1982 // operand, mirroring TS::String2 (#1261): operands are `f` and
1983 // `` `hello` `` (2 each).
1984 check_metrics::<TsxParser>(
1985 "function f(): string { return `hello`; }",
1986 "foo.tsx",
1987 |metric| {
1988 assert_eq!(metric.halstead.unique_operands(), 2);
1989 assert_eq!(metric.halstead.total_operands(), 2);
1990 },
1991 );
1992 }
1993
1994 #[cfg(feature = "typescript")]
1995 #[test]
1996 fn tsx_template_string_interpolation_no_double_count() {
1997 // Regression: issue #192. Mirrors
1998 // `javascript_template_string_interpolation_no_double_count`
1999 // for the TSX (TypeScript + JSX) variant.
2000 //
2001 // The `: string` annotations contribute no `String3` operands
2002 // (#1261); see `typescript_template_string_…` for the count
2003 // derivation.
2004 check_metrics::<TsxParser>(
2005 "function f(name: string): string { return `Hi ${name}!`; }",
2006 "foo.tsx",
2007 |metric| {
2008 assert_eq!(metric.halstead.unique_operands(), 2);
2009 assert_eq!(metric.halstead.total_operands(), 3);
2010 },
2011 );
2012 }
2013
2014 /// The JS-family regex fixture, asserted against all four grammars.
2015 ///
2016 /// `impl_js_family_get_op_type!` is instantiated four times against
2017 /// four distinct `kind_id` enums (`SLASH` 87/81/90/87, `Regex`
2018 /// 224/250/264/225), so each expansion is a separate compiled arm
2019 /// and a drift in one grammar is invisible if only one is checked
2020 /// (grammar-dispatch section 11).
2021 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2022 fn assert_js_family_counts(source: &str, expected: [u64; 4]) {
2023 assert_halstead_counts::<JavascriptParser>(source, "foo.js", expected, "javascript");
2024 assert_halstead_counts::<MozjsParser>(source, "foo.jsm", expected, "mozjs");
2025 assert_halstead_counts::<TypescriptParser>(source, "foo.ts", expected, "typescript");
2026 assert_halstead_counts::<TsxParser>(source, "foo.tsx", expected, "tsx");
2027 }
2028
2029 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2030 #[test]
2031 fn js_family_regex_delimiters_are_not_operators() {
2032 // Regression: issue #1314, the JS-family sibling of Elixir
2033 // #1256 and Ruby/Perl #1312. A `regex` literal spells both of
2034 // its delimiters `SLASH` — the kind id real division uses — so
2035 // `const a = /abc/g;` reported a `/` operator with no division
2036 // in the source, and n1/N1 counted the literal's punctuation as
2037 // arithmetic.
2038 //
2039 // The same fixture pins the second, independent half: `Regex`
2040 // was in neither arm, so the literal contributed no operand
2041 // either and reached the vocabulary from *neither* side.
2042 //
2043 // expected: operators `const`, `=`, `;`, `let` → n1 = 4;
2044 // `const` `=` `;` on line 1, `let` `=` `;` on line 2, `=` `;`
2045 // on line 3 → N1 = 8. Operands `a`, `/abc/g`, `b` → n2 = 3,
2046 // with `a` used three times and `b` twice → N2 = 6.
2047 //
2048 // Before the fix: n1 = 5 and N1 = 10 (the two fabricated `/`),
2049 // n2 = 2 and N2 = 5 (no operand for the literal).
2050 //
2051 // The four values are deliberately distinct so no transposition
2052 // of the unique-vs-total axes inside `assert_halstead_counts`
2053 // can pass (#1312).
2054 assert_js_family_counts("const a = /abc/g;\nlet b = a;\nb = a;\n", [4, 8, 3, 6]);
2055 }
2056
2057 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2058 #[test]
2059 fn js_family_division_survives_the_regex_guard() {
2060 // Control for #1314: the guard is scoped to a `Regex` parent,
2061 // so real division must still count. This fixture holds both
2062 // sides at once — two divisions and one regex literal — so a
2063 // guard widened to every `SLASH` fails here rather than
2064 // silently passing the test above.
2065 //
2066 // expected: operators `const`, `=`, `;`, `/` → n1 = 4; two
2067 // `const`, two `=`, two `;` and two `/` → N1 = 8. Operands
2068 // `q`, `a`, `b`, `c`, `r`, `/x/` → n2 = N2 = 6.
2069 assert_js_family_counts("const q = a / b / c;\nconst r = /x/;\n", [4, 8, 6, 6]);
2070 }
2071
2072 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2073 #[test]
2074 fn js_regex_delimiter_guard_is_parent_scoped_is_unobservable() {
2075 // Companion to the two above, and a statement of what they do
2076 // *not* cover. Ruby's guard has
2077 // `ruby_regex_guard_is_parent_scoped_not_ancestor_scoped`
2078 // because a division inside `#{…}` sits under a `Regex`
2079 // ancestor without being its child. No JS fixture can do that:
2080 // a regex literal admits no nested expression at all, its
2081 // `regex_pattern` and `regex_flags` children being leaves. So
2082 // the ancestor-scoped mutant of this guard — the one #1256's
2083 // post-mortem says survives every ordinary fixture — is
2084 // unobservable here. Measured, not assumed.
2085 //
2086 // Rather than write a fixture that would pass under both
2087 // spellings and read as coverage, pin the grammar property the
2088 // claim rests on: within a fixture that puts a division, a
2089 // template substitution and a regex in one file, every `SLASH`
2090 // reachable *below* a `Regex` is its immediate child. Should a
2091 // bump start nesting expressions inside a regex, this turns red
2092 // and the distinction becomes both observable and worth a real
2093 // test.
2094 //
2095 // Checked against all four grammars, not just JavaScript: the
2096 // guard is instantiated four times against four distinct enums,
2097 // and the property this test exists to watch could hold in one
2098 // and lapse in another.
2099 let source = b"const a = /abc/g;\nconst q = x / y;\nconst t = `p ${x / y} ${/zz/} q`;\n";
2100 assert_regex_slashes_are_immediate_children::<crate::langs::JavascriptCode>(
2101 source,
2102 Javascript::SLASH as u16,
2103 Javascript::Regex as u16,
2104 "javascript",
2105 );
2106 assert_regex_slashes_are_immediate_children::<crate::langs::MozjsCode>(
2107 source,
2108 Mozjs::SLASH as u16,
2109 Mozjs::Regex as u16,
2110 "mozjs",
2111 );
2112 assert_regex_slashes_are_immediate_children::<crate::langs::TypescriptCode>(
2113 source,
2114 Typescript::SLASH as u16,
2115 Typescript::Regex as u16,
2116 "typescript",
2117 );
2118 assert_regex_slashes_are_immediate_children::<crate::langs::TsxCode>(
2119 source,
2120 Tsx::SLASH as u16,
2121 Tsx::Regex as u16,
2122 "tsx",
2123 );
2124 }
2125
2126 /// Asserts every `slash` token below a `regex` node in `source` is
2127 /// that node's *immediate* child, for one grammar.
2128 ///
2129 /// Backs `js_regex_delimiter_guard_is_parent_scoped_is_unobservable`
2130 /// — see there for why the property is worth pinning.
2131 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2132 fn assert_regex_slashes_are_immediate_children<L: crate::traits::LanguageInfo>(
2133 source: &[u8],
2134 slash: u16,
2135 regex: u16,
2136 label: &str,
2137 ) {
2138 let mut slashes_below_a_regex = 0;
2139 let visited = for_each_node_with_chain::<L>(source, |node: &Node<'_>, chain| {
2140 if node.kind_id() != slash {
2141 return;
2142 }
2143 let Some(depth) = chain.iter().position(|a| a.kind_id() == regex) else {
2144 return;
2145 };
2146 slashes_below_a_regex += 1;
2147 assert_eq!(
2148 depth,
2149 chain.len() - 1,
2150 "{label}: a slash at row {} has a regex ancestor that is not its parent, so \
2151 the parent-vs-ancestor mutant is now observable and needs a real test",
2152 node.start_row()
2153 );
2154 });
2155 assert!(visited > 20, "{label}: fixture is too small to prove much");
2156 // Without this the assertion above is vacuous whenever the
2157 // fixture stops containing a regex at all — the failure mode a
2158 // filter that matches nothing always has.
2159 assert_eq!(
2160 slashes_below_a_regex, 4,
2161 "{label}: expected the two regex literals' four delimiters; the fixture no \
2162 longer exercises what this test claims"
2163 );
2164 }
2165
2166 // Issue #281: optional chaining (`?.`) was double-counted as a
2167 // Halstead operator in TypeScript and TSX because the grammar
2168 // exposes both an `optional_chain` named wrapper AND a child
2169 // `?.` token, and both were classified as `Operator`. The fix
2170 // counts only the bare `?.` token (`QMARKDOT`) in TS/TSX so each
2171 // textual `?.` contributes exactly once, matching JS / MozJS
2172 // (whose grammars expose only `OptionalChain` — the `?.` token
2173 // itself).
2174 //
2175 // The four assertions below all compare against the same totals:
2176 // for `function f(a) { return a?.b?.c; }` the operator stream is
2177 // `function`, `(`, `{`, `return`, `?.`, `?.`, `;` (7 total, 6
2178 // unique — `LPAREN`/`LBRACE` count once, closing tokens are not
2179 // in the operator set). Before the fix, TS/TSX reported 9/7
2180 // instead of 7/6.
2181 #[cfg(feature = "javascript")]
2182 #[test]
2183 fn javascript_optional_chain_not_double_counted_in_halstead_281() {
2184 check_metrics::<JavascriptParser>("function f(a) { return a?.b?.c; }", "foo.js", |m| {
2185 assert_eq!(m.halstead.unique_operators(), 6);
2186 assert_eq!(m.halstead.total_operators(), 7);
2187 });
2188 }
2189
2190 #[cfg(feature = "mozjs")]
2191 #[test]
2192 fn mozjs_optional_chain_not_double_counted_in_halstead_281() {
2193 check_metrics::<MozjsParser>("function f(a) { return a?.b?.c; }", "foo.js", |m| {
2194 assert_eq!(m.halstead.unique_operators(), 6);
2195 assert_eq!(m.halstead.total_operators(), 7);
2196 });
2197 }
2198
2199 #[cfg(feature = "typescript")]
2200 #[test]
2201 fn typescript_optional_chain_not_double_counted_in_halstead_281() {
2202 // The TS grammar wraps member-expression `?.` in an
2203 // `optional_chain` named node containing the bare `?.`
2204 // token; classifying both as `Operator` double-counted the
2205 // chain. We now count only the bare token, so TS matches JS.
2206 check_metrics::<TypescriptParser>("function f(a) { return a?.b?.c; }", "foo.ts", |m| {
2207 assert_eq!(m.halstead.unique_operators(), 6);
2208 assert_eq!(m.halstead.total_operators(), 7);
2209 });
2210 }
2211
2212 #[cfg(feature = "typescript")]
2213 #[test]
2214 fn tsx_optional_chain_not_double_counted_in_halstead_281() {
2215 check_metrics::<TsxParser>("function f(a) { return a?.b?.c; }", "foo.tsx", |m| {
2216 assert_eq!(m.halstead.unique_operators(), 6);
2217 assert_eq!(m.halstead.total_operators(), 7);
2218 });
2219 }
2220
2221 // Issue #299: parity guard for the JS-family `get_op_type` macro
2222 // on the optional-chain operator token (#281's prior regression
2223 // surface). All four languages must classify the bare `?.` token
2224 // identically — `OptionalChain` in JS/MozJS, `QMARKDOT` in
2225 // TS/TSX — and emit the same totals for
2226 // `function f(a) { return a?.b?.c; }`:
2227 //
2228 // * Operators: `function`, `(`, `{`, `return`, `?.`, `?.`, `;`
2229 // (7 total, 6 unique).
2230 // * Operands: `f`, `a` (parameter), `a`, `b`, `c` — the identifier
2231 // and property leaves only (5 total, 4 unique). Until #1263 the
2232 // two wrapping member expressions (`a?.b`, `a?.b?.c`) were
2233 // classified as `MemberExpression*` operands on top of the leaves
2234 // they contain, making this 7 total / 6 unique.
2235 //
2236 // Verified by test-via-revert: dropping `OptionalChain` from
2237 // JS/MozJS, or `QMARKDOT` from TS/TSX, trips the test
2238 // (u_operators 6→5). This input does NOT exercise every operand
2239 // alias in the per-language `operand_extras` (`Identifier2`, the
2240 // JS/MozJS/TSX string-literal `String2`); drift in
2241 // those is out of scope for this regression guard and would need a
2242 // separate fixture. The `PredefinedType` operator path (`: void`
2243 // double-count) is now covered by `ts_void_return_type_single_operator_453`
2244 // below.
2245 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2246 #[test]
2247 fn js_family_get_op_type_parity_optional_chain_member_299() {
2248 // Non-capturing closure (coerced to the `fn` pointer that
2249 // `check_metrics` accepts) avoids the
2250 // `clippy::needless_pass_by_value` warning that a free `fn`
2251 // taking `CodeMetrics` by value would trigger.
2252 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2253 const SRC: &str = "function f(a) { return a?.b?.c; }";
2254 let check = |m: crate::CodeMetrics| {
2255 assert_eq!(m.halstead.unique_operators(), 6);
2256 assert_eq!(m.halstead.total_operators(), 7);
2257 assert_eq!(m.halstead.unique_operands(), 4);
2258 assert_eq!(m.halstead.total_operands(), 5);
2259 };
2260
2261 check_metrics::<JavascriptParser>(SRC, "foo.js", check);
2262 check_metrics::<MozjsParser>(SRC, "foo.js", check);
2263 check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2264 check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2265 }
2266
2267 // Issue #1263: a member access contributes its leaves and the `.`
2268 // operator, never the `member_expression` composite as well. The
2269 // classification does not stop the walk, so `a` and `b` were always
2270 // counted; listing the wrapper billed a third operand keyed on the
2271 // whole `a.b` text, which no other language here does.
2272 //
2273 // expected, for `var r = a.b;`:
2274 //
2275 // * Operators: `var`, `=`, `.`, `;` — 4 total, 4 unique.
2276 // * Operands: `r`, `a`, `b` — 3 total, 3 unique. Before the fix
2277 // the `member_expression` wrapper added `a.b`, making both 4.
2278 //
2279 // All four JS-family languages are asserted because
2280 // `impl_js_family_get_op_type!` emits one shared operand arm: the
2281 // lockstep is the point of the macro, and a per-language extras
2282 // list is exactly where a future edit could break it.
2283 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2284 #[test]
2285 fn js_family_member_access_counts_leaves_not_the_composite_1263() {
2286 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2287 const SRC: &str = "var r = a.b;";
2288 let check = |m: crate::CodeMetrics| {
2289 assert_eq!(m.halstead.unique_operators(), 4);
2290 assert_eq!(m.halstead.total_operators(), 4);
2291 assert_eq!(m.halstead.unique_operands(), 3);
2292 assert_eq!(m.halstead.total_operands(), 3);
2293 };
2294
2295 check_metrics::<JavascriptParser>(SRC, "foo.js", check);
2296 check_metrics::<MozjsParser>(SRC, "foo.js", check);
2297 check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2298 check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2299 }
2300
2301 // Issue #1263, the grammar-dispatch section 6 half: dropping
2302 // `MemberExpression*` from the operand arm would have regressed
2303 // private-field access to *zero* operands for the field, because
2304 // `PrivatePropertyIdentifier` — the `#x` leaf — was in no operand
2305 // list and the composite `this.#x` had been its only count. Adding
2306 // the leaf also fixes the declaration site `#x = 1`, which no
2307 // wrapper covered and which therefore counted nothing at all.
2308 //
2309 // expected, for `class C { #x = 1; m() { return this.#x; } }`:
2310 //
2311 // * Operators: `class`, `{`×2 (class body, method body), `=`,
2312 // `;`×2, `(`, `return`, `.` — 9 total, 7 unique. (`class` joined
2313 // the JS-family operator arm in #1552.)
2314 // * Operands: `C`, `#x`, `1`, `m`, `this`, `#x` — 6 total, 5
2315 // unique in all four grammars. Under TS/TSX the class *name* `C`
2316 // is a `type_identifier`, which those getters did not classify
2317 // until #1557, so they read 5/4 before it.
2318 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2319 #[test]
2320 fn js_family_private_field_leaf_is_the_operand_1263() {
2321 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2322 const SRC: &str = "class C { #x = 1; m() { return this.#x; } }";
2323 let check = |m: crate::CodeMetrics| {
2324 assert_eq!(m.halstead.unique_operators(), 7);
2325 assert_eq!(m.halstead.total_operators(), 9);
2326 assert_eq!(m.halstead.unique_operands(), 5);
2327 assert_eq!(m.halstead.total_operands(), 6);
2328 };
2329
2330 check_metrics::<JavascriptParser>(SRC, "foo.js", check);
2331 check_metrics::<MozjsParser>(SRC, "foo.js", check);
2332 check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2333 check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2334 }
2335
2336 // Issue #1263, the other section 6 half: `meta_property` is the one
2337 // composite the leaves-not-composites drop has to keep. `import.meta`
2338 // / `new.target` have no classified leaf — `meta` and `target` are
2339 // anonymous tokens in no arm — so with `MemberExpression*` gone the
2340 // meta-object contributed no operand at all while `this.env.x` still
2341 // yielded three.
2342 //
2343 // expected operands, for `var t = import.meta.url; function f() {
2344 // return new.target; }`: `t`, `import.meta`, `url`, `f`,
2345 // `new.target` — 5 total, 5 unique. Operators are deliberately not
2346 // asserted: the `import` / `new` keyword tokens inside the
2347 // meta-property keep their pre-existing operator classification,
2348 // which this fixture neither pins nor contests.
2349 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2350 #[test]
2351 fn js_family_meta_property_is_one_operand_1263() {
2352 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2353 const SRC: &str = "var t = import.meta.url; function f() { return new.target; }";
2354 let check = |m: crate::CodeMetrics| {
2355 assert_eq!(m.halstead.unique_operands(), 5);
2356 assert_eq!(m.halstead.total_operands(), 5);
2357 };
2358
2359 check_metrics::<JavascriptParser>(SRC, "foo.js", check);
2360 check_metrics::<MozjsParser>(SRC, "foo.js", check);
2361 check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2362 check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2363 }
2364
2365 // Issue #1263: TS/TSX `nested_identifier` (`namespace N.M`) is the
2366 // same container/leaf double-count as `member_expression`.
2367 //
2368 // expected, for `namespace N.M { }`:
2369 //
2370 // * Operators: `namespace`, `.`, `{` — 3 total, 3 unique.
2371 // (`namespace` joined the TS operator extras in #1557.)
2372 // * Operands: `N`, `M` — 2 total, 2 unique. Before the fix the
2373 // `nested_identifier` added `N.M`, making both 3.
2374 #[cfg(feature = "typescript")]
2375 #[test]
2376 fn ts_nested_identifier_counts_leaves_not_the_composite_1263() {
2377 const SRC: &str = "namespace N.M { }";
2378 let check = |m: crate::CodeMetrics| {
2379 assert_eq!(m.halstead.unique_operators(), 3);
2380 assert_eq!(m.halstead.total_operators(), 3);
2381 assert_eq!(m.halstead.unique_operands(), 2);
2382 assert_eq!(m.halstead.total_operands(), 2);
2383 };
2384
2385 check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2386 check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2387 }
2388
2389 // Issue #1261 (inverting the #313 pin): the `"string"` type-keyword
2390 // aliases the TS / TSX grammars expose must contribute NO operand.
2391 // #313 put them in `operand_extras` for parity with the then-wider
2392 // `Checker::is_string`, but the `predefined_type` wrapper already
2393 // counts as the text-keyed `"string"` operator, so one `: string`
2394 // token tallied as operator AND operand while `: number` counted
2395 // once. #1261 drops the aliases from both `operand_extras` and
2396 // `is_string`, so the keyword counts once, as the operator.
2397 //
2398 // For the input `let x: string = "y";`:
2399 //
2400 // * TypeScript emits `Typescript::String2` for the `string` type
2401 // keyword (kind_id 135, in the type-keyword block of the enum).
2402 // * TSX emits `Tsx::String3` for the same role (kind_id 141).
2403 //
2404 // Verified by test-via-revert: restoring `String2` to TS's
2405 // `operand_extras` (or `String3` to TSX's) trips this test on
2406 // `u_operands` / `operands` for the affected language.
2407 #[cfg(feature = "typescript")]
2408 #[test]
2409 fn ts_family_type_keyword_counts_once_1261() {
2410 const SRC: &str = "let x: string = \"y\";";
2411 // Operators (n1 = 5, N1 = 5):
2412 // `let`, `:`, `=`, `;`, plus `string` (PredefinedType wrapper,
2413 // routed through `is_primitive` so it's keyed by its lexeme
2414 // `"string"` in `primitive_operators`).
2415 // Operands (n2 = 2, N2 = 2):
2416 // `x` and the `"y"` literal. Under #313 the type-keyword
2417 // child of `predefined_type` added a third, phantom
2418 // `"string"` operand (n2 = 3 / N2 = 3).
2419 let check = |m: crate::CodeMetrics| {
2420 assert_eq!(m.halstead.unique_operators(), 5);
2421 assert_eq!(m.halstead.total_operators(), 5);
2422 assert_eq!(m.halstead.unique_operands(), 2);
2423 assert_eq!(m.halstead.total_operands(), 2);
2424 };
2425
2426 check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2427 check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2428 }
2429
2430 // Issue #1261 regression, the issue's reproducer plus a literal
2431 // whose contents spell the keyword: `: string` and `: number` must
2432 // contribute symmetrically — one text-keyed operator each, zero
2433 // operands — while a string *literal* `"string"` stays an operand
2434 // (distinct from the keyword: TS kind `String`, TSX kind `String2`,
2435 // both quoted in the operand key).
2436 #[cfg(feature = "typescript")]
2437 #[test]
2438 fn ts_family_string_annotation_symmetric_with_number_1261() {
2439 const SRC: &str = "let x: string = \"a\";\nlet y: number = 1;\nlet s = \"string\";";
2440 // Operators (n1 = 6, N1 = 13):
2441 // `let` ×3, `:` ×2, `=` ×3, `;` ×3, `string` ×1, `number` ×1.
2442 // Pre-fix N1 was identical — the wrapper operator was always
2443 // counted; the defect was the extra operand below.
2444 // Operands (n2 = 6, N2 = 6):
2445 // `x`, `"a"`, `y`, `1`, `s`, `"string"` — one each. Pre-fix
2446 // the `: string` keyword added a bare `string` operand
2447 // (n2 = 7 / N2 = 7) that `: number` had no analogue of.
2448 let check = |m: crate::CodeMetrics| {
2449 assert_eq!(m.halstead.unique_operators(), 6);
2450 assert_eq!(m.halstead.total_operators(), 13);
2451 assert_eq!(m.halstead.unique_operands(), 6);
2452 assert_eq!(m.halstead.total_operands(), 6);
2453 };
2454
2455 check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2456 check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2457 }
2458
2459 /// Drift marker for #1261 (lesson 34 / grammar-dispatch §2): the
2460 /// anonymous `string` type-keyword token appears **only** as a
2461 /// `predefined_type` child.
2462 ///
2463 /// That is the whole argument for classifying the keyword without a
2464 /// parent guard — the wrapper is guaranteed to be there to carry the
2465 /// operator. The two tests above measure the consequence and would
2466 /// still pass if the grammar started emitting the keyword somewhere
2467 /// else, as long as their own two fixtures kept their counts; this one
2468 /// measures the premise, over every position #1261's dump probes
2469 /// covered: annotation, parameter and return type, union member,
2470 /// generic argument, and template-literal type. A string *literal*
2471 /// spelling `"string"` is a different kind and must not be confused
2472 /// for the keyword, so one is in the fixture too.
2473 #[cfg(feature = "typescript")]
2474 #[test]
2475 fn ts_family_type_keyword_only_appears_under_predefined_type_1261() {
2476 // Exercises each position the keyword can take. Valid in both
2477 // grammars: no angle-bracket cast, which TSX would read as JSX.
2478 const SRC: &str = "const a: string = \"string\";\n\
2479 function f(x: string): string {\n\
2480 return x;\n\
2481 }\n\
2482 type U = string | number;\n\
2483 type A = Array<string>;\n\
2484 type M = Map<string, number>;\n\
2485 type T = `id-${string}`;\n";
2486 // Seven type positions: `a`, `x`, `f`'s return, the union member,
2487 // `Array`'s argument, `Map`'s first argument, and the template
2488 // placeholder. The `"string"` initialiser is a literal, not the
2489 // keyword, and must not be among them.
2490 const EXPECTED_OCCURRENCES: usize = 7;
2491
2492 fn keyword_occurrences<P: ParserTrait>(
2493 path: &str,
2494 keyword: u16,
2495 predefined_type: u16,
2496 ) -> usize {
2497 let parser = P::new(SRC.as_bytes().to_vec(), &PathBuf::from(path), None);
2498 parser
2499 .root()
2500 .preorder()
2501 .filter(|node| node.kind_id() == keyword)
2502 .inspect(|node| {
2503 assert_eq!(
2504 node.parent().map(|parent| parent.kind_id()),
2505 Some(predefined_type),
2506 "the `string` type keyword surfaced outside \
2507 `predefined_type` in {path}; the operator is carried \
2508 by the wrapper, so `get_op_type` needs a parent guard \
2509 before that arm can be trusted (#1261)",
2510 );
2511 })
2512 .count()
2513 }
2514
2515 // Kind ids re-read from the generated enums, not carried over: TS
2516 // `String2` = 135, TSX `String3` = 141 (TSX's `String2` = 261 is the
2517 // string-literal production and stays an operand).
2518 assert_eq!(
2519 keyword_occurrences::<TypescriptParser>(
2520 "foo.ts",
2521 Typescript::String2 as u16,
2522 Typescript::PredefinedType as u16,
2523 ),
2524 EXPECTED_OCCURRENCES,
2525 "TypeScript no longer emits the `string` type keyword in every \
2526 position #1261 probed",
2527 );
2528 assert_eq!(
2529 keyword_occurrences::<TsxParser>(
2530 "foo.tsx",
2531 Tsx::String3 as u16,
2532 Tsx::PredefinedType as u16,
2533 ),
2534 EXPECTED_OCCURRENCES,
2535 "TSX no longer emits the `string` type keyword in every position \
2536 #1261 probed",
2537 );
2538 }
2539
2540 // Issue #453: a `void` return type must contribute exactly one
2541 // Halstead operator. The TS / TSX grammars parse `: void` as a
2542 // `predefined_type` wrapper around an inner `void` token. `is_primitive`
2543 // routes the wrapper into the text-keyed `primitive_operators` map as
2544 // `"void"`, while the inner `Void` token is independently a standalone
2545 // expression operator (`void 0`). Pre-fix both classified as operators
2546 // and one source `void` counted as TWO distinct Halstead operators.
2547 // The fix suppresses the wrapper when its child is a `Void` token, so
2548 // only the inner token carries the operator — matching expression
2549 // `void 0` and keeping the kind_id-keyed count consistent.
2550 //
2551 // For `function f(): void { return; }`:
2552 //
2553 // * Operators (n1 = 7, N1 = 7): `function`, `()`, `{}`, `:`, `return`,
2554 // `;`, and a single `void`. (The untyped form is n1 = 5; the `: void`
2555 // annotation adds the `:` operator and one `void`, NOT two — the
2556 // issue's "n1 = 6" target overlooked the annotation colon.)
2557 //
2558 // Verified by test-via-revert: removing the `predefined_void` guard
2559 // restores the pre-fix `u_operators` 7 -> 8 with a duplicate `"void"`
2560 // (one kind_id-keyed, one in `primitive_operators`). Both `metrics()`
2561 // and the `ops`-list dedup invariant (`ts_void_return_and_expression_*`
2562 // in `ops.rs`) are pinned per lesson 4.
2563 #[cfg(feature = "typescript")]
2564 #[test]
2565 fn ts_void_return_type_single_operator_453() {
2566 const SRC: &str = "function f(): void { return; }";
2567 let check = |m: crate::CodeMetrics| {
2568 assert_eq!(m.halstead.unique_operators(), 7);
2569 assert_eq!(m.halstead.total_operators(), 7);
2570 };
2571
2572 check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2573 check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2574 }
2575
2576 // Issue #453 over-suppression guard: expression `void 0` (a
2577 // `unary_expression`, NOT a `predefined_type` wrapper) must still
2578 // count `void` as exactly one operator. The fix keys only on a
2579 // `predefined_type` whose child is a `Void` token, so the bare
2580 // expression operator is untouched.
2581 //
2582 // For `const x = void 0;`:
2583 //
2584 // * Operators (n1 = 4, N1 = 4): `const`, `=`, `void`, `;`.
2585 // * Operands (n2 = 2, N2 = 2): `x`, `0`.
2586 #[cfg(feature = "typescript")]
2587 #[test]
2588 fn ts_void_expression_still_single_operator_453() {
2589 const SRC: &str = "const x = void 0;";
2590 let check = |m: crate::CodeMetrics| {
2591 assert_eq!(m.halstead.unique_operators(), 4);
2592 assert_eq!(m.halstead.total_operators(), 4);
2593 assert_eq!(m.halstead.unique_operands(), 2);
2594 assert_eq!(m.halstead.total_operands(), 2);
2595 };
2596
2597 check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2598 check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2599 }
2600
2601 #[cfg(feature = "python")]
2602 #[test]
2603 fn python_wrong_operators() {
2604 // Not valid Python — the parser recovers `()` as a tuple, the `[`
2605 // as a bare token and `{}` as a dict — but each opener is still a
2606 // delimiter the source spells, so since #1486 the three are
2607 // billed once each as `()`, `[]` and `{}`. The closers never are.
2608 check_metrics::<PythonParser>("()[]{}", "foo.py", |metric| {
2609 assert_eq!(metric.halstead.unique_operators(), 3);
2610 assert_eq!(metric.halstead.total_operators(), 3);
2611 insta::assert_json_snapshot!(
2612 metric.halstead,
2613 @r#"
2614 {
2615 "unique_operators": 3,
2616 "total_operators": 3,
2617 "unique_operands": 0,
2618 "total_operands": 0,
2619 "length": 3,
2620 "estimated_program_length": 4.754887502163468,
2621 "purity_ratio": 1.584962500721156,
2622 "vocabulary": 3,
2623 "volume": 4.754887502163468,
2624 "difficulty": 0.0,
2625 "level": 0.0,
2626 "effort": 0.0,
2627 "time": 0.0,
2628 "bugs": 0.0
2629 }
2630 "#
2631 );
2632 });
2633 }
2634
2635 #[cfg(feature = "python")]
2636 #[test]
2637 fn python_check_metrics() {
2638 check_metrics::<PythonParser>(
2639 "def f():
2640 pass",
2641 "foo.py",
2642 |metric| {
2643 // expected operators: def, (), :, pass — one each (#1486
2644 // added the `()` and the block `:`).
2645 assert_eq!(metric.halstead.unique_operators(), 4);
2646 assert_eq!(metric.halstead.total_operators(), 4);
2647 insta::assert_json_snapshot!(
2648 metric.halstead,
2649 @r#"
2650 {
2651 "unique_operators": 4,
2652 "total_operators": 4,
2653 "unique_operands": 1,
2654 "total_operands": 1,
2655 "length": 5,
2656 "estimated_program_length": 8.0,
2657 "purity_ratio": 1.6,
2658 "vocabulary": 5,
2659 "volume": 11.60964047443681,
2660 "difficulty": 2.0,
2661 "level": 0.5,
2662 "effort": 23.21928094887362,
2663 "time": 1.289960052715201,
2664 "bugs": 0.002712967490108627
2665 }
2666 "#
2667 );
2668 },
2669 );
2670 }
2671
2672 #[cfg(feature = "java")]
2673 #[test]
2674 fn java_operators_and_operands() {
2675 check_metrics::<JavaParser>(
2676 "public class Main {
2677 public static void main(string args[]) {
2678 int a, b, c, avg;
2679 a = 5; b = 5; c = 5;
2680 avg = (a + b + c) / 3;
2681 MessageFormat.format(\"{0}\", avg);
2682 }
2683 }",
2684 "foo.java",
2685 |metric| {
2686 // Operators (n1=12): class {} void () [] , . ; int = + /
2687 // Operands (n2=13): Main main string args a b c avg 5 3 MessageFormat format "{0}"
2688 // (`string` is the parameter's type name, an operand since #1560)
2689 insta::assert_json_snapshot!(
2690 metric.halstead,
2691 @r#"
2692 {
2693 "unique_operators": 12,
2694 "total_operators": 27,
2695 "unique_operands": 13,
2696 "total_operands": 23,
2697 "length": 50,
2698 "estimated_program_length": 91.12526634448807,
2699 "purity_ratio": 1.8225053268897613,
2700 "vocabulary": 25,
2701 "volume": 232.19280948873623,
2702 "difficulty": 10.615384615384615,
2703 "level": 0.09420289855072464,
2704 "effort": 2464.8159776496614,
2705 "time": 136.93422098053674,
2706 "bugs": 0.06082308053869885
2707 }
2708 "#
2709 );
2710 },
2711 );
2712 }
2713
2714 #[cfg(feature = "java")]
2715 #[test]
2716 fn java_primitive_types_and_booleans() {
2717 check_metrics::<JavaParser>(
2718 "public class Prims {
2719 byte a = 1;
2720 short b = 2;
2721 int c = 3;
2722 long d = 4;
2723 char e = 'x';
2724 float f = 1.0f;
2725 double g = 2.0;
2726 boolean h = true;
2727 boolean i = false;
2728 }",
2729 "foo.java",
2730 |metric| {
2731 // Verifies all 8 Java primitive-type keywords (byte, short, int, long,
2732 // char, float, double, boolean) are counted as distinct operators, and
2733 // that true/false are counted as operands. The other four operators
2734 // are `class` (#1552), `{}`, `=` and `;`.
2735 insta::assert_json_snapshot!(
2736 metric.halstead,
2737 @r#"
2738 {
2739 "unique_operators": 12,
2740 "total_operators": 29,
2741 "unique_operands": 19,
2742 "total_operands": 19,
2743 "length": 48,
2744 "estimated_program_length": 123.730172764082,
2745 "purity_ratio": 2.5777119325850415,
2746 "vocabulary": 31,
2747 "volume": 237.80142289857,
2748 "difficulty": 6.0,
2749 "level": 0.16666666666666666,
2750 "effort": 1426.80853739142,
2751 "time": 79.26714096619,
2752 "bugs": 0.04224635182363915
2753 }
2754 "#
2755 );
2756 },
2757 );
2758 }
2759
2760 // #1380: `this` and `super` were swept into the operator arm under a
2761 // `// Operator: … keywords` heading — classification by lexical
2762 // class rather than by Halstead role. A member access is
2763 // `<receiver> <op> <field>`, so billing the receiver as an operator
2764 // scored `this.x` as a binary operator with one operand while `p.x`
2765 // is one operator and two operands. Both are operands now, matching
2766 // the eleven other languages that classify a self-reference.
2767 #[cfg(feature = "java")]
2768 #[test]
2769 fn java_self_and_super_references_are_operands() {
2770 let source = "class T {\n int f() { return this.x + super.y; }\n}";
2771 // Operators (n1=8, N1=10): class (#1552), {} x2, int, (),
2772 // return, . x2, +, ;
2773 // Operands (n2=6, N2=6): T, f, this, x, super, y
2774 // Before the fix this read (10, 12, 4, 4) — `this` and `super`
2775 // billed into the operator side of both counts.
2776 assert_halstead_counts::<JavaParser>(
2777 source,
2778 "foo.java",
2779 [8, 10, 6, 6],
2780 "java self/super references",
2781 );
2782 assert_keywords_are_operands_only::<JavaParser>(source, "foo.java", &["this", "super"]);
2783 }
2784
2785 // The `super` of a wildcard type bound denotes no value, so it keeps
2786 // the operator classification `? extends T`'s `extends` already has
2787 // — the two keywords are siblings under the same `wildcard` node and
2788 // nothing but the parent separates them from a real self-reference
2789 // (#1380). The explicit receiver parameter in the same fixture is
2790 // the independent path through the operand arm (grammar-dispatch
2791 // section 11): only it can put `this` in the operand vocabulary
2792 // here, and only the wildcard can put `super` in the operator one.
2793 #[cfg(feature = "java")]
2794 #[test]
2795 fn java_wildcard_super_bound_stays_an_operator() {
2796 let source = "import java.util.List;\n\
2797 class T {\n \
2798 void m(T T.this, List<? super String> a, List<? extends String> b) { }\n\
2799 }";
2800 let ops = ops_of::<JavaParser>(source, "foo.java");
2801
2802 assert!(
2803 ops.operators.iter().any(|o| o == "super"),
2804 "`? super String` must keep `super` an operator; operators were {:?}",
2805 ops.operators
2806 );
2807 assert!(
2808 !ops.operands.iter().any(|o| o == "super"),
2809 "a wildcard bound must not bill `super` as an operand; operands were {:?}",
2810 ops.operands
2811 );
2812 assert!(
2813 ops.operators.iter().any(|o| o == "extends"),
2814 "the fixture must still contain the `? extends String` \
2815 bound this arm mirrors; operators were {:?}",
2816 ops.operators
2817 );
2818 assert_keywords_are_operands_only::<JavaParser>(source, "foo.java", &["this"]);
2819 }
2820
2821 /// One `this` and one `super` per container the pinned
2822 /// `tree-sitter-java` can put them in: a delegating constructor
2823 /// call, an explicit superclass constructor call, a field access, a
2824 /// method-invocation receiver, a method reference, a call argument,
2825 /// and the two qualified forms an inner class allows.
2826 #[cfg(feature = "java")]
2827 const JAVA_SELF_POSITIONS: &str = "class Pos extends P {
2828 int x;
2829 Pos() { this(1); }
2830 Pos(int v) { super(v); this.x = v; }
2831 int a() { return this.x; }
2832 int b() { return super.h(); }
2833 java.util.function.IntSupplier c() { return this::a; }
2834 void d() { g(this); }
2835 void g(Object o) { }
2836 class In {
2837 int e() { return Pos.this.x; }
2838 int f() { return Pos.super.h(); }
2839 }
2840 }";
2841
2842 #[cfg(feature = "java")]
2843 #[test]
2844 fn java_self_and_super_leaves_are_unaliased_and_unconditional() {
2845 assert_self_reference_leaves::<JavaCode>(
2846 JAVA_SELF_POSITIONS,
2847 &[
2848 ("this", Java::This as u16, 6),
2849 ("super", Java::Super as u16, 3),
2850 ],
2851 "java",
2852 );
2853 }
2854
2855 // Groovy's `Super` arm, pinned from all three sides. The pinned
2856 // grammar emits the `super` token only as a `wildcard` bound, where it
2857 // is an operator exactly as in Java above; every super-*reference* is
2858 // a plain `identifier`, and so an operand.
2859 //
2860 // Since #1419 the arm carries Java's `Wildcard` parent gate, so the
2861 // bound half below exercises the gate's operator branch — inverting
2862 // the gate fails it. The gate's operand branch is unreachable under
2863 // the pin (only error recovery on invalid Groovy, `List<? super>`,
2864 // detaches a `super` token from its `wildcard`), so it is
2865 // deliberately untested rather than pinned against an invalid
2866 // fixture. The node census is the drift marker for it: a grammar bump
2867 // that routes a reference to `Groovy::Super` fails here by name.
2868 #[cfg(feature = "groovy")]
2869 #[test]
2870 fn groovy_wildcard_super_bound_stays_an_operator() {
2871 let bounds =
2872 "class T {\n void m(List<? super String> a, List<? extends String> b) { }\n}";
2873 let ops = ops_of::<GroovyParser>(bounds, "foo.groovy");
2874 assert!(
2875 ops.operators.iter().any(|o| o == "super"),
2876 "`? super String` must keep `super` an operator; operators were {:?}",
2877 ops.operators
2878 );
2879 assert!(
2880 !ops.operands.iter().any(|o| o == "super"),
2881 "a wildcard bound must not bill `super` as an operand; operands were {:?}",
2882 ops.operands
2883 );
2884 assert!(
2885 ops.operators.iter().any(|o| o == "extends"),
2886 "the fixture must still contain the `? extends String` bound this arm \
2887 mirrors; operators were {:?}",
2888 ops.operators
2889 );
2890
2891 let mixed = "class T extends B {\n void m(List<? super String> a) { super.h() }\n}";
2892 let mut tokens = 0;
2893 for_each_node_with_chain::<GroovyCode>(mixed.as_bytes(), |node, chain| {
2894 if node.kind_id() == Groovy::Super as u16 {
2895 tokens += 1;
2896 assert_eq!(
2897 chain.last().map(Node::kind_id),
2898 Some(Groovy::Wildcard as u16),
2899 "groovy: a `super` token outside a wildcard bound now reaches \
2900 the gate's operand branch, which the pinned grammar made \
2901 unreachable — re-check the arm against the new grammar (#1419)"
2902 );
2903 }
2904 });
2905 assert_eq!(
2906 tokens, 1,
2907 "the fixture's one wildcard bound must still spell `super`, and its \
2908 `super.h()` reference must not"
2909 );
2910
2911 // The reference direction, on valid input the gate must leave
2912 // alone: every super-reference form the grammar accepts stays an
2913 // operand, so splitting `Super` out of the operator alternation
2914 // cannot have moved one. `this` rides along for the same reason
2915 // Java's test carries it.
2916 let refs = "class A extends B {\n A() { super(1) }\n \
2917 def g() { return super.h() }\n \
2918 def i() { return A.super.h() }\n \
2919 def j() { return super::h }\n \
2920 def k() { return super?.h() }\n \
2921 def l() { return this.x }\n}";
2922 assert_keywords_are_operands_only::<GroovyParser>(refs, "foo.groovy", &["super", "this"]);
2923 }
2924
2925 #[cfg(feature = "groovy")]
2926 #[test]
2927 fn groovy_operators_and_operands() {
2928 check_metrics::<GroovyParser>(
2929 "class Main {
2930 static void main(String[] args) {
2931 int a, b, c, avg;
2932 a = 5; b = 5; c = 5;
2933 avg = (a + b + c) / 3;
2934 println(avg);
2935 }
2936 }",
2937 "foo.groovy",
2938 |metric| {
2939 // Groovy mirror of `java_operators_and_operands`. The juxt
2940 // call `println avg` exercises `juxt_function_call` in
2941 // place of Java's `MessageFormat.format(...)`. amaanq's
2942 // grammar inherits Java's tokenisation, so n1/N1/n2/N2
2943 // shapes match Java up to those substitutions.
2944 // The dekobon grammar parses primitive type names
2945 // (`void`, `int`, `String`) as `type_identifier`
2946 // rather than as distinct keyword tokens, so they
2947 // count as operands here — the prior amaanq grammar
2948 // treated them as operators. Net shift: −2 unique
2949 // operators (`void`, `int`), +2 unique operands
2950 // (`void`, `int` were the only two type_identifiers
2951 // not already counted as operands, since `String`
2952 // was already an identifier in the prior grammar's
2953 // counting).
2954 // `class` is the ninth unique operator since #1552.
2955 assert_eq!(metric.halstead.unique_operators(), 9);
2956 assert_eq!(metric.halstead.unique_operands(), 13);
2957 // `bugs` is `effort.powf(2/3) / 3000`, and the platform libm's
2958 // `powf` differs in the last ulp: macOS rendered this value
2959 // as `…92342184` where Linux and Windows gave `…923421845`.
2960 // Round it so the snapshot pins the metric, not the libm.
2961 insta::assert_json_snapshot!(
2962 metric.halstead,
2963 { ".bugs" => insta::rounded_redaction(12) },
2964 @r#"
2965 {
2966 "unique_operators": 9,
2967 "total_operators": 23,
2968 "unique_operands": 13,
2969 "total_operands": 23,
2970 "length": 46,
2971 "estimated_program_length": 76.63504134881501,
2972 "purity_ratio": 1.665979159756848,
2973 "vocabulary": 22,
2974 "volume": 205.13385445731566,
2975 "difficulty": 7.961538461538462,
2976 "level": 0.12560386473429952,
2977 "effort": 1633.1810720255517,
2978 "time": 90.73228177919731,
2979 "bugs": 0.046227630923
2980 }
2981 "#
2982 );
2983 },
2984 );
2985 }
2986
2987 #[cfg(feature = "groovy")]
2988 #[test]
2989 fn groovy_primitive_types_and_booleans() {
2990 check_metrics::<GroovyParser>(
2991 "class Prims {
2992 byte a = 1
2993 short b = 2
2994 int c = 3
2995 long d = 4
2996 char e = 'x'
2997 float f = 1.0f
2998 double g = 2.0
2999 boolean h = true
3000 boolean i = false
3001 }",
3002 "foo.groovy",
3003 |metric| {
3004 // The dekobon grammar consolidates the 8 primitive
3005 // type names (`byte`, `short`, `int`, `long`, `char`,
3006 // `float`, `double`, `boolean`) under `type_identifier`
3007 // — so they count as operands, not as distinct
3008 // operators. Likewise numeric literals collapse to one
3009 // `NumberLiteral` shape (no Hex/Octal/Binary/Decimal
3010 // split), and `'x'` parses as `StringLiteral` (Groovy
3011 // single-quoted strings) rather than as
3012 // `CharacterLiteral`. Operators remaining in this
3013 // fixture: `=`, the `class` keyword (#1552) and the
3014 // class-body braces (only `{` is in the operator set).
3015 // True/false collapse under one `BooleanLiteral`.
3016 assert_eq!(metric.halstead.unique_operators(), 3);
3017 assert_eq!(metric.halstead.unique_operands(), 27);
3018 insta::assert_json_snapshot!(
3019 metric.halstead,
3020 @r#"
3021 {
3022 "unique_operators": 3,
3023 "total_operators": 11,
3024 "unique_operands": 27,
3025 "total_operands": 28,
3026 "length": 39,
3027 "estimated_program_length": 133.1368500605771,
3028 "purity_ratio": 3.413765386168644,
3029 "vocabulary": 30,
3030 "volume": 191.36873322873222,
3031 "difficulty": 1.5555555555555556,
3032 "level": 0.6428571428571428,
3033 "effort": 297.6846961335835,
3034 "time": 16.53803867408797,
3035 "bugs": 0.014861058740943886
3036 }
3037 "#
3038 );
3039 },
3040 );
3041 }
3042
3043 // Issue #1263 swept Groovy alongside the JS family and C#: its
3044 // operand arm listed `QualifiedName` (a `package` / `import` path)
3045 // and `QualifiedType` on top of the identifier leaves the walker
3046 // already reached.
3047 //
3048 // Only the `QualifiedName` half was observable. The runtime emits
3049 // `qualified_type` as the *alias* `QualifiedType2` (kind_id 228),
3050 // which the arm never named — a lesson-2 miss that, by accident,
3051 // made that half already leaves-only and is why #1263's issue body
3052 // recorded Groovy as compliant. Both kinds are gone rather than
3053 // completed.
3054 //
3055 // expected, for `package com.example`: operators `.` (1/1);
3056 // operands `com`, `example` (2/2). Pre-fix the `qualified_name`
3057 // added `com.example`, making the operand counts 3/3.
3058 #[cfg(feature = "groovy")]
3059 #[test]
3060 fn groovy_qualified_name_counts_leaves_not_the_composite_1263() {
3061 check_metrics::<GroovyParser>("package com.example", "foo.groovy", |metric| {
3062 assert_eq!(metric.halstead.unique_operators(), 1);
3063 assert_eq!(metric.halstead.total_operators(), 1);
3064 assert_eq!(metric.halstead.unique_operands(), 2);
3065 assert_eq!(metric.halstead.total_operands(), 2);
3066 });
3067 }
3068
3069 // The type half of the same arm (#1352). #1263 dropped
3070 // `QualifiedType` alongside `QualifiedName`, but only the latter
3071 // was pinned: adding `QualifiedType2` back to the operand arm
3072 // failed none of the 3,523 tests then in the lib targets, so the
3073 // leaves-only reading of a qualified *type* was correct by accident
3074 // rather than by contract. The tempting "fix" for the alias miss
3075 // #1263 recorded is to complete the list with 228, which is exactly
3076 // the double count #1263 removed — this test is what stops that.
3077 //
3078 // The kind assertions are the grammar-dispatch section 1 / 2 drift
3079 // marker: if a grammar bump renumbers the alias, the operand
3080 // assertions below would keep passing while measuring a construct
3081 // this arm no longer describes.
3082 //
3083 // expected, for `java.util.List x = null`: operators `.` × 2 and
3084 // `=` → n1 = 2, N1 = 3; operands `java`, `util`, `List`, `x`,
3085 // `null` → n2 = 5, N2 = 5. Listing the wrapper would add the whole
3086 // span `java.util.List`, making the operand counts 6/6.
3087 #[cfg(feature = "groovy")]
3088 #[test]
3089 fn groovy_qualified_type_counts_leaves_not_the_composite_1352() {
3090 const SOURCE: &str = "java.util.List x = null";
3091
3092 let parser = GroovyParser::new(
3093 SOURCE.as_bytes().to_vec(),
3094 &PathBuf::from("foo.groovy"),
3095 None,
3096 );
3097 assert!(
3098 ast_has_kind_id(&parser, Groovy::QualifiedType2 as u16),
3099 "the dekobon grammar no longer emits `qualified_type` as the \
3100 alias `QualifiedType2`; re-derive the Groovy operand arm \
3101 before trusting the counts below",
3102 );
3103 assert!(
3104 !ast_has_kind_id(&parser, Groovy::QualifiedType as u16),
3105 "the unsuffixed `QualifiedType` is now reachable; it is a \
3106 second wrapper this arm must keep excluded",
3107 );
3108
3109 check_metrics::<GroovyParser>(SOURCE, "foo.groovy", |metric| {
3110 assert_eq!(metric.halstead.unique_operators(), 2);
3111 assert_eq!(metric.halstead.total_operators(), 3);
3112 assert_eq!(metric.halstead.unique_operands(), 5);
3113 assert_eq!(metric.halstead.total_operands(), 5);
3114 });
3115 }
3116
3117 #[cfg(feature = "groovy")]
3118 #[test]
3119 fn groovy_closure_operators_and_operands() {
3120 check_metrics::<GroovyParser>("def double = { x -> x * 2 }", "foo.groovy", |metric| {
3121 // Closure with arrow-style parameter list.
3122 // Distinct operators: def, =, {}, ->, * = 5.
3123 // Distinct operands: double, x, 2 = 3.
3124 assert_eq!(metric.halstead.unique_operators(), 5);
3125 assert_eq!(metric.halstead.unique_operands(), 3);
3126 });
3127 }
3128
3129 /// Regression for issue #247: every Groovy-specific operator the
3130 /// prior amaanq grammar dropped to ERROR or mis-shaped as a Java
3131 /// node now parses as a distinct lexer token in the dekobon
3132 /// grammar, so Halstead counts each one. The fixture below
3133 /// exercises Elvis `?:`, safe-nav `?.`, safe-chain `??.`,
3134 /// spread-dot `*.`, method-pointer `.&`, direct-field `.@`,
3135 /// identity `===` / `!==`, spaceship `<=>`, regex `=~` / `==~`,
3136 /// exclusive ranges `..<` / `<..` / `<..<`, `as` coercion, and
3137 /// `?[` safe index — every distinct operator kind must appear in
3138 /// `u_operators` (the count grows by exactly the number of new
3139 /// distinct operator tokens introduced).
3140 #[cfg(feature = "groovy")]
3141 #[test]
3142 fn groovy_dekobon_operator_coverage_247() {
3143 check_metrics::<GroovyParser>(
3144 "def f(a, b, list, s) {
3145 def x = a ?: b
3146 def y = a?.field
3147 def z = a??.field
3148 def items = list*.size()
3149 def ptr = a.&size
3150 def fld = a.@field
3151 def id1 = a === b
3152 def id2 = a !== b
3153 def ship = a <=> b
3154 def find = s =~ /pat/
3155 def match = s ==~ /^pat\\$/
3156 def r1 = 0..<10
3157 def r2 = 0<..10
3158 def r3 = 0<..<10
3159 def cast = a as String
3160 def safe = list?[0]
3161 return x
3162 }",
3163 "foo.groovy",
3164 |metric| {
3165 // Exact pin: with the dekobon Groovy grammar this
3166 // fixture exercises 16 Groovy-specific tokens (`?:`,
3167 // `?.`, `??.`, `*.`, `.&`, `.@`, `===`, `!==`, `<=>`,
3168 // `=~`, `==~`, `..<`, `<..`, `<..<`, `as`, `?[`) plus
3169 // 6 ambient Java-shaped operators the fixture also
3170 // uses (`def`, `=`, `,`, `{}`, `()`, `return`), for a
3171 // total of 22 distinct operator kinds. A regression
3172 // that drops any one of the 16 #247 operators would
3173 // push the count below 22 and fail this assertion. The
3174 // complementary AST walk below pins each #247
3175 // operator's identity individually so a grammar change
3176 // that adds an unrelated operator (lifting
3177 // `u_operators` to 23) still flags the loss of a #247
3178 // operator at the per-token level.
3179 //
3180 // Was 23 until #1314. The extra entry was a `/` — the
3181 // fixture's two slashy literals (`/pat/`, `/^pat\$/`)
3182 // each spelled their closing delimiter with the
3183 // division kind, and the arm now guards them. The
3184 // enumeration above was wrong in two ways at that
3185 // count: it listed an ambient `[`, which this fixture
3186 // never emits (`list?[0]` is the single `?[` token),
3187 // and omitted the fabricated `/` that made up the
3188 // difference. Both are corrected here.
3189 assert_eq!(
3190 metric.halstead.unique_operators(),
3191 22,
3192 "u_operators changed; check whether a #247 operator was dropped or an unrelated operator added (and update the comment / token list above accordingly)",
3193 );
3194 },
3195 );
3196 }
3197
3198 #[cfg(feature = "groovy")]
3199 #[test]
3200 fn groovy_gstring_no_double_count() {
3201 // Issue #454: before the fix Groovy had no interpolation guard
3202 // at all — `StringLiteral` was classified as a plain operand, so
3203 // a GString counted the wrapping literal AND descended into its
3204 // interpolated expression, double-counting the inner identifier
3205 // in N2. The fix routes `StringLiteral` through
3206 // `string_operand_type` with both GString interpolation child
3207 // kinds (`gstring_brace_interpolation` / `gstring_dollar_-
3208 // interpolation`), so the wrapper is Unknown and only the inner
3209 // expression contributes.
3210 //
3211 // `def greet(name) {\n return "Hi ${name}"\n}\n`
3212 // operands by token text: `greet` × 1, `name` × 2 (param +
3213 // inside `${name}`). The wrapping `"Hi ${name}"` is suppressed
3214 // → u_operands = 2 (`greet`, `name`), N2 = 3. Without the fix
3215 // the wrapping literal would also count → u_operands = 3,
3216 // N2 = 4.
3217 let src = "def greet(name) {\n return \"Hi ${name}\"\n}\n";
3218 check_metrics::<GroovyParser>(src, "foo.groovy", |metric| {
3219 assert_eq!(metric.halstead.unique_operands(), 2);
3220 assert_eq!(metric.halstead.total_operands(), 3);
3221 });
3222 assert_ops_operands::<GroovyParser>(src, "foo.groovy", 2, vec!["greet", "name"]);
3223 }
3224
3225 #[cfg(feature = "groovy")]
3226 #[test]
3227 fn groovy_gstring_dollar_form_no_double_count() {
3228 // Issue #454: the short `$name` GString form emits a distinct
3229 // `gstring_dollar_interpolation` child whose inner `identifier`
3230 // text is `$name` (the grammar's identifier node spans the
3231 // leading `$`). The wrapper is suppressed; the inner `$name`
3232 // operand is distinct from the bare `name` param.
3233 //
3234 // `def greet(name) {\n return "Hi $name"\n}\n`
3235 // operands: `greet`, `name` (param), `$name` (interp) →
3236 // u_operands = 3, N2 = 3. Without the fix the wrapping
3237 // `"Hi $name"` would also count → u_operands = 4, N2 = 4.
3238 let src = "def greet(name) {\n return \"Hi $name\"\n}\n";
3239 check_metrics::<GroovyParser>(src, "foo.groovy", |metric| {
3240 assert_eq!(metric.halstead.unique_operands(), 3);
3241 assert_eq!(metric.halstead.total_operands(), 3);
3242 });
3243 assert_ops_operands::<GroovyParser>(src, "foo.groovy", 3, vec!["greet", "name", "$name"]);
3244 }
3245
3246 // `Groovy::Identifier2` (141) is the grammar's external token
3247 // `_gstring_dollar_path` — the `$a.b` path of a dollar interpolation —
3248 // whose public symbol is `identifier`, so the parser reports it as
3249 // the unsuffixed `Identifier` and never as 141 (#1475). That is why
3250 // the operand arm lists `Identifier` but not `Identifier2`; if a
3251 // grammar bump starts emitting 141, the dotted path drops out of N2
3252 // with no error.
3253 //
3254 // expected: operands `greet`, `name` (param), `$name.b` (the whole
3255 // path, one node) → n2 = 3, N2 = 3.
3256 #[cfg(feature = "groovy")]
3257 #[test]
3258 fn groovy_gstring_dollar_path_is_the_public_identifier() {
3259 let src = "def greet(name) {\n return \"Hi $name.b\"\n}\n";
3260 let parser = GroovyParser::new(src.as_bytes().to_vec(), &PathBuf::from("foo.groovy"), None);
3261 assert!(
3262 ast_has_kind_id(&parser, Groovy::GstringDollarInterpolation as u16),
3263 "the fixture no longer spells a dollar interpolation",
3264 );
3265 assert!(
3266 !ast_has_kind_id(&parser, Groovy::Identifier2 as u16),
3267 "the hidden `_gstring_dollar_path` token is now emitted as \
3268 `Identifier2`; add it to the Groovy operand arm",
3269 );
3270 check_metrics::<GroovyParser>(src, "foo.groovy", |metric| {
3271 assert_eq!(metric.halstead.unique_operands(), 3);
3272 assert_eq!(metric.halstead.total_operands(), 3);
3273 });
3274 assert_ops_operands::<GroovyParser>(src, "foo.groovy", 3, vec!["greet", "name", "$name.b"]);
3275 }
3276
3277 #[cfg(feature = "groovy")]
3278 #[test]
3279 fn groovy_plain_string_still_operand() {
3280 // Counterpart to `groovy_gstring_no_double_count`: a plain
3281 // non-interpolated literal has neither GString interpolation
3282 // child and must still contribute exactly one operand.
3283 //
3284 // `def f() {\n return "plain"\n}\n`
3285 // operands: `f`, `"plain"` → u_operands = 2, N2 = 2.
3286 let src = "def f() {\n return \"plain\"\n}\n";
3287 check_metrics::<GroovyParser>(src, "foo.groovy", |metric| {
3288 assert_eq!(metric.halstead.unique_operands(), 2);
3289 assert_eq!(metric.halstead.total_operands(), 2);
3290 });
3291 assert_ops_operands::<GroovyParser>(src, "foo.groovy", 2, vec!["f", "\"plain\""]);
3292 }
3293
3294 #[cfg(feature = "groovy")]
3295 #[test]
3296 fn groovy_slashy_string_delimiter_is_not_an_operator() {
3297 // Regression: issue #1314, the Groovy sibling of Elixir #1256
3298 // and Ruby/Perl #1312. A slashy string is a `StringLiteral`
3299 // whose closing delimiter is a `SLASH` — the kind id real
3300 // division uses — so `def b = /xyz/` reported a `/` operator
3301 // with no division in the source. Only the closer is a child
3302 // (the grammar folds the opening `/` into the literal's span),
3303 // so this fabricated one `/` per literal rather than Ruby's two.
3304 //
3305 // expected: operators `def` × 3, `=` × 3 → n1 = 2, N1 = 6.
3306 // Operands `b`, `/xyz/` × 2, `c`, `s` → n2 = 4, N2 = 6. Before
3307 // the guard the two closers added `/` → n1 = 3, N1 = 8.
3308 check_metrics::<GroovyParser>(
3309 "def b = /xyz/\ndef c = /xyz/\ndef s = b\n",
3310 "foo.groovy",
3311 |metric| {
3312 assert_eq!(metric.halstead.unique_operators(), 2);
3313 assert_eq!(metric.halstead.total_operators(), 6);
3314 assert_eq!(metric.halstead.unique_operands(), 4);
3315 assert_eq!(metric.halstead.total_operands(), 6);
3316 },
3317 );
3318 }
3319
3320 #[cfg(feature = "groovy")]
3321 #[test]
3322 fn groovy_division_survives_the_slashy_guard() {
3323 // Control for #1314: the guard is scoped to a `StringLiteral`
3324 // parent, so real division must still count. Both sides are in
3325 // one fixture — two divisions and one slashy literal — so a
3326 // guard widened to every `SLASH` fails here rather than
3327 // silently passing the test above.
3328 //
3329 // expected: operators `def` × 2, `=` × 2, `/` × 2 → n1 = 3,
3330 // N1 = 6. Operands `q`, `a`, `b`, `c`, `r`, `/x/` → n2 = N2 = 6.
3331 check_metrics::<GroovyParser>("def q = a / b / c\ndef r = /x/\n", "foo.groovy", |metric| {
3332 assert_eq!(metric.halstead.unique_operators(), 3);
3333 assert_eq!(metric.halstead.total_operators(), 6);
3334 assert_eq!(metric.halstead.unique_operands(), 6);
3335 assert_eq!(metric.halstead.total_operands(), 6);
3336 });
3337 }
3338
3339 #[cfg(feature = "groovy")]
3340 #[test]
3341 fn groovy_slashy_guard_is_parent_scoped_not_ancestor_scoped() {
3342 // The input that separates the parent-scoped guard from the
3343 // ancestor-scanning mutant of it — the mutant #1256's
3344 // post-mortem says survives every ordinary fixture. Groovy is
3345 // one of only two languages in #1314 where such an input
3346 // exists at all: a slashy string may carry a GString
3347 // interpolation, so `/x${a / b}y/` puts a real division under a
3348 // `StringLiteral` *ancestor* while its parent is the
3349 // `binary_expression`. An ancestor scan swallows it; the parent
3350 // check leaves it alone. (The JS, C++ and Tcl/iRules guards
3351 // have no such input — see
3352 // `js_regex_delimiter_guard_is_parent_scoped_is_unobservable`.)
3353 //
3354 // expected: operators `def` × 2, `=` × 2, `/` (the
3355 // interpolated division) → n1 = 3, N1 = 5. The wrapping literal
3356 // is not an operand — it carries an interpolation, so
3357 // `string_operand_type` yields `Unknown` and the inner
3358 // expression's operands carry the count (#454) — leaving `r`,
3359 // `a`, `b`, `s` → n2 = 4, with `a` twice → N2 = 5. Under the
3360 // ancestor-scoped mutant the division vanishes: n1 = 2, N1 = 4.
3361 check_metrics::<GroovyParser>(
3362 "def r = /x${a / b}y/\ndef s = a\n",
3363 "foo.groovy",
3364 |metric| {
3365 assert_eq!(metric.halstead.unique_operators(), 3);
3366 assert_eq!(metric.halstead.total_operators(), 5);
3367 assert_eq!(metric.halstead.unique_operands(), 4);
3368 assert_eq!(metric.halstead.total_operands(), 5);
3369 },
3370 );
3371 }
3372
3373 #[cfg(feature = "groovy")]
3374 #[test]
3375 fn groovy_every_string_spelling_scores_alike() {
3376 // Companion to the three above (#1314). Groovy has five ways to
3377 // write an inert one-character string, and the choice is
3378 // spelling rather than semantics, so all five must score
3379 // identically. Before the guard the two slashy forms reported
3380 // an extra `/` operator that the other three did not — the
3381 // author's delimiter choice moved n1/N1.
3382 //
3383 // The dollar-slashy and quoted forms are no-change controls:
3384 // `$/…/$` closes with `/$` (kind 144) and `"…"` with `"` (134),
3385 // neither of which the operator arm classifies. `'x'` is a
3386 // childless leaf. The escaped-slash row is the one that would
3387 // regress if the guard were ever narrowed to a literal whose
3388 // *only* child is the closer.
3389 //
3390 // expected per spelling: operators `def` × 3, `=` × 3 → n1 = 2,
3391 // N1 = 6; operands `a`, the literal, `b`, `c` → n2 = 4, with
3392 // `a` used three times → N2 = 6.
3393 for literal in ["/x/", "$/x/$", "'x'", "\"x\"", r"/esc\/aped/"] {
3394 assert_halstead_counts::<GroovyParser>(
3395 &format!("def a = {literal}\ndef b = a\ndef c = a\n"),
3396 "foo.groovy",
3397 [2, 6, 4, 6],
3398 &format!("literal {literal}"),
3399 );
3400 }
3401 }
3402
3403 #[cfg(feature = "csharp")]
3404 #[test]
3405 fn csharp_operators_and_operands() {
3406 // After issue #286, `void`, `string`, and `int` count as three
3407 // distinct Halstead operators rather than collapsing into one
3408 // `PredefinedType` kind_id entry, lifting u_operators from 13
3409 // to 15. Total operators (N1) is unchanged because the same
3410 // nodes are still counted, just keyed by lexeme.
3411 check_metrics::<CsharpParser>(
3412 "public class Main {
3413 public static void Run(string[] args) {
3414 int a, b, c, avg;
3415 a = 5; b = 5; c = 5;
3416 avg = (a + b + c) / 3;
3417 System.Console.WriteLine(\"{0}\", avg);
3418 }
3419 }",
3420 "foo.cs",
3421 |metric| {
3422 assert_eq!(metric.halstead.unique_operators(), 15);
3423 assert_eq!(metric.halstead.total_operators(), 32);
3424 assert_eq!(metric.halstead.unique_operands(), 13);
3425 assert_eq!(metric.halstead.total_operands(), 23);
3426 // Pin every Halstead field; values are whatever the
3427 // classifier produces and become the regression spec.
3428 insta::assert_json_snapshot!(metric.halstead);
3429 },
3430 );
3431 }
3432
3433 // Issue #1263: C#'s three name *containers* — `qualified_name`
3434 // (`System.Text`), `generic_name` (`List<int>`) and
3435 // `alias_qualified_name` (`global::Foo`) — were operands alongside
3436 // every leaf the walker already reached, so one occurrence of each
3437 // billed twice. `member_access_expression` never was, which is why
3438 // `csharp_operators_and_operands`' `System.Console.WriteLine` is
3439 // unaffected by this change: the bug lived in the *name* grammar,
3440 // not in member access.
3441 //
3442 // Three fixtures rather than one, so a regression names which
3443 // container came back. Each is hand-tallied; the removed composite
3444 // is called out per case.
3445 #[cfg(feature = "csharp")]
3446 #[test]
3447 fn csharp_name_containers_count_leaves_not_the_composite_1263() {
3448 // expected: operators `using`, `.`, `;` (3/3); operands
3449 // `System`, `Text` (2/2). Pre-fix the `qualified_name` added
3450 // `System.Text`, making the operand counts 3/3.
3451 check_metrics::<CsharpParser>("using System.Text;", "foo.cs", |metric| {
3452 assert_eq!(metric.halstead.unique_operators(), 3);
3453 assert_eq!(metric.halstead.total_operators(), 3);
3454 assert_eq!(metric.halstead.unique_operands(), 2);
3455 assert_eq!(metric.halstead.total_operands(), 2);
3456 });
3457
3458 // expected: operators `class`, `{`×2, `void`, `(`, `;`, `<>`,
3459 // `int` — 8 total, 7 unique (`int` is the text-keyed primitive
3460 // operator, per #286; `<>` is the type-argument pair, #1559). Operands `C`, `M`, `List`, `l`
3461 // — 4/4. Pre-fix the `generic_name` added `List<int>`, making
3462 // them 5/5.
3463 check_metrics::<CsharpParser>(
3464 "class C { void M() { List<int> l; } }",
3465 "foo.cs",
3466 |metric| {
3467 assert_eq!(metric.halstead.unique_operators(), 7);
3468 assert_eq!(metric.halstead.total_operators(), 8);
3469 assert_eq!(metric.halstead.unique_operands(), 4);
3470 assert_eq!(metric.halstead.total_operands(), 4);
3471 },
3472 );
3473
3474 // expected: operators `class`, `{`×2, `void`, `(`, `;`, `=`,
3475 // `::`, `.` — 9 total, 8 unique. `var` has no operator arm.
3476 // Operands `C`, `M`, `x`, `global`, `Foo`, `Bar` — 6/6. Pre-fix
3477 // the `alias_qualified_name` added `global::Foo`, making them
3478 // 7/7. The `::` staying an operator is what makes the leaf-only
3479 // tally lossless here, so it is asserted by the operator count
3480 // rather than assumed.
3481 check_metrics::<CsharpParser>(
3482 "class C { void M() { var x = global::Foo.Bar; } }",
3483 "foo.cs",
3484 |metric| {
3485 assert_eq!(metric.halstead.unique_operators(), 8);
3486 assert_eq!(metric.halstead.total_operators(), 9);
3487 assert_eq!(metric.halstead.unique_operands(), 6);
3488 assert_eq!(metric.halstead.total_operands(), 6);
3489 },
3490 );
3491 }
3492
3493 #[cfg(feature = "csharp")]
3494 #[test]
3495 fn csharp_primitive_types_and_booleans() {
3496 // After issue #286: each of `byte`, `short`, `int`, `long`,
3497 // `char`, `float`, `double`, `bool`, `object` is now a distinct
3498 // Halstead operator (9 primitives) rather than collapsing into
3499 // one `PredefinedType` kind_id entry. u_operators rises from 6
3500 // to 14 (5 non-primitive operators + 9 distinct primitives);
3501 // total operators (N1) is unchanged because the same nodes are
3502 // still counted, just keyed by lexeme.
3503 //
3504 // N2 dropped 23 → 21 with issue #1253: `true` and `false` each
3505 // reached the walker twice — once as `boolean_literal`, once as
3506 // the keyword leaf under it — so each added one spurious
3507 // occurrence. n2 is unchanged at 21 because operands are keyed
3508 // by source text, so the duplicate collapsed into the existing
3509 // vocabulary entry; that is exactly why the inflation was
3510 // invisible in n2. Every operand here is distinct, so
3511 // N2 == n2 == 21 after the fix.
3512 check_metrics::<CsharpParser>(
3513 "public class Prims {
3514 byte a = 1;
3515 short b = 2;
3516 int c = 3;
3517 long d = 4;
3518 char e = 'x';
3519 float f = 1.0f;
3520 double g = 2.0;
3521 bool h = true;
3522 bool i = false;
3523 object j = null;
3524 }",
3525 "foo.cs",
3526 |metric| {
3527 assert_eq!(metric.halstead.unique_operators(), 14);
3528 assert_eq!(metric.halstead.total_operators(), 33);
3529 assert_eq!(metric.halstead.unique_operands(), 21);
3530 assert_eq!(metric.halstead.total_operands(), 21);
3531 insta::assert_json_snapshot!(metric.halstead);
3532 },
3533 );
3534 }
3535
3536 #[cfg(feature = "csharp")]
3537 #[test]
3538 fn csharp_boolean_literal_counts_once() {
3539 // Regression: issue #1253. `boolean_literal: choice('true',
3540 // 'false')` wraps the keyword leaf, and both kinds sat in the
3541 // operand arm, so every `true` / `false` occurrence added +1 to
3542 // N2. Operands are keyed by source text, so the duplicate
3543 // collapsed into the same vocabulary entry and n2 stayed
3544 // correct — which is why nothing caught it.
3545 //
3546 // Source repeats `true` so N2 exceeds n2 and the assertions can
3547 // tell "counted once per occurrence" from "deduplicated into
3548 // the vocabulary".
3549 //
3550 // Operands by text key: `A`, `M`, `a`, `b`, `c`, `d`, `true` × 2,
3551 // `false`, `null` ⇒ n2 = 9, N2 = 10. Before the fix the keyword
3552 // leaves added one occurrence per boolean ⇒ N2 = 13.
3553 check_metrics::<CsharpParser>(
3554 "class A {\n void M() {\n bool a = true;\n bool b = false;\n bool c = true;\n object d = null;\n }\n}\n",
3555 "foo.cs",
3556 |metric| {
3557 assert_eq!(metric.halstead.unique_operands(), 9);
3558 assert_eq!(metric.halstead.total_operands(), 10);
3559 },
3560 );
3561 }
3562
3563 #[cfg(feature = "csharp")]
3564 #[test]
3565 fn csharp_operator_true_names_an_operator_not_a_value() {
3566 // C#'s overloadable-operator list emits a bare `true` / `false`
3567 // token with no `boolean_literal` wrapper — `operator_declaration`
3568 // is the grammar's only such position. There the keyword names
3569 // the operator being declared, as `+` does in `operator +`, so it
3570 // is an operator (#1296); the companion test above pins the
3571 // wrapped literal, which bills only its wrapper (#1253).
3572 //
3573 // Each declaration names one boolean and returns the other, so
3574 // the fixture carries each keyword once as a name and once as a
3575 // value.
3576 //
3577 // Operators (n1 = 11, N1 = 18): class 1, {} 1, public 2,
3578 // static 2, bool 2, operator 2, true 1, false 1, () 2, => 2,
3579 // ; 2. Operands (n2 = 4, N2 = 7): A × 3 (class name, two
3580 // parameter types), a × 2, and the literals false and true once
3581 // each. Before #1296 the two names were operands instead:
3582 // (9, 16, 4, 9). A blanket exclusion of the leaf would give
3583 // (9, 16, 4, 7), and no guard at all (11, 18, 4, 9).
3584 let source = "class A {\n public static bool operator true(A a) => false;\n public static bool operator false(A a) => true;\n}\n";
3585 assert_halstead_counts::<CsharpParser>(source, "foo.cs", [11, 18, 4, 7], "operator true");
3586
3587 // `true` and `false` are each an operator *and* an operand here,
3588 // once apiece: the name and the literal land in different halves.
3589 let ops = ops_of::<CsharpParser>(source, "foo.cs");
3590 for keyword in ["true", "false"] {
3591 for (half, list) in [("operators", &ops.operators), ("operands", &ops.operands)] {
3592 assert_eq!(
3593 list.iter().filter(|o| *o == keyword).count(),
3594 1,
3595 "`{keyword}` must appear once among the {half}; they were {list:?}"
3596 );
3597 }
3598 }
3599
3600 // The fallback arm: outside both positions — here a bare `true`
3601 // under error recovery, the only other parent the grammar
3602 // produces — the keyword keeps its role as a value.
3603 assert_halstead_counts::<CsharpParser>("true\n", "foo.cs", [0, 0, 1, 1], "bare true");
3604 }
3605
3606 #[cfg(feature = "csharp")]
3607 #[test]
3608 fn csharp_predefined_types_keyed_by_lexeme() {
3609 // Regression: issue #286. The C# grammar emits one `PredefinedType`
3610 // kind_id for every keyword type (`int`, `string`, `bool`, …).
3611 // Without keying by source text the entire family collapses into
3612 // a single Halstead operator (n1 += 1) instead of one per distinct
3613 // keyword. This test pins the post-fix behaviour using four
3614 // distinct primitives — `int`, `string`, `bool`, `object` —
3615 // appearing as parameter types so no other operators interact
3616 // with the count.
3617 //
3618 // expected: operators are `class`, `void`, `M`, `{}`, `()`, `,`
3619 // (×3 between 4 params), plus the four distinct predefined types
3620 // → u_operators = 5 + 4 = 9. Without the fix the four primitives
3621 // collapse to one entry, giving u_operators = 6.
3622 check_metrics::<CsharpParser>(
3623 "class C { void M(int a, string b, bool c, object d) {} }",
3624 "foo.cs",
3625 |metric| {
3626 // The headline assertion: four distinct primitive
3627 // keywords contribute four distinct operators, not one.
3628 assert_eq!(metric.halstead.unique_operators(), 9);
3629 },
3630 );
3631 }
3632
3633 #[cfg(feature = "csharp")]
3634 #[test]
3635 fn csharp_interpolated_string_no_double_count() {
3636 // Regression: issue #183. A C# `$"Hi {name}!"` used to be
3637 // classified as a Halstead operand (the wrapping
3638 // `InterpolatedStringExpression`) AND have its inner
3639 // `Interpolation`'s identifier classified as an operand too.
3640 // The fix routes `InterpolatedStringExpression` through a
3641 // conditional: when it has an `Interpolation` child, the inner
3642 // identifier already carries the operand contribution and the
3643 // wrapper is treated as `Unknown`; when it does not (static
3644 // `$"hello"`), the wrapper still counts as one operand.
3645 //
3646 // expected: operand contributions for
3647 // `class C { void M(string name) { string s = $"Hi {name}!"; } }`
3648 // — `C` (class), `M` (method), `name` (param), `s` (local),
3649 // and the inner `name` (inside `{...}`). With the fix,
3650 // u_operands = 4 (C, M, name, s); N2 = 5 (`name` twice).
3651 // Without the fix, the wrapping `$"Hi {name}!"` would also
3652 // count → u_operands = 5, N2 = 6.
3653 check_metrics::<CsharpParser>(
3654 "class C { void M(string name) { string s = $\"Hi {name}!\"; } }",
3655 "foo.cs",
3656 |metric| {
3657 assert_eq!(metric.halstead.unique_operands(), 4);
3658 assert_eq!(metric.halstead.total_operands(), 5);
3659 },
3660 );
3661 }
3662
3663 #[cfg(feature = "csharp")]
3664 #[test]
3665 fn csharp_static_interpolated_string_is_operand() {
3666 // Regression: issue #183. A `$"..."` with no `{...}` is
3667 // semantically identical to `"..."` and must still contribute
3668 // exactly one operand — the conditional `is_child(Interpolation)`
3669 // check distinguishes it from a true interpolation. expected:
3670 // operands are `C`, `M`, `s`, `$"hello"` → u_operands = 4, N2 = 4.
3671 // A naive "always Unknown" fix would yield u_operands = 3, N2 = 3,
3672 // diverging from the plain-string equivalent below.
3673 check_metrics::<CsharpParser>(
3674 "class C { void M() { string s = $\"hello\"; } }",
3675 "foo.cs",
3676 |metric| {
3677 assert_eq!(metric.halstead.unique_operands(), 4);
3678 assert_eq!(metric.halstead.total_operands(), 4);
3679 },
3680 );
3681 }
3682
3683 #[cfg(feature = "csharp")]
3684 #[test]
3685 fn csharp_plain_string_still_operand() {
3686 // The fix for #183 only changes how `InterpolatedStringExpression`
3687 // is classified; plain `StringLiteral` (and `VerbatimStringLiteral`
3688 // / `RawStringLiteral`) must still contribute exactly one operand
3689 // each. expected: operands are `C`, `M`, `s`, `"hi"` →
3690 // u_operands = 4, N2 = 4.
3691 check_metrics::<CsharpParser>(
3692 "class C { void M() { string s = \"hi\"; } }",
3693 "foo.cs",
3694 |metric| {
3695 assert_eq!(metric.halstead.unique_operands(), 4);
3696 assert_eq!(metric.halstead.total_operands(), 4);
3697 },
3698 );
3699 }
3700
3701 // C# half of #1380 — see `java_self_and_super_references_are_operands`
3702 // for the structural argument. `base` moves with `this`: both are
3703 // receivers of a member access.
3704 #[cfg(feature = "csharp")]
3705 #[test]
3706 fn csharp_self_and_base_references_are_operands() {
3707 let source = "class T {\n int F() { return this.x + base.y; }\n}";
3708 // Operators (n1=8, N1=10): class, {} x2, int, (), return, . x2, +, ;
3709 // Operands (n2=6, N2=6): T, F, this, x, base, y
3710 // Before the fix this read (10, 12, 4, 4).
3711 assert_halstead_counts::<CsharpParser>(
3712 source,
3713 "foo.cs",
3714 [8, 10, 6, 6],
3715 "csharp self/base references",
3716 );
3717 assert_keywords_are_operands_only::<CsharpParser>(source, "foo.cs", &["this", "base"]);
3718 }
3719
3720 // An `indexer_declaration` spells the member's *name* with the same
3721 // `this` token kind the receiver uses (both are kind 91), so a
3722 // blanket move would have billed a declarator keyword as a value.
3723 // The declaration keeps the operator classification that the
3724 // `operator` keyword of an overload declaration already has (#1380).
3725 //
3726 // The fixture carries both uses, which is what makes the gate
3727 // observable: `this` has to reach *both* vocabularies from one file.
3728 // Asserting only the operand side would pass with the gate deleted,
3729 // and only the operator side would pass with the whole #1380 change
3730 // reverted (grammar-dispatch section 11).
3731 #[cfg(feature = "csharp")]
3732 #[test]
3733 fn csharp_indexer_declaration_keyword_is_not_a_self_reference() {
3734 let source = "class C {\n int[] _a;\n \
3735 public int this[int i] { get { return this._a[i]; } }\n}";
3736
3737 // Asserted per space, not through the root. The root merges
3738 // every space's vocabulary, and the fixture has one declarator
3739 // `this` and one receiver `this`, so at the root `this` is a
3740 // member of *both* vocabularies whether the gate is right or
3741 // exactly backwards — and every Halstead count is bit-identical
3742 // under the swap, one occurrence on each side either way. The
3743 // space tree is where the two uses stay apart: the declarator
3744 // sits in the class's own vocabulary and the receiver in the
3745 // accessor's.
3746 let mut roles = Vec::new();
3747 collect_keyword_roles(
3748 &ops_of::<CsharpParser>(source, "foo.cs"),
3749 "this",
3750 &mut roles,
3751 );
3752
3753 // `Both` can only arise from two `this` nodes in one space that
3754 // the getter classifies differently — it is the gate's whole
3755 // effect, and it disappears the moment `this` gets one role.
3756 // `OperandOnly` is the accessor, the innermost space and the one
3757 // holding the receiver alone; it is what flips when the gate is
3758 // inverted rather than dropped. Correct gives
3759 // `[Both, Both, OperandOnly]`; inverting gives
3760 // `[Both, Both, OperatorOnly]`; dropping the gate gives three
3761 // `OperandOnly`; reverting #1380 gives three `OperatorOnly`.
3762 // Asserting the whole sequence rather than two `contains` calls:
3763 // the comment above already derives it, and a `contains` pair
3764 // leaves the length unasserted, so a space appearing or vanishing
3765 // goes unnoticed and `[Both, X, OperandOnly]` passes for any `X`.
3766 assert_eq!(
3767 roles,
3768 vec![Role::Both, Role::Both, Role::OperandOnly],
3769 "the declarator and the receiver must disagree in the two outer spaces \
3770 and the accessor must bill `this` as an operand alone",
3771 );
3772 }
3773
3774 /// One `this` and one `base` per container the pinned
3775 /// `tree-sitter-c-sharp` can put them in: a delegating constructor
3776 /// initializer, a base-constructor initializer, a member access, an
3777 /// element access, and a call argument. No `indexer_declaration` —
3778 /// that position is the gated one and is an operator.
3779 #[cfg(feature = "csharp")]
3780 const CSHARP_SELF_POSITIONS: &str = "class Pos : B {
3781 int[] _a;
3782 public Pos() : this(1) { }
3783 public Pos(int x) : base(x) { }
3784 int A() { return this._a[0]; }
3785 int C() { return base.H(); }
3786 int E() { return base[0]; }
3787 int F() { return this[0]; }
3788 void G() { M(this); }
3789 void M(object o) { }
3790 }";
3791
3792 #[cfg(feature = "csharp")]
3793 #[test]
3794 fn csharp_self_and_base_leaves_are_unaliased_and_unconditional() {
3795 assert_self_reference_leaves::<CsharpCode>(
3796 CSHARP_SELF_POSITIONS,
3797 &[
3798 ("this", Csharp::This as u16, 4),
3799 ("base", Csharp::Base as u16, 3),
3800 ],
3801 "csharp",
3802 );
3803 }
3804
3805 /// Every spelling of the six keywords `_parameter_type_with_modifiers`
3806 /// aliases to `modifier`, plus two controls that were billed before
3807 /// #1418: the bare `params` of a parameter array (kind 94, never
3808 /// aliased) and the bare `out` of a call argument (kind 56).
3809 ///
3810 /// `D` is declared *and* called so the argument `out` has a caller
3811 /// to sit in, which is what makes the two spellings of one keyword
3812 /// meet in one file.
3813 #[cfg(feature = "csharp")]
3814 const CSHARP_PARAMETER_MODIFIERS: &str = "static class E {
3815 static void A(this Foo f) { }
3816 static void B(scoped ref Foo f) { }
3817 static void C(ref readonly Foo f) { }
3818 static void D(out int x) { x = 1; }
3819 static void G(in Foo f) { }
3820 static void H(params int[] xs) { }
3821 static void I(ref int y) { D(out y); }
3822}";
3823
3824 /// Each of the five operator keywords in *both* of its kind
3825 /// spellings — the childless `modifier` of a parameter, and the
3826 /// keyword's own token elsewhere: `ref` as a ref local, ref
3827 /// initializer and ref argument (kind 28); `readonly` as the leaf
3828 /// under a field's `modifier` wrapper (48); `in` as the `foreach`
3829 /// separator and an `in` argument (55); `out` as an out argument
3830 /// (56); `scoped` as a scoped ref local (93).
3831 ///
3832 /// Separate from [`CSHARP_PARAMETER_MODIFIERS`] because that fixture
3833 /// pairs the two spellings for `out` alone, which left four of the
3834 /// five bare kinds `CsharpCode::is_primitive` lists with no input
3835 /// (#1418).
3836 #[cfg(feature = "csharp")]
3837 const CSHARP_MODIFIER_BOTH_SPELLINGS: &str = "class S {
3838 static readonly int q = 0;
3839 static void M(scoped ref int a, ref readonly int b, in int c, out int d) { d = q; }
3840 static void N(int[] xs) {
3841 ref int r = ref xs[0];
3842 scoped ref int s = ref xs[1];
3843 foreach (var e in xs) { }
3844 M(ref r, in s, in s, out r);
3845 }
3846}";
3847
3848 /// The six aliased parameter modifiers reach a Halstead half, and
3849 /// each reaches the same one its bare token reaches elsewhere
3850 /// (#1418).
3851 #[cfg(feature = "csharp")]
3852 #[test]
3853 fn csharp_parameter_modifiers_are_classified_1418() {
3854 // Operators (n1 = 15, N1 = 48): class 1; {} 8 (the class body
3855 // and seven method bodies); static 8 (the class and seven
3856 // methods); void 7; () 8 (seven parameter lists and the call);
3857 // int 3 (`out int x`, `int[]`, `ref int y`); [] 1; = 1; ; 2;
3858 // scoped 1; ref 3; readonly 1; out 2 (the parameter and the
3859 // argument); in 1; params 1.
3860 //
3861 // Operands (n2 = 15, N2 = 24): E 1; A/B/C/G/H/I 1 each; D 2
3862 // (declaration and call); this 1; Foo 4; f 4; x 2; 1 1; xs 1;
3863 // y 2.
3864 //
3865 // Before the fix this read (11, 41, 14, 23). The eight childless
3866 // `modifier` nodes were billed nowhere, which cost seven
3867 // operator occurrences and one operand occurrence, and emptied
3868 // `scoped`, `ref`, `readonly` and `in` out of the operator
3869 // vocabulary entirely — `out` survived only because the call
3870 // argument spells it with the unaliased kind.
3871 assert_halstead_counts::<CsharpParser>(
3872 CSHARP_PARAMETER_MODIFIERS,
3873 "foo.cs",
3874 [15, 48, 15, 24],
3875 "csharp parameter modifiers",
3876 );
3877
3878 // The receiver is an operand, matching every other C# `this`
3879 // (#1380). Both halves matter: an arm listing it twice would
3880 // still put it among the operands.
3881 assert_keywords_are_operands_only::<CsharpParser>(
3882 CSHARP_PARAMETER_MODIFIERS,
3883 "foo.cs",
3884 &["this"],
3885 );
3886
3887 let ops = ops_of::<CsharpParser>(CSHARP_PARAMETER_MODIFIERS, "foo.cs");
3888 for keyword in ["ref", "out", "in", "scoped", "readonly", "params"] {
3889 assert!(
3890 ops.operators.iter().any(|o| o == keyword),
3891 "`{keyword}` must be an operator; operators were {:?}",
3892 ops.operators
3893 );
3894 assert!(
3895 !ops.operands.iter().any(|o| o == keyword),
3896 "`{keyword}` must not be an operand; operands were {:?}",
3897 ops.operands
3898 );
3899 }
3900
3901 // The vocabulary is not deduplicated — it is the concatenation
3902 // of the kind-keyed and the lexeme-keyed operator maps — so a
3903 // keyword appearing twice is the #453 shape: one keyword split
3904 // across the two maps and counted twice in `n1`. Keeping both
3905 // spellings in one map is what `CsharpCode::is_primitive`
3906 // listing the five bare kinds buys, and this is the only
3907 // assertion that can see it.
3908 //
3909 // It needs a fixture carrying *both* spellings of each keyword,
3910 // which `CSHARP_PARAMETER_MODIFIERS` does for `out` alone: its
3911 // `ref`, `in`, `scoped` and `readonly` are all in parameter
3912 // position, so for those the count can only ever be one and the
3913 // assertion is dead. That left four of the five bare kinds with
3914 // no input at all — dropping `Csharp::Ref`, `In`, `Readonly` or
3915 // `Scoped` from `is_primitive` failed no test in the workspace.
3916 assert_modifier_spellings_are_one_operator(
3917 CSHARP_MODIFIER_BOTH_SPELLINGS,
3918 &[
3919 ("ref", Csharp::Ref as u16),
3920 ("out", Csharp::Out as u16),
3921 ("in", Csharp::In as u16),
3922 ("scoped", Csharp::Scoped as u16),
3923 ("readonly", Csharp::Readonly as u16),
3924 ],
3925 );
3926 }
3927
3928 /// Asserts each keyword in `spellings` appears in `source` both as
3929 /// its own `kind` and as a childless aliased `modifier`, and that the
3930 /// two spellings render as exactly one entry in the operator
3931 /// vocabulary.
3932 ///
3933 /// The vocabulary is the concatenation of the kind-keyed and the
3934 /// lexeme-keyed operator maps, so a keyword listed twice is one
3935 /// keyword split across both maps and counted twice in `n1`.
3936 #[cfg(feature = "csharp")]
3937 #[track_caller]
3938 fn assert_modifier_spellings_are_one_operator(source: &str, spellings: &[(&str, u16)]) {
3939 let code = source.as_bytes();
3940 // (bare, aliased) occurrences per keyword.
3941 let mut seen = vec![(0_usize, 0_usize); spellings.len()];
3942
3943 for_each_node_with_chain::<CsharpCode>(code, |node, _| {
3944 let text = &code[node.start_byte()..node.end_byte()];
3945 let Some(index) = spellings.iter().position(|(kw, _)| kw.as_bytes() == text) else {
3946 return;
3947 };
3948 if node.kind_id() == Csharp::Modifier as u16 && node.child_count() == 0 {
3949 seen[index].1 += 1;
3950 } else if node.kind_id() == spellings[index].1 {
3951 seen[index].0 += 1;
3952 }
3953 });
3954
3955 // Without this the vocabulary count below is vacuous for any
3956 // keyword the fixture stopped spelling both ways — the decay
3957 // that once left four of #1418's bare kinds with no input.
3958 for ((keyword, _), (bare, aliased)) in spellings.iter().zip(&seen) {
3959 assert!(
3960 *bare > 0 && *aliased > 0,
3961 "fixture must spell `{keyword}` both ways; it has {bare} bare and {aliased} aliased"
3962 );
3963 }
3964
3965 let ops = ops_of::<CsharpParser>(source, "both.cs");
3966 for (keyword, _) in spellings {
3967 assert_eq!(
3968 ops.operators.iter().filter(|o| o == keyword).count(),
3969 1,
3970 "`{keyword}` must be one operator across both of its kind spellings; \
3971 operators were {:?}",
3972 ops.operators
3973 );
3974 }
3975 }
3976
3977 /// Pins the grammar shape the #1418 arm gates on, in both
3978 /// directions.
3979 ///
3980 /// The arm reads `modifier` nodes with no children as aliased bare
3981 /// keywords and leaves the rest to their classified keyword leaf.
3982 /// That is a claim about two populations, and a grammar bump can
3983 /// break either: giving an alias a token child would double-count
3984 /// it, and flattening a declaration modifier to a childless node
3985 /// would bill `public` / `static` as an unknown keyword. Both are
3986 /// checked here, so neither can move quietly.
3987 ///
3988 /// Childlessness says "aliased", *not* "parameter modifier" —
3989 /// `_lambda_expression_init` and `anonymous_method_expression` alias
3990 /// a bare `static` / `async` onto the same kind, which the second
3991 /// walk below pins so the distinction cannot be forgotten. Both are
3992 /// operators, as their declaration spelling is (#1482).
3993 #[cfg(feature = "csharp")]
3994 #[test]
3995 fn csharp_aliased_modifier_is_childless() {
3996 let code = CSHARP_PARAMETER_MODIFIERS.as_bytes();
3997 let (mut aliased, mut declaration) = (0_usize, 0_usize);
3998
3999 for_each_node_with_chain::<CsharpCode>(code, |node, chain| {
4000 if node.kind_id() != Csharp::Modifier as u16 {
4001 return;
4002 }
4003 let role = CsharpCode::get_op_type_with_code(node, code, Ancestors::known(chain));
4004 let text = &code[node.start_byte()..node.end_byte()];
4005
4006 if node.child_count() == 0 {
4007 aliased += 1;
4008 // Spelled out rather than `this`-versus-everything-else,
4009 // so a text this fixture does not carry fails loudly;
4010 // the lambda spellings are owned by the second walk.
4011 let (correct, expected) = match text {
4012 b"this" => (matches!(role, TokenRole::Operand), "an operand"),
4013 b"scoped" | b"ref" | b"out" | b"in" | b"readonly" => {
4014 (matches!(role, TokenRole::Operator), "an operator")
4015 }
4016 other => panic!(
4017 "`{}` is not one of the six parameter spellings this fixture carries",
4018 String::from_utf8_lossy(other)
4019 ),
4020 };
4021 assert!(
4022 correct,
4023 "`{}` is an aliased parameter modifier and must be {expected}",
4024 String::from_utf8_lossy(text)
4025 );
4026 // Grammar-dispatch section 5: nothing containing the
4027 // token is classified, so billing the token cannot
4028 // count the same source text twice. Every ancestor,
4029 // not just `parameter` — `parameter_list` and the
4030 // declaration above it contain it too.
4031 for (depth, ancestor) in chain.iter().enumerate() {
4032 assert!(
4033 matches!(
4034 CsharpCode::get_op_type_with_code(
4035 ancestor,
4036 code,
4037 Ancestors::known(&chain[..depth])
4038 ),
4039 TokenRole::Unknown
4040 ),
4041 "`{}` contains an aliased modifier and is itself classified, so the \
4042 keyword now counts twice",
4043 ancestor.kind()
4044 );
4045 }
4046 } else {
4047 declaration += 1;
4048 assert_eq!(
4049 node.child_count(),
4050 1,
4051 "a declaration `modifier` wraps exactly one keyword leaf"
4052 );
4053 assert!(
4054 matches!(role, TokenRole::Unknown),
4055 "the `{}` wrapper must stay unclassified; its leaf carries the operator",
4056 String::from_utf8_lossy(text)
4057 );
4058 let leaf = node.child(0).expect("child_count is 1");
4059 let mut leaf_chain = chain.to_vec();
4060 leaf_chain.push(*node);
4061 assert!(
4062 matches!(
4063 CsharpCode::get_op_type_with_code(
4064 &leaf,
4065 code,
4066 Ancestors::known(&leaf_chain)
4067 ),
4068 TokenRole::Operator
4069 ),
4070 "the leaf under a declaration `modifier` must carry the operator"
4071 );
4072 }
4073 });
4074
4075 // Eight of each: `this`, `scoped`, `ref` x3, `readonly`, `out`,
4076 // `in` in parameter position, and the `static` of the class and
4077 // of its seven methods. Asserted so a fixture that stopped
4078 // carrying one population cannot pass having checked nothing.
4079 assert_eq!(aliased, 8, "fixture lost an aliased parameter modifier");
4080 assert_eq!(declaration, 8, "fixture lost a declaration modifier");
4081
4082 // The second alias site. `static (int x) => …` and `async
4083 // delegate (int y) { … }` put a bare keyword under the same
4084 // childless `modifier`, so `csharp_is_aliased_modifier` holds
4085 // for a node that is not in a parameter at all — which is why
4086 // the getter dispatches on the text and not on the kind.
4087 let lambda = "static class L {
4088 static void M() {
4089 var a = static (int x) => x + 1;
4090 var b = async delegate (int y) { return y; };
4091 }
4092}"
4093 .as_bytes();
4094 let mut lambda_aliases = 0_usize;
4095
4096 for_each_node_with_chain::<CsharpCode>(lambda, |node, chain| {
4097 if node.kind_id() != Csharp::Modifier as u16 || node.child_count() != 0 {
4098 return;
4099 }
4100 lambda_aliases += 1;
4101 let text = &lambda[node.start_byte()..node.end_byte()];
4102 assert!(
4103 matches!(text, b"static" | b"async"),
4104 "`{}` is not a lambda modifier the grammar aliases",
4105 String::from_utf8_lossy(text)
4106 );
4107 assert!(
4108 matches!(
4109 CsharpCode::get_op_type_with_code(node, lambda, Ancestors::known(chain)),
4110 TokenRole::Operator
4111 ),
4112 "`{}` on a lambda must be an operator, as its declaration spelling is (#1482)",
4113 String::from_utf8_lossy(text)
4114 );
4115 });
4116
4117 assert_eq!(
4118 lambda_aliases, 2,
4119 "fixture lost a lambda / anonymous-method modifier alias"
4120 );
4121 }
4122
4123 /// `static` and `async` in both of their kind spellings: the keyword
4124 /// leaf under a method's `modifier` wrapper, and the childless
4125 /// `modifier` the grammar aliases a lambda's keyword to (#1482).
4126 #[cfg(feature = "csharp")]
4127 const CSHARP_LAMBDA_MODIFIER_BOTH_SPELLINGS: &str = "class L {
4128 static void N() { }
4129 async void M() { }
4130 void P() {
4131 var a = static (int x) => x;
4132 var b = async (int y) => y;
4133 }
4134}";
4135
4136 /// A C# lambda's bare parameter (`x => x`) is the operand its
4137 /// parenthesised twin (`(x) => x`) bills (#1544). Each row is one
4138 /// lambda in a method body; the twins differ only by the `()` the
4139 /// parenthesised spelling writes, so the operand halves must match
4140 /// and the operator halves differ by exactly that one `()`.
4141 #[cfg(feature = "csharp")]
4142 #[test]
4143 fn csharp_implicit_lambda_parameter_is_an_operand() {
4144 let wrap = |lambda: &str| {
4145 format!("class C {{\n void M() {{\n var f = {lambda};\n }}\n}}\n")
4146 };
4147 // Operators: class, {} x2, void, () x1 (M's parameter list), =,
4148 // =>, ; — n1 = 7, N1 = 8 — plus the lambda's own `()` in the
4149 // parenthesised spelling. Operands: C, M, f, and the parameter
4150 // twice; `var` is an unclassified `implicit_type`. `_ => 0`
4151 // names `_` once and adds the literal `0`.
4152 //
4153 // Before #1544 the bare rows read (7, 8, 4, 4) for both: `x`
4154 // lost one occurrence, and `_` left the vocabulary entirely.
4155 for (lambda, expected) in [
4156 ("x => x", [7, 8, 4, 5]),
4157 ("(x) => x", [7, 9, 4, 5]),
4158 ("_ => 0", [7, 8, 5, 5]),
4159 ("(_) => 0", [7, 9, 5, 5]),
4160 ] {
4161 assert_halstead_counts::<CsharpParser>(&wrap(lambda), "foo.cs", expected, lambda);
4162 }
4163
4164 // The bare parameter is billed under its own text, not under
4165 // the kind name, and as an operand only.
4166 assert_keywords_are_operands_only::<CsharpParser>(&wrap("_ => 0"), "foo.cs", &["_"]);
4167 }
4168
4169 /// A lambda's `static` / `async` is the same operator as the
4170 /// declaration's, counted once in `n1` across both spellings
4171 /// (#1482).
4172 #[cfg(feature = "csharp")]
4173 #[test]
4174 fn csharp_lambda_modifiers_are_operators_1482() {
4175 // Operators (n1 = 10, N1 = 25): class 1; {} 4 (the class body
4176 // and three method bodies); static 2; async 2; void 3; () 5
4177 // (three method parameter lists and two lambda ones); int 2;
4178 // = 2; ; 2; => 2.
4179 //
4180 // Operands (n2 = 8, N2 = 10): L, N, M, P, a, b once each; x 2
4181 // and y 2 (the parameter and the lambda body). `var` is an
4182 // unclassified `implicit_type` wrapper.
4183 //
4184 // Before the fix this read (10, 23, 8, 10): the two lambda
4185 // keywords were billed nowhere. Billing them without listing
4186 // `Static` / `Async` in `CsharpCode::is_primitive` reads
4187 // (12, 25, 8, 10) — each keyword split across the kind-keyed
4188 // and the lexeme-keyed operator map.
4189 assert_halstead_counts::<CsharpParser>(
4190 CSHARP_LAMBDA_MODIFIER_BOTH_SPELLINGS,
4191 "foo.cs",
4192 [10, 25, 8, 10],
4193 "csharp lambda modifiers",
4194 );
4195
4196 assert_modifier_spellings_are_one_operator(
4197 CSHARP_LAMBDA_MODIFIER_BOTH_SPELLINGS,
4198 &[
4199 ("static", Csharp::Static as u16),
4200 ("async", Csharp::Async as u16),
4201 ],
4202 );
4203 }
4204
4205 #[cfg(feature = "go")]
4206 #[test]
4207 fn go_operators_and_operands() {
4208 check_metrics::<GoParser>(
4209 "package main
4210 func sum(a, b int) int {
4211 return a + b
4212 }",
4213 "foo.go",
4214 |metric| {
4215 insta::assert_json_snapshot!(
4216 metric.halstead,
4217 @r#"
4218 {
4219 "unique_operators": 7,
4220 "total_operators": 7,
4221 "unique_operands": 5,
4222 "total_operands": 8,
4223 "length": 15,
4224 "estimated_program_length": 31.26112492884004,
4225 "purity_ratio": 2.0840749952560027,
4226 "vocabulary": 12,
4227 "volume": 53.77443751081734,
4228 "difficulty": 5.6,
4229 "level": 0.17857142857142858,
4230 "effort": 301.1368500605771,
4231 "time": 16.729825003365395,
4232 "bugs": 0.014975730436275946
4233 }
4234 "#
4235 );
4236 },
4237 );
4238 }
4239
4240 #[cfg(feature = "perl")]
4241 #[test]
4242 fn perl_operators_and_operands() {
4243 check_metrics::<PerlParser>(
4244 "sub sum {
4245 my ($a, $b) = @_;
4246 return $a + $b;
4247 }",
4248 "foo.pl",
4249 |metric| {
4250 insta::assert_json_snapshot!(
4251 metric.halstead,
4252 @r#"
4253 {
4254 "unique_operators": 10,
4255 "total_operators": 14,
4256 "unique_operands": 4,
4257 "total_operands": 6,
4258 "length": 20,
4259 "estimated_program_length": 41.219280948873624,
4260 "purity_ratio": 2.0609640474436812,
4261 "vocabulary": 14,
4262 "volume": 76.14709844115208,
4263 "difficulty": 7.5,
4264 "level": 0.13333333333333333,
4265 "effort": 571.1032383086406,
4266 "time": 31.727957683813365,
4267 "bugs": 0.02294502281013948
4268 }
4269 "#
4270 );
4271 },
4272 );
4273 }
4274
4275 #[cfg(feature = "perl")]
4276 #[test]
4277 fn perl_interpolated_string_no_double_count() {
4278 // Regression: issue #199. A `string_double_quoted` (and
4279 // `string_qq_quoted` / `backtick_quoted` / `command_qx_quoted`)
4280 // wrapping an `interpolation` child used to be counted as a
4281 // Halstead operand while the inner scalar/array/hash variable
4282 // was also walked and counted — double-counting the inner
4283 // variable's contribution to `N2`. Mirrors #180 (Bash/Elixir),
4284 // #183 (C#), #184 (PHP), #191 (Kotlin).
4285 //
4286 // expected: for
4287 // sub greet { my $name = shift; my $msg = "Hi $name"; return $msg; }
4288 // — operands are `greet`, `$name`, `shift`, `$msg`. With the
4289 // fix the wrapping `"Hi $name"` is skipped (has `Interpolation`
4290 // child), so u_operands = 4 and N2 = 6 (`$name` x2 from the
4291 // `my` binding and the interpolation; `$msg` x2 from the `my`
4292 // binding and `return`; `greet`, `shift` once each). Without
4293 // the fix the wrapping literal would also be counted, lifting
4294 // u_operands to 5 and N2 to 7.
4295 check_metrics::<PerlParser>(
4296 "sub greet { my $name = shift; my $msg = \"Hi $name\"; return $msg; }",
4297 "foo.pl",
4298 |metric| {
4299 assert_eq!(metric.halstead.unique_operands(), 4);
4300 assert_eq!(metric.halstead.total_operands(), 6);
4301 insta::assert_json_snapshot!(metric.halstead);
4302 },
4303 );
4304 }
4305
4306 #[cfg(feature = "perl")]
4307 #[test]
4308 fn perl_plain_string_still_operand() {
4309 // The fix for #199 only skips wrapping literals that carry an
4310 // `Interpolation` child; a plain `"hello"` (no `$…` inside)
4311 // must still contribute exactly one operand. expected: operands
4312 // `greet`, `$msg`, `"hello"` → u_operands = 3, N2 = 4 (`$msg`
4313 // appears in the `my` binding and the `return`).
4314 check_metrics::<PerlParser>(
4315 "sub greet { my $msg = \"hello\"; return $msg; }",
4316 "foo.pl",
4317 |metric| {
4318 assert_eq!(metric.halstead.unique_operands(), 3);
4319 assert_eq!(metric.halstead.total_operands(), 4);
4320 },
4321 );
4322 }
4323
4324 #[cfg(feature = "perl")]
4325 #[test]
4326 fn perl_single_quoted_string_never_interpolates() {
4327 // Single-quoted (`'…'`) and `q{…}` literals are not subject to
4328 // interpolation in Perl, so even when their text contains a
4329 // `$name`-shaped sequence the wrapper is still counted as one
4330 // operand and the inner text is not parsed as a variable.
4331 // expected: operands `greet`, `$msg`, `'Hi $name'` →
4332 // u_operands = 3, N2 = 4 (`$msg` x2).
4333 check_metrics::<PerlParser>(
4334 "sub greet { my $msg = 'Hi $name'; return $msg; }",
4335 "foo.pl",
4336 |metric| {
4337 assert_eq!(metric.halstead.unique_operands(), 3);
4338 assert_eq!(metric.halstead.total_operands(), 4);
4339 },
4340 );
4341 }
4342
4343 #[cfg(feature = "perl")]
4344 #[test]
4345 fn perl_plain_heredoc_counts_as_one_operand() {
4346 // Regression: issue #287. A plain (non-interpolating) Perl
4347 // heredoc body used to be classified `TokenRole::Unknown`,
4348 // so its visible `HeredocBodyStatement` node contributed
4349 // nothing to N2 even though it is a string literal. The fix
4350 // adds `HeredocBodyStatement` to the interpolation-aware
4351 // operand arm, so an inert heredoc counts as one operand.
4352 //
4353 // Source (heredoc body lives at the source_file level, not
4354 // inside any sub):
4355 // my $msg = <<END;
4356 // hello world
4357 // END
4358 //
4359 // Operands traversed:
4360 // * `$msg` (`scalar_variable`) × 1
4361 // * heredoc body (`heredoc_body_statement`) × 1
4362 // expected: u_operands = 2, N2 = 2.
4363 check_metrics::<PerlParser>("my $msg = <<END;\nhello world\nEND\n", "foo.pl", |metric| {
4364 assert_eq!(metric.halstead.unique_operands(), 2);
4365 assert_eq!(metric.halstead.total_operands(), 2);
4366 });
4367 }
4368
4369 #[cfg(feature = "perl")]
4370 #[test]
4371 fn perl_interpolated_heredoc_no_double_count() {
4372 // Regression: issue #287. An interpolating Perl heredoc
4373 // (`<<"TAG"` or bare `<<TAG`) carries an `Interpolation` child
4374 // when its body contains a `$var`. The wrapper must drop to
4375 // `Unknown` so the inner scalar variable carries the operand
4376 // count — same dispatch as the existing double-quoted /
4377 // backtick / qx wrappers (issue #199) and the PHP heredoc fix
4378 // (issue #184).
4379 //
4380 // Source:
4381 // my $name = "x";
4382 // my $msg = <<"END";
4383 // hi $name
4384 // END
4385 //
4386 // Operands by text key:
4387 // * `$name` × 2 (my-binding + interpolation inside heredoc)
4388 // * `"x"` × 1 (inert double-quoted string)
4389 // * `$msg` × 1
4390 // expected: u_operands = 3, N2 = 4. Without the
4391 // interpolation-aware drop the wrapping heredoc body would
4392 // also count, lifting u_operands to 4 and N2 to 5.
4393 check_metrics::<PerlParser>(
4394 "my $name = \"x\";\nmy $msg = <<\"END\";\nhi $name\nEND\n",
4395 "foo.pl",
4396 |metric| {
4397 assert_eq!(metric.halstead.unique_operands(), 3);
4398 assert_eq!(metric.halstead.total_operands(), 4);
4399 },
4400 );
4401 }
4402
4403 #[cfg(feature = "perl")]
4404 #[test]
4405 fn perl_bare_pattern_delimiters_are_not_operators() {
4406 // Regression: issue #1312, the Perl sibling of Elixir #1256.
4407 // The bare match form is the only one of Perl's regex literals
4408 // whose delimiters are spelled with an operator token kind —
4409 // `bca dump` shows `/abc/` emitting two `SLASH` under
4410 // `PatternMatcher` — so `$s =~ /abc/;` reported a `/` operator
4411 // with no division in the source.
4412 //
4413 // expected: operators `$` (the `scalar_variable` sigil), `=~`
4414 // and `;` → n1 = N1 = 3. Operands `$s` and the `/abc/` pattern
4415 // → n2 = N2 = 2. Before the guard the two delimiters added `/`
4416 // → n1 = 4, N1 = 5; the pattern operand arrived with #1314,
4417 // which promoted all three pattern spellings together (see
4418 // `perl_every_pattern_value_spelling_scores_alike`).
4419 check_metrics::<PerlParser>("$s =~ /abc/;\n", "foo.pl", |metric| {
4420 assert_eq!(metric.halstead.unique_operators(), 3);
4421 assert_eq!(metric.halstead.total_operators(), 3);
4422 assert_eq!(metric.halstead.unique_operands(), 2);
4423 assert_eq!(metric.halstead.total_operands(), 2);
4424 });
4425 }
4426
4427 #[cfg(feature = "perl")]
4428 #[test]
4429 fn perl_every_pattern_value_spelling_scores_alike() {
4430 // Companion to the test above (#1312, extended by #1314).
4431 // `m/abc/` is exactly `/abc/` in Perl and `qr/abc/` is the same
4432 // pattern as a value, so the three spellings of a pattern
4433 // *value* must score identically whatever delimiters they use.
4434 //
4435 // Until #1314 they scored alike at *zero*: no Perl pattern
4436 // wrapper was in the operand arm, so the literal never counted,
4437 // unlike Ruby's `Regex` and Elixir's `Sigil`. #1312 declined to
4438 // promote the bare form on its own precisely because that would
4439 // have scored `/abc/` at one operand and its synonyms at zero,
4440 // reintroducing the spelling sensitivity this test pins.
4441 // Promoting all three together closes the gap and keeps the
4442 // equality, which is what this test now asserts.
4443 //
4444 // `s///` and `tr///` are deliberately *not* rows here any more.
4445 // They are operations applied to a target rather than pattern
4446 // values, so #1314 made them operators; their own equality is
4447 // pinned by `perl_every_pattern_operation_spelling_scores_alike`
4448 // below. Splitting the one loop in two is the substantive
4449 // disagreement #1314 had with the reasoning recorded here: this
4450 // test governs *synonyms*, and `s///` is not a synonym of
4451 // `/abc/`.
4452 //
4453 // The fixture matches twice against one variable so that no two
4454 // of `[n1, N1, n2, N2]` are equal. A square tuple would leave
4455 // `assert_halstead_counts`' unique-vs-total axes unpinned —
4456 // transposing n1 with N1 inside the helper failed no test while
4457 // all three of its callers expected a square tuple.
4458 //
4459 // expected per variant: operators `$` × 2 (one per
4460 // `scalar_variable`), `=~` × 2, `and`, `;` → n1 = 4, N1 = 6.
4461 // The pattern contributes no *operator* — that is #1312's half
4462 // — and one operand, so with `$s` twice and the pattern twice
4463 // → n2 = 2, N2 = 4.
4464 for pattern in ["/abc/", "m/abc/", "m{abc}", "qr/abc/"] {
4465 assert_halstead_counts::<PerlParser>(
4466 &format!("$s =~ {pattern} and $s =~ {pattern};\n"),
4467 "foo.pl",
4468 [4, 6, 2, 4],
4469 &format!("pattern {pattern}"),
4470 );
4471 }
4472 }
4473
4474 #[cfg(feature = "perl")]
4475 #[test]
4476 fn perl_comment_leader_is_not_an_operator_1549() {
4477 // tree-sitter-perl emits a comment's leading `#` as a `#` token
4478 // under `comments`, and the getter listed it as an operator, so
4479 // every comment billed one `#` (#1549). The oracle is the same
4480 // source with its comments deleted, which must score identically.
4481 // `$#a` and `qw#…#` are kept to pin that dropping `#` leaves the
4482 // distinct `$#` operator and the `qw` delimiters alone.
4483 //
4484 // expected: operators `sub`, `{}`, `my`, `=`, `;` × 2, `return`,
4485 // `$#` × 2, `+` → n1 = 8, N1 = 10. Operands `f`, `@a`, `x`, `y`,
4486 // `a` × 2 → n2 = 5, N2 = 6. Before the fix the commented row
4487 // added `#` → n1 = 9, N1 = 13. Once excluded, a comment feeds no
4488 // axis to anchor on, so only the revert run guards the fixture:
4489 // keep its comments.
4490 for (source, label) in [
4491 (
4492 "sub f {\n # leading\n my @a = qw#x y#; # tail\n return $#a + $#a; # tail\n}\n",
4493 "commented",
4494 ),
4495 (
4496 "sub f {\n my @a = qw#x y#;\n return $#a + $#a;\n}\n",
4497 "comment-free twin",
4498 ),
4499 ] {
4500 assert_halstead_counts::<PerlParser>(source, "foo.pl", [8, 10, 5, 6], label);
4501 }
4502 }
4503
4504 #[cfg(feature = "perl")]
4505 #[test]
4506 fn perl_bare_pattern_text_bills_no_operator_1549() {
4507 // The bare `/…/` form keeps its pattern text in a
4508 // `regex_pattern_content` that spells `#`, `[`, `]` and `\` as
4509 // anonymous tokens, three of which an operator arm listed, so
4510 // `/a#b[c]\#/` billed `#`, `[]` and `\` operators. `m//` and
4511 // `qr//` emit no node for the same text, so they are the oracle:
4512 // all three spellings must score as `/abc/` does in
4513 // `perl_every_pattern_value_spelling_scores_alike`.
4514 //
4515 // expected per variant: operators `$` × 2, `=~` × 2, `and`, `;`
4516 // → n1 = 4, N1 = 6; operands `$s` × 2 and the pattern × 2 →
4517 // n2 = 2, N2 = 4. Before the fix the bare row scored n1 = 7,
4518 // N1 = 14. The `#`, `[c]` and `\#` feed no axis once excluded,
4519 // so nothing anchors them and only the revert run guards the
4520 // fixture: keep them in the bare row.
4521 for pattern in [r"/a#b[c]\#/", r"m/a#b[c]\#/", r"qr/a#b[c]\#/"] {
4522 assert_halstead_counts::<PerlParser>(
4523 &format!("$s =~ {pattern} and $s =~ {pattern};\n"),
4524 "foo.pl",
4525 [4, 6, 2, 4],
4526 &format!("pattern {pattern}"),
4527 );
4528 }
4529 }
4530
4531 #[cfg(feature = "perl")]
4532 #[test]
4533 fn perl_every_pattern_operation_spelling_scores_alike() {
4534 // The other half of the split (#1314). Substitution and
4535 // transliteration are operations applied to a target, so they
4536 // are operators — and like the value spellings, their delimiter
4537 // choice must not move the count. `y///` is a synonym of
4538 // `tr///` and shares `TransliterationTrOrY`, so the two fold to
4539 // one operator entry, which is why all four rows agree on n1.
4540 //
4541 // expected per variant: operators `$` × 2, `=~` × 2, `and`,
4542 // `;`, and the operation itself × 2 → n1 = 5, N1 = 8. The
4543 // pattern and replacement text is invisible to this grammar —
4544 // `substitution_pattern_s` emits only its keyword and
4545 // delimiters, no content node — so the sole operand is `$s`,
4546 // twice → n2 = 1, N2 = 2.
4547 for pattern in ["s/a/b/", "s{a}{b}", "tr/a/b/", "y/a/b/"] {
4548 assert_halstead_counts::<PerlParser>(
4549 &format!("$s =~ {pattern} and $s =~ {pattern};\n"),
4550 "foo.pl",
4551 [5, 8, 1, 2],
4552 &format!("pattern {pattern}"),
4553 );
4554 }
4555 }
4556
4557 #[cfg(feature = "perl")]
4558 #[test]
4559 fn perl_interpolated_pattern_operands_agree_but_operators_do_not() {
4560 // Two things at once (#1314), because they are the same
4561 // measurement: why the three pattern-value spellings route
4562 // through `string_operand_type` rather than a plain operand
4563 // arm, and what that routing does *not* fix.
4564 //
4565 // `m/$x/` and `qr/$x/` emit a real `Interpolation` wrapping a
4566 // `scalar_variable`, while the bare form keeps its `$x` inside
4567 // an unclassified `regex_pattern_content`. So:
4568 //
4569 // * Operands agree at n2 = N2 = 2 (`$s` plus one contribution
4570 // from the pattern) only because of the interpolation guard.
4571 // A plain operand arm would count the wrapper *and* the inner
4572 // `$x` for the suffixed forms — n2 = 3 — reintroducing
4573 // through the back door the divergence
4574 // `perl_every_pattern_value_spelling_scores_alike` exists to
4575 // prevent. Which node carries the one operand still differs
4576 // by spelling: the wrapper for the bare form, the inner `$x`
4577 // for the other two.
4578 // * Operators do *not* agree: the exposed `scalar_variable`
4579 // brings a `$` sigil, an operator here, that the bare form
4580 // has no node for. Over two matches N1 is 6 for `/$x/` and
4581 // 8 for the other two.
4582 //
4583 // The operator asymmetry is a grammar gap this classifier
4584 // cannot repair — there is nothing to classify in the bare
4585 // form — so it is pinned rather than papered over, the same
4586 // treatment `perl_division_emits_no_slash_token` gives the
4587 // missing division token. A bump that starts exposing the bare
4588 // form's interpolation turns this red, at which point the
4589 // expectations above need re-deriving.
4590 // Each fixture matches twice, so `N1 > n1` and `N2 > n2` and no
4591 // row is a square tuple that a transposition inside
4592 // `assert_halstead_counts` could pass (#1312).
4593 assert_halstead_counts::<PerlParser>(
4594 "$s =~ /$x/ and $s =~ /$x/;\n",
4595 "foo.pl",
4596 [4, 6, 2, 4],
4597 "bare /$x/",
4598 );
4599 for pattern in ["m/$x/", "qr/$x/"] {
4600 assert_halstead_counts::<PerlParser>(
4601 &format!("$s =~ {pattern} and $s =~ {pattern};\n"),
4602 "foo.pl",
4603 [4, 8, 2, 4],
4604 &format!("suffixed {pattern}"),
4605 );
4606 }
4607 }
4608
4609 #[cfg(feature = "perl")]
4610 #[test]
4611 fn perl_division_emits_no_slash_token() {
4612 // Drift marker, not an endorsement. Ruby's counterpart
4613 // (`ruby_division_survives_the_regex_guard`) proves #1312's
4614 // guard cannot swallow a real division; Perl has no such
4615 // fixture to write, because at the pinned grammar `$a / $b`
4616 // emits *no* `SLASH` token at all — `binary_expression`'s
4617 // children skip straight from one `scalar_variable` to the
4618 // other. Perl division therefore counts zero operators today,
4619 // a pre-existing grammar gap this fix neither causes nor
4620 // repairs.
4621 //
4622 // Pinning it keeps the gap in CI: a bump that starts emitting
4623 // the token turns this red, at which point the division would
4624 // begin counting (its parent is `BinaryExpression`, not
4625 // `PatternMatcher`, so the guard leaves it alone) and the
4626 // expectations above need re-deriving.
4627 //
4628 // The same gap is why no Perl test can distinguish the
4629 // parent-scoped guard from an ancestor-scoped one: with no
4630 // `SLASH` reachable below a `PatternMatcher`, that mutant is
4631 // unobservable here — measured, not assumed. Perl's guard is
4632 // parent-scoped for correctness by construction and for
4633 // symmetry with Ruby's, where the distinction *is* observable
4634 // and is pinned by
4635 // `ruby_regex_guard_is_parent_scoped_not_ancestor_scoped`.
4636 let path = PathBuf::from("foo.pl");
4637 let source = "my $z = $a / $b;\n";
4638 let parser = PerlParser::new(source.as_bytes().to_vec(), &path, None);
4639 assert!(
4640 !ast_has_kind_id(&parser, Perl::SLASH as u16),
4641 "tree-sitter-perl still emits no SLASH for `{source}`"
4642 );
4643 // Anchor the negative assertion to *this* fixture. Without it
4644 // the test stays green when `source` is edited to something
4645 // containing no division at all — measured: swapping in
4646 // `my $z = 1;` failed nothing.
4647 //
4648 // expected: operators `my`, `=`, `$` × 3 (one per
4649 // `scalar_variable`), `;` → n1 = 4, N1 = 6, with no `/` among
4650 // them. Operands `$z`, `$a`, `$b` → n2 = N2 = 3.
4651 check_metrics::<PerlParser>(source, "foo.pl", |metric| {
4652 assert_eq!(metric.halstead.unique_operators(), 4);
4653 assert_eq!(metric.halstead.total_operators(), 6);
4654 assert_eq!(metric.halstead.unique_operands(), 3);
4655 assert_eq!(metric.halstead.total_operands(), 3);
4656 });
4657 // Positive control: the same kind *is* reachable in this
4658 // grammar, so the assertion above is about division and not
4659 // about `Perl::SLASH` being enum-only dead weight.
4660 let matcher = PerlParser::new(b"$s =~ /abc/;\n".to_vec(), &path, None);
4661 assert!(
4662 ast_has_kind_id(&matcher, Perl::SLASH as u16),
4663 "Perl::SLASH must be the bare pattern delimiter kind"
4664 );
4665 }
4666
4667 #[cfg(feature = "perl")]
4668 #[test]
4669 fn perl_readline_closer_alias_never_reaches_kind_id() {
4670 // Drift marker for `Perl::GT2` (#1395). The grammar closes
4671 // `<FH>` / `<$fh>` with `token.immediate('>')`, which the
4672 // generated parser gives its own symbol — hence the `GT2`
4673 // variant sitting next to `FileHandle` in the enum — but whose
4674 // `public_symbol_map` entry collapses it onto `GT` before
4675 // `kind_id()`, exactly like `LPAREN2` in #768 and Ruby's
4676 // `SLASH2` in #1312. Nothing in `getter/perl.rs` names it, and
4677 // nothing should: `compute_halstead` keys a non-primitive
4678 // operator on `kind_id()`, so two live kinds both rendering
4679 // `">"` would split one `>` across two `n1` entries. If a bump
4680 // ever starts emitting it, the repair is to route `>` through
4681 // the lexeme-keyed map, not to list both — and this test is
4682 // what says so.
4683 //
4684 // The C++ grammars' template-list closer collapses the same way;
4685 // `cpp_template_closer_alias_never_reaches_kind_id` pins that.
4686 let path = PathBuf::from("foo.pl");
4687 for source in ["my $l = <FH>;\n", "my $l = <$fh>;\n", "my $b = $x > $y;\n"] {
4688 let parser = PerlParser::new(source.as_bytes().to_vec(), &path, None);
4689 assert!(
4690 !ast_has_kind_id(&parser, Perl::GT2 as u16),
4691 "Perl::GT2 must stay collapsed to Perl::GT for `{source}`"
4692 );
4693 // Positive control: the id the operator arm fires on is
4694 // present, so the assertion above cannot pass merely
4695 // because the fixture grew no `>` at all.
4696 assert!(
4697 ast_has_kind_id(&parser, Perl::GT as u16),
4698 "Perl::GT must be the `>` kind for `{source}`"
4699 );
4700 }
4701 }
4702
4703 /// Drift marker for `Cpp::GT2` / `Mozcpp::GT2`. Both grammars close
4704 /// a template parameter or argument list with
4705 /// `alias(token(prec(1, '>')), '>')`, which the generated parser
4706 /// numbers separately, but `ts_symbol_map` folds that symbol onto
4707 /// `GT` before `kind_id()`, as with Perl's readline closer above. The
4708 /// `GT2` arms in `getter/cpp.rs` and `getter/mozcpp.rs` are therefore
4709 /// defensive; this fails if a grammar bump makes them live, at which
4710 /// point the two kinds would split one `>` across two `n1` entries.
4711 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
4712 fn assert_template_closer_folds_to_gt<P: crate::ParserTrait>(gt: u16, gt2: u16, label: &str) {
4713 // One parameter list (`S<T>`'s declaration) and two argument
4714 // lists, the inner one closed by the first half of a `>>`.
4715 let source = "template <typename T> struct S {};\n\
4716 std::vector<std::vector<int>> v;\n\
4717 bool b = x > y;\n";
4718 let parser = P::new(source.as_bytes().to_vec(), &PathBuf::from("foo.cpp"), None);
4719 assert!(
4720 !ast_has_kind_id(&parser, gt2),
4721 "{label}::GT2 must stay collapsed to {label}::GT",
4722 );
4723 // Positive control: every template-list closer is present and
4724 // carries `GT`, so the assertion above cannot pass merely
4725 // because the fixture stopped producing template lists.
4726 let closers = parser
4727 .root()
4728 .preorder()
4729 .filter(|n| {
4730 n.kind_id() == gt
4731 && n.parent().is_some_and(|p| {
4732 matches!(
4733 p.kind(),
4734 "template_parameter_list" | "template_argument_list"
4735 )
4736 })
4737 })
4738 .count();
4739 assert_eq!(
4740 closers, 3,
4741 "{label}: each template-list closer must be a `GT`"
4742 );
4743 }
4744
4745 #[cfg(feature = "cpp")]
4746 #[test]
4747 fn cpp_template_closer_alias_never_reaches_kind_id() {
4748 assert_template_closer_folds_to_gt::<CppParser>(Cpp::GT as u16, Cpp::GT2 as u16, "Cpp");
4749 }
4750
4751 #[cfg(feature = "mozcpp")]
4752 #[test]
4753 fn mozcpp_template_closer_alias_never_reaches_kind_id() {
4754 assert_template_closer_folds_to_gt::<MozcppParser>(
4755 Mozcpp::GT as u16,
4756 Mozcpp::GT2 as u16,
4757 "Mozcpp",
4758 );
4759 }
4760
4761 /// Asserts a grammar's wrapped generic-list opener, `LT2`, never
4762 /// reaches a node, so the `LT` its getter pairs as `<>` is the only
4763 /// `<` a list opens with (#1559, grammar-dispatch §2). `lists` are the
4764 /// lists the wrapped token opens; the positive control counts their
4765 /// `LT` openers, so the assertion cannot pass on a fixture that
4766 /// stopped producing those lists.
4767 #[cfg(any(feature = "groovy", feature = "objc", feature = "rust"))]
4768 #[track_caller]
4769 fn assert_generic_opener_folds_to_lt<P: crate::ParserTrait>(
4770 source: &str,
4771 [lt, lt2]: [u16; 2],
4772 lists: &[&str],
4773 openers: usize,
4774 label: &str,
4775 ) {
4776 let parser = P::new(source.as_bytes().to_vec(), &PathBuf::from("foo"), None);
4777 assert!(
4778 !ast_has_kind_id(&parser, lt2),
4779 "{label}::LT2 must stay collapsed to {label}::LT",
4780 );
4781 let found = parser
4782 .root()
4783 .preorder()
4784 .filter(|n| n.kind_id() == lt && n.parent().is_some_and(|p| lists.contains(&p.kind())))
4785 .count();
4786 assert_eq!(
4787 found, openers,
4788 "{label}: each generic-list opener must be an `LT`"
4789 );
4790 }
4791
4792 #[cfg(feature = "rust")]
4793 #[test]
4794 fn rust_generic_opener_alias_never_reaches_kind_id() {
4795 assert_generic_opener_folds_to_lt::<RustParser>(
4796 "fn u<T>(x: Vec<T>) -> impl Sized + use<T> { x }\n",
4797 [Rust::LT as u16, Rust::LT2 as u16],
4798 &["type_arguments", "use_bounds"],
4799 2,
4800 "Rust",
4801 );
4802 }
4803
4804 #[cfg(feature = "groovy")]
4805 #[test]
4806 fn groovy_generic_opener_alias_never_reaches_kind_id() {
4807 assert_generic_opener_folds_to_lt::<GroovyParser>(
4808 "class A<T> { List<T> v }\n",
4809 [Groovy::LT as u16, Groovy::LT2 as u16],
4810 &["type_parameters", "type_arguments"],
4811 2,
4812 "Groovy",
4813 );
4814 }
4815
4816 #[cfg(feature = "objc")]
4817 #[test]
4818 fn objc_generic_opener_alias_never_reaches_kind_id() {
4819 assert_generic_opener_folds_to_lt::<ObjcParser>(
4820 "int f(void) { return g(NSArray<NSString *>); }\n",
4821 [Objc::LT as u16, Objc::LT2 as u16],
4822 &["argument_list"],
4823 1,
4824 "Objc",
4825 );
4826 }
4827
4828 #[cfg(feature = "perl")]
4829 #[test]
4830 fn perl_readline_brackets_are_operators() {
4831 // #1395: a readline's `<` / `>` delimit syntax, not a literal,
4832 // so under the policy on `Getter::get_op_type` they are
4833 // ordinary operators — even though ABC's `conditions` excludes
4834 // them (#1297). `<STDIN>` is the one spelling that cannot show
4835 // this: the grammar lexes it as a single `standard_input`
4836 // token, so there are no brackets to classify.
4837 //
4838 // expected: operators `sub`, `{}`, `my`, `$` × 2 (one per
4839 // `scalar_variable`), `=`, `<`, `>`, `;` × 2, `return` →
4840 // n1 = 9, N1 = 11. Operands `r`, `$l` × 2, `FH` →
4841 // n2 = 3, N2 = 4.
4842 check_metrics::<PerlParser>("sub r { my $l = <FH>; return $l; }\n", "foo.pl", |metric| {
4843 assert_eq!(metric.halstead.unique_operators(), 9);
4844 assert_eq!(metric.halstead.total_operators(), 11);
4845 assert_eq!(metric.halstead.unique_operands(), 3);
4846 assert_eq!(metric.halstead.total_operands(), 4);
4847 });
4848 }
4849
4850 /// Every (name wrapper, contained operand) pairing tree-sitter-perl's
4851 /// node-types.json admits, and the single source of truth for both
4852 /// halves of #1355's guard: its parent set is the distinct first
4853 /// components, the kinds it subsumes the distinct second ones.
4854 /// `perl_name_wrappers_bill_the_name_once_1355` witnesses every row
4855 /// and fails on an eleventh pairing.
4856 #[cfg(feature = "perl")]
4857 const PERL_NAME_WRAPPER_PAIRINGS: [(Perl, Perl); 10] = [
4858 (Perl::PackageName, Perl::Identifier),
4859 (Perl::PackageName, Perl::ScalarVariable),
4860 (Perl::PackageName, Perl::ArrayVariable),
4861 (Perl::PackageName, Perl::HashVariable),
4862 (Perl::PackageName, Perl::SpecialScalarVariable),
4863 (Perl::PackageName, Perl::Typeglob),
4864 (Perl::PackageName, Perl::PackageVariable),
4865 (Perl::PackageVariable, Perl::PackageName),
4866 (Perl::PackageVariable, Perl::ScalarVariable),
4867 (Perl::Typeglob, Perl::Identifier),
4868 ];
4869
4870 /// The anonymous tokens those wrappers also hold. `::` and `*` are
4871 /// operators (matched above the guard, so they keep that reading);
4872 /// `{` folds into the `{}` glyph and `}` has never been classified
4873 /// at all. Listing them is what lets
4874 /// `perl_name_wrappers_bill_the_name_once_1355` police *every*
4875 /// child rather than only the named ones — the operand arm carries
4876 /// token-shaped kinds too (`True`, `FILE`, `SUB`, …), and a bump
4877 /// that let one of those inside a wrapper would otherwise be
4878 /// silenced with nothing failing.
4879 #[cfg(feature = "perl")]
4880 const PERL_NAME_WRAPPER_TOKENS: [Perl; 4] =
4881 [Perl::COLONCOLON, Perl::STAR, Perl::LBRACE, Perl::RBRACE];
4882
4883 /// The occurrences #1355's guard suppresses in `source`, paired
4884 /// with the (wrapper kind, child kind) pairings they witness.
4885 ///
4886 /// Walks with `for_each_node_with_chain`, which maintains the
4887 /// ancestor chain exactly as `spaces::compute` does, so "parent"
4888 /// here means what `Ancestors::parent` means inside the guard
4889 /// rather than what a differently-built chain would say.
4890 #[cfg(feature = "perl")]
4891 fn perl_subsumed_operands(source: &str) -> (Vec<String>, HashSet<(u16, u16)>) {
4892 let wrappers: HashSet<u16> = PERL_NAME_WRAPPER_PAIRINGS
4893 .map(|(wrapper, _)| wrapper as u16)
4894 .into();
4895 let subsumed: HashSet<u16> = PERL_NAME_WRAPPER_PAIRINGS
4896 .map(|(_, child)| child as u16)
4897 .into();
4898 let tokens: HashSet<u16> = PERL_NAME_WRAPPER_TOKENS.map(|kind| kind as u16).into();
4899 let code = source.as_bytes();
4900 let mut hidden = Vec::new();
4901 let mut pairings = HashSet::new();
4902 for_each_node_with_chain::<PerlCode>(code, |node, chain| {
4903 let Some(parent) = chain.last() else { return };
4904 if !wrappers.contains(&parent.kind_id()) {
4905 return;
4906 }
4907 assert!(
4908 subsumed.contains(&node.kind_id()) || tokens.contains(&node.kind_id()),
4909 "`{source}`: a `{}` inside a `{}` is a child this guard was not \
4910 derived against; re-read node-types.json before trusting it",
4911 node.kind(),
4912 parent.kind(),
4913 );
4914 if subsumed.contains(&node.kind_id()) {
4915 pairings.insert((parent.kind_id(), node.kind_id()));
4916 hidden.push(
4917 node.utf8_text(code)
4918 .expect("fixture is valid UTF-8")
4919 .to_owned(),
4920 );
4921 }
4922 });
4923 (hidden, pairings)
4924 }
4925
4926 /// One row of `perl_name_wrappers_bill_the_name_once_1355`'s
4927 /// table: a fixture, what it must measure now, what it measured
4928 /// before #1355, and the operand text behind the counts.
4929 struct PerlNameWrapperCase {
4930 source: &'static str,
4931 /// `[n1, N1, n2, N2]` with the guard in place.
4932 counts: [u64; 4],
4933 /// `[n2, N2]` without it. Re-derived by the loop rather than
4934 /// trusted, so a stale row fails instead of misinforming.
4935 before: [u64; 2],
4936 operands: &'static [&'static str],
4937 }
4938
4939 /// Regression for #1355. `package_name`, `package_variable` and
4940 /// `typeglob` are operands spanning a whole name, so every
4941 /// operand-classified node *inside* one was billed a second time:
4942 /// `use strict;` scored `N2` 2 for one name, `our $Foo::count = 3;`
4943 /// n2 5 / N2 6 for two, and the vocabulary grew a bare `::` entry
4944 /// because a `package_variable`'s qualifier slot is itself a
4945 /// childless `package_name`.
4946 ///
4947 /// Each row's `before` column is what it measured without the
4948 /// guard, and the loop re-derives both halves from the current
4949 /// parse rather than trusting the column — the guard is the only
4950 /// difference between the two classifications, so the occurrences
4951 /// it removes are exactly the subsumed-kind children of a wrapper:
4952 ///
4953 /// - `N2` before minus `N2` after must equal how many of those
4954 /// there are. That identity *is* the defect: one spurious operand
4955 /// per contained name part.
4956 /// - `n2` before is the post-fix vocabulary unioned with their
4957 /// texts. It exceeds `n2` after wherever a part's spelling is not
4958 /// already an operand on its own (`Data`, `::`, `count`).
4959 ///
4960 /// The walk doubles as the grammar-dispatch §1 / §2 drift marker.
4961 /// It asserts that every *named* child of a wrapper is one of the
4962 /// eight subsumed kinds — which is what makes keying the arm on the
4963 /// parent alone safe — and that all ten pairings node-types.json
4964 /// admits are exercised here, so a bump that renumbers or re-parents
4965 /// one fails loudly instead of leaving the counts below measuring a
4966 /// construct the arm no longer reaches.
4967 ///
4968 /// Two mutants this does *not* catch, measured rather than assumed.
4969 /// Widening the guard from parent- to ancestor-scoped fails nothing,
4970 /// for the reason `perl_division_emits_no_slash_token` already
4971 /// records about the other guard in this getter: every operand-kinded
4972 /// descendant of a wrapper is also a direct child of one, and the
4973 /// intervening sigil tokens are matched by the operator arm above
4974 /// before the guard is reached. Parent-scoping stands on
4975 /// grammar-dispatch §5 and on symmetry with that guard, not on a
4976 /// test. What *is* pinned is the arm's position: moving it above the
4977 /// operator arm swallows `::`, `*` and the typeglob's opening brace,
4978 /// and the operator columns below fail.
4979 #[cfg(feature = "perl")]
4980 #[test]
4981 fn perl_name_wrappers_bill_the_name_once_1355() {
4982 let cases: [PerlNameWrapperCase; 12] = [
4983 // identifier under package_name, the single-segment form.
4984 PerlNameWrapperCase {
4985 source: "use strict;\n",
4986 counts: [2, 2, 1, 1],
4987 before: [1, 2],
4988 operands: &["strict"],
4989 },
4990 // …and the multi-segment one, twice over.
4991 PerlNameWrapperCase {
4992 source: "require Data::Dumper;\n",
4993 counts: [3, 3, 1, 1],
4994 before: [3, 3],
4995 operands: &["Data::Dumper"],
4996 },
4997 PerlNameWrapperCase {
4998 source: "package Foo::Bar;\n",
4999 counts: [3, 3, 1, 1],
5000 before: [3, 3],
5001 operands: &["Foo::Bar"],
5002 },
5003 // The `bar` of a qualified call is a *sibling* of the
5004 // `package_name`, not a child, so it still counts while the
5005 // `Foo` inside the wrapper does not. That is the row saying
5006 // the guard reads position and not kind: keying it on the
5007 // child kinds instead fails 14 tests here, this one among
5008 // them.
5009 PerlNameWrapperCase {
5010 source: "Foo::bar();\n",
5011 counts: [3, 3, 2, 2],
5012 before: [2, 3],
5013 operands: &["Foo", "bar"],
5014 },
5015 // identifier under typeglob, bare and brace-delimited.
5016 PerlNameWrapperCase {
5017 source: "my $g = *STDOUT;\n",
5018 counts: [5, 5, 2, 2],
5019 before: [3, 3],
5020 operands: &["$g", "*STDOUT"],
5021 },
5022 PerlNameWrapperCase {
5023 source: "my $t = *{Foo};\n",
5024 counts: [6, 6, 2, 2],
5025 before: [3, 3],
5026 operands: &["$t", "*{Foo}"],
5027 },
5028 // package_name and scalar_variable under package_variable,
5029 // and scalar_variable under package_name — the reported
5030 // fixture, where `$Foo` was billed twice and `::` once.
5031 PerlNameWrapperCase {
5032 source: "our $Foo::count = 3;\n",
5033 counts: [4, 4, 2, 2],
5034 before: [5, 6],
5035 operands: &["$Foo::count", "3"],
5036 },
5037 // array_variable / hash_variable / special_scalar_variable
5038 // under package_name: the same shape with the other sigils.
5039 PerlNameWrapperCase {
5040 source: "my @l = @Foo::list;\n",
5041 counts: [3, 3, 2, 2],
5042 before: [5, 6],
5043 operands: &["@l", "@Foo::list"],
5044 },
5045 PerlNameWrapperCase {
5046 source: "my %h = %Foo::hash;\n",
5047 counts: [3, 3, 2, 2],
5048 before: [5, 6],
5049 operands: &["%h", "%Foo::hash"],
5050 },
5051 PerlNameWrapperCase {
5052 source: "my $z = $_::x;\n",
5053 counts: [4, 5, 2, 2],
5054 before: [5, 6],
5055 operands: &["$z", "$_::x"],
5056 },
5057 // typeglob under package_name.
5058 PerlNameWrapperCase {
5059 source: "*Foo::glob = 1;\n",
5060 counts: [3, 3, 2, 2],
5061 before: [6, 7],
5062 operands: &["*Foo::glob", "1"],
5063 },
5064 // package_variable under package_name: the nesting that
5065 // makes qualifier depth unbounded. One variable reference
5066 // used to spell seven vocabulary entries.
5067 PerlNameWrapperCase {
5068 source: "my $x = $Foo::Bar::baz;\n",
5069 counts: [4, 5, 2, 2],
5070 before: [7, 10],
5071 operands: &["$x", "$Foo::Bar::baz"],
5072 },
5073 ];
5074
5075 let mut witnessed: HashSet<(u16, u16)> = HashSet::new();
5076 for PerlNameWrapperCase {
5077 source,
5078 counts,
5079 before: [n2_before, total_before],
5080 operands,
5081 } in cases
5082 {
5083 let (hidden, pairings) = perl_subsumed_operands(source);
5084 assert!(
5085 !hidden.is_empty(),
5086 "row {source:?} contains no name-wrapper child, so it witnesses nothing",
5087 );
5088 witnessed.extend(pairings);
5089
5090 // Phrased as an addition rather than a subtraction so a
5091 // future edit that inverts the two underflows nothing and
5092 // fails with the message below.
5093 assert_eq!(
5094 total_before,
5095 counts[3] + hidden.len() as u64,
5096 "row {source:?} must shed exactly one operand per contained \
5097 name part; recorded N2_before {total_before}, parts {hidden:?}",
5098 );
5099 let mut vocabulary: HashSet<&str> = operands.iter().copied().collect();
5100 vocabulary.extend(hidden.iter().map(String::as_str));
5101 assert_eq!(
5102 vocabulary.len() as u64,
5103 n2_before,
5104 "row {source:?}: n2 before the fix is the post-fix vocabulary \
5105 plus the contained name parts; got {vocabulary:?}",
5106 );
5107
5108 assert_halstead_counts::<PerlParser>(source, "foo.pl", counts, source);
5109 assert_ops_operands::<PerlParser>(source, "foo.pl", operands.len(), operands.to_vec());
5110 }
5111
5112 let mut got: Vec<(u16, u16)> = witnessed.into_iter().collect();
5113 got.sort_unstable();
5114 let mut expected_pairings: Vec<(u16, u16)> = PERL_NAME_WRAPPER_PAIRINGS
5115 .map(|(wrapper, child)| (wrapper as u16, child as u16))
5116 .into();
5117 expected_pairings.sort_unstable();
5118 assert_eq!(
5119 got, expected_pairings,
5120 "the table must exercise every (wrapper, child) pairing \
5121 node-types.json admits, and no other",
5122 );
5123 }
5124
5125 /// The over-suppression half of #1355 (grammar-dispatch §6 and §11).
5126 /// The guard is keyed on the parent, so the same kinds it silences
5127 /// inside a name wrapper have to keep counting everywhere else —
5128 /// otherwise "one operand per name" would have been bought by
5129 /// zeroing ordinary variables and calls.
5130 ///
5131 /// `module_name` rides along because the issue asserted it shares
5132 /// the `identifier` leaf and would be collateral damage. It does
5133 /// not: `use 'Foo.pm'` parses to a leaf holding only its two quote
5134 /// tokens, so it wraps nothing and is untouched either way.
5135 #[cfg(feature = "perl")]
5136 #[test]
5137 fn perl_qw_list_bills_one_operand_per_element() {
5138 // `qw(a b c)` was invisible to Halstead — neither the elements,
5139 // the wrapper nor the `qw` keyword had an arm — so it billed
5140 // nothing where its synonym `("a", "b", "c")` billed three
5141 // operands. Each `list_item` is now one operand and the
5142 // `word_list_qw` wrapper is gated on holding one, the #1353
5143 // Ruby `%w[]` rule: one operand per element, or one for the
5144 // empty literal.
5145 assert_ops_operands::<PerlParser>(
5146 "my @a = qw(a b c);\n",
5147 "foo.pl",
5148 4,
5149 vec!["@a", "a", "b", "c"],
5150 );
5151 assert_ops_operands::<PerlParser>("my @a = qw();\n", "foo.pl", 2, vec!["@a", "qw()"]);
5152 assert_ops_operands::<PerlParser>(
5153 "use POSIX qw(floor ceil);\n",
5154 "foo.pl",
5155 3,
5156 vec!["POSIX", "floor", "ceil"],
5157 );
5158 // expected: [n1, N1, n2, N2] = [3, 3, 4, 4] for every delimiter —
5159 // operators `my`, `=`, `;`; the `qw` keyword and its delimiters
5160 // are unclassified, as Ruby's `%w[` is, so the choice of
5161 // delimiter cannot move the score (#1312).
5162 for spelling in [
5163 "qw(a b c)",
5164 "qw/a b c/",
5165 "qw{a b c}",
5166 "qw[a b c]",
5167 "qw<a b c>",
5168 ] {
5169 assert_halstead_counts::<PerlParser>(
5170 &format!("my @a = {spelling};\n"),
5171 "foo.pl",
5172 [3, 3, 4, 4],
5173 spelling,
5174 );
5175 }
5176 // The synonym: the same four operands, plus the `()` and `,`
5177 // operators the list spelling carries.
5178 assert_halstead_counts::<PerlParser>(
5179 "my @a = (\"a\", \"b\", \"c\");\n",
5180 "foo.pl",
5181 [5, 6, 4, 4],
5182 "list literal",
5183 );
5184 }
5185
5186 #[cfg(feature = "perl")]
5187 #[test]
5188 fn perl_qualified_name_leaves_still_count_elsewhere_1355() {
5189 // expected: operators `my` × 4, `$` × 3 (one per `$`-sigilled
5190 // variable), `=` × 4, `;` × 5, `()` × 3 and the fat comma
5191 // → n1 = 6, N1 = 20. Operands are the four declared variables,
5192 // the three integers, the hash key, `$_` and the call target
5193 // → n2 = N2 = 10. Five of the eight kinds the guard silences
5194 // under a name wrapper appear among them — `scalar_variable`,
5195 // `array_variable`, `hash_variable`, `special_scalar_variable`
5196 // and `identifier` — and all five still count here.
5197 let bare = "my $x = 1;\nmy @a = (2);\nmy %h = (k => 3);\nmy $u = $_;\nfoo();\n";
5198 assert_halstead_counts::<PerlParser>(bare, "foo.pl", [6, 20, 10, 10], bare);
5199 assert_ops_operands::<PerlParser>(
5200 bare,
5201 "foo.pl",
5202 10,
5203 vec!["$x", "1", "@a", "2", "%h", "k", "3", "$u", "$_", "foo"],
5204 );
5205 let (hidden, _) = perl_subsumed_operands(bare);
5206 assert!(
5207 hidden.is_empty(),
5208 "no name wrapper appears here, so the guard must be inert; got {hidden:?}",
5209 );
5210
5211 // expected: operators `use`, `;` → n1 = N1 = 2; the quoted
5212 // module name is the sole operand → n2 = N2 = 1.
5213 let quoted = "use 'Some.pm';\n";
5214 assert_halstead_counts::<PerlParser>(quoted, "foo.pl", [2, 2, 1, 1], quoted);
5215 assert_ops_operands::<PerlParser>(quoted, "foo.pl", 1, vec!["'Some.pm'"]);
5216 assert!(
5217 ast_has_kind_id(
5218 &PerlParser::new(quoted.as_bytes().to_vec(), &PathBuf::from("foo.pl"), None),
5219 Perl::ModuleName as u16,
5220 ),
5221 "the quoted `use` form no longer parses to `module_name`, so this \
5222 row no longer says anything about it",
5223 );
5224 }
5225
5226 #[cfg(feature = "lua")]
5227 #[test]
5228 fn lua_operators_and_operands() {
5229 check_metrics::<LuaParser>(
5230 "local function add(a, b)
5231 local result = a + b
5232 if result > 0 then
5233 return result
5234 end
5235 return 0
5236end",
5237 "foo.lua",
5238 |metric| {
5239 // n1=11: local,function,(,,,=,+,if,>,then,return,end
5240 // (after #695 the `)` closer no longer counts — only the
5241 // folded `(` opener does; was n1=12).
5242 // n2=5: add,a,b,result,0
5243 insta::assert_json_snapshot!(metric.halstead, @r#"
5244 {
5245 "unique_operators": 11,
5246 "total_operators": 14,
5247 "unique_operands": 5,
5248 "total_operands": 10,
5249 "length": 24,
5250 "estimated_program_length": 49.66338827944708,
5251 "purity_ratio": 2.0693078449769615,
5252 "vocabulary": 16,
5253 "volume": 96.0,
5254 "difficulty": 11.0,
5255 "level": 0.09090909090909091,
5256 "effort": 1056.0,
5257 "time": 58.666666666666664,
5258 "bugs": 0.03456644293839657
5259 }
5260 "#);
5261 },
5262 );
5263 }
5264
5265 /// Regression for #695. Lua/Bash/Tcl/iRules/PHP/Ruby/Elixir used to
5266 /// classify the *closing* delimiter (`)`/`]`/`}`) as a separate
5267 /// operator, while the C-family majority folds each balanced pair to a
5268 /// single glyph via `get_operator_id_as_str` and counts only the
5269 /// opener. A balanced `(1)` therefore double-counted as `()` + `)`,
5270 /// inflating n1 and N1. With the fix only the folded `(` opener counts:
5271 /// `local x = (1)` yields operators `local`, `=`, `()` — n1 = N1 = 3,
5272 /// with no standalone `)`.
5273 #[cfg(feature = "lua")]
5274 #[test]
5275 fn lua_balanced_paren_counts_opener_only() {
5276 let source = "local x = (1)\n";
5277 let path = PathBuf::from("foo.lua");
5278 let parser = LuaParser::new(source.as_bytes().to_vec(), &path, None);
5279 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
5280 let paren = ops.operators.iter().filter(|o| o.as_str() == "()").count();
5281 assert_eq!(
5282 paren, 1,
5283 "balanced `(1)` must be one `()` operator; operators were {:?}",
5284 ops.operators
5285 );
5286 assert!(
5287 !ops.operators.iter().any(|o| o.as_str() == ")"),
5288 "the closing `)` must not be a separate operator; operators were {:?}",
5289 ops.operators
5290 );
5291 }
5292
5293 /// Guard for #768. Several `get_op_type` impls (Cpp/C/Objc/Mozcpp/
5294 /// Tcl/iRules/Php/Elixir/Ruby) classify a grammar's *second-alias*
5295 /// opener — `LPAREN2`, and for Elixir/Ruby `LBRACK2`/`LBRACK3` — as a
5296 /// Halstead operator alongside the base `LPAREN`/`LBRACK`. #768 worried
5297 /// that an alias opener would reach `compute_halstead` with a kind_id
5298 /// distinct from the base, inflating n1 (a second `()` entry) and
5299 /// rendering a bare `"("` instead of the folded `"()"`.
5300 ///
5301 /// That cannot happen: tree-sitter's runtime collapses each alias to
5302 /// its base via the grammar's `public_symbol_map` *before*
5303 /// `Node::kind_id()` (`ts_node_symbol`) ever returns. So the alias
5304 /// kind_id is unobservable to the metric layer and the alias match arms
5305 /// are defensive — they only fire if a future grammar bump drops that
5306 /// collapse. This test pins the invariant: parsing the exact
5307 /// constructs each grammar produces the alias for internally
5308 /// (pp-conditional `defined(...)` for Cpp; call arg-list / subscript /
5309 /// constant-array-pattern for Ruby) must yield **no** node carrying the
5310 /// alias kind_id, and the balanced opener must count once and render as
5311 /// the pair glyph. If a grammar bump makes an alias id observable, this
5312 /// goes red and signals that the alias arms must additionally fold to
5313 /// the base in `get_operator_id_as_str` (the fix #768 proposed).
5314 #[cfg(all(feature = "c", feature = "cpp", feature = "elixir", feature = "ruby"))]
5315 #[test]
5316 fn second_alias_opener_collapses_to_base_kind_id() {
5317 #[cfg(any(feature = "c", feature = "cpp", feature = "elixir", feature = "ruby"))]
5318 fn assert_no_alias<T: crate::ParserTrait>(
5319 source: &str,
5320 file: &str,
5321 alias_id: u16,
5322 alias_name: &str,
5323 ) {
5324 let path = PathBuf::from(file);
5325 let parser = T::new(source.as_bytes().to_vec(), &path, None);
5326 let mut stack = vec![parser.root()];
5327 while let Some(node) = stack.pop() {
5328 assert_ne!(
5329 node.kind_id(),
5330 alias_id,
5331 "{alias_name} (kind_id {alias_id}) must never reach kind_id() \
5332 for `{source}`; the runtime public_symbol_map should have \
5333 collapsed it to the base opener. If this fires after a \
5334 grammar bump, fold {alias_name} to its pair glyph in \
5335 get_operator_id_as_str (issue #768)."
5336 );
5337 for child in node.children() {
5338 stack.push(child);
5339 }
5340 }
5341 }
5342
5343 // Balanced openers must count once and render folded (no bare
5344 // `(`/`[`, no n1 inflation) — the property #768 feared was broken.
5345 #[cfg(any(feature = "c", feature = "cpp", feature = "elixir", feature = "ruby"))]
5346 fn assert_folded_openers<T: crate::MetricSuite>(source: &str, file: &str) {
5347 let path = PathBuf::from(file);
5348 let parser = T::new(source.as_bytes().to_vec(), &path, None);
5349 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
5350 assert!(
5351 !ops.operators.iter().any(|o| o.as_str() == "("),
5352 "no bare `(` operator (must fold to `()`); operators were {:?}",
5353 ops.operators
5354 );
5355 assert!(
5356 !ops.operators.iter().any(|o| o.as_str() == "["),
5357 "no bare `[` operator (must fold to `[]`); operators were {:?}",
5358 ops.operators
5359 );
5360 // Each pair glyph appears at most once — the alias does not add
5361 // a second `()`/`[]` entry to n1.
5362 assert!(
5363 ops.operators.iter().filter(|o| o.as_str() == "()").count() <= 1,
5364 "`()` must be a single n1 entry; operators were {:?}",
5365 ops.operators
5366 );
5367 assert!(
5368 ops.operators.iter().filter(|o| o.as_str() == "[]").count() <= 1,
5369 "`[]` must be a single n1 entry; operators were {:?}",
5370 ops.operators
5371 );
5372 }
5373
5374 // Cpp/C/Mozcpp: LPAREN2 = 20. The grammar emits it internally only
5375 // inside preprocessor-conditional expressions (`#if defined(FOO)`).
5376 assert_no_alias::<crate::CppParser>(
5377 "#if defined(FOO)\n#endif\n",
5378 "a.cpp",
5379 20,
5380 "Cpp::LPAREN2",
5381 );
5382 assert_no_alias::<crate::CParser>("#if defined(FOO)\n#endif\n", "a.c", 20, "C::LPAREN2");
5383
5384 // Ruby: LPAREN2 = 47 (call arg-list), LBRACK3 = 155 (element-
5385 // reference subscript), LBRACK2 = 46 (constant array pattern).
5386 assert_no_alias::<crate::RubyParser>("f(1)\n", "a.rb", 47, "Ruby::LPAREN2");
5387 assert_no_alias::<crate::RubyParser>("a[0]\n", "a.rb", 155, "Ruby::LBRACK3");
5388 assert_no_alias::<crate::RubyParser>(
5389 "case p\nin Point[1, 2] then 1\nend\n",
5390 "a.rb",
5391 46,
5392 "Ruby::LBRACK2",
5393 );
5394
5395 // Elixir: LPAREN2 = 95 (immediate call paren), LBRACK2 = 96
5396 // (access / subscript).
5397 assert_no_alias::<crate::ElixirParser>("f(1)\n", "a.ex", 95, "Elixir::LPAREN2");
5398 assert_no_alias::<crate::ElixirParser>("x[0]\n", "a.ex", 96, "Elixir::LBRACK2");
5399
5400 assert_folded_openers::<crate::CppParser>("int main(){ int a[3]; return a[0]; }", "b.cpp");
5401 assert_folded_openers::<crate::RubyParser>("f(1)\nb = [1]\nb[0]\n", "b.rb");
5402 }
5403
5404 #[cfg(feature = "kotlin")]
5405 #[test]
5406 fn kotlin_halstead_basic() {
5407 check_metrics::<KotlinParser>(
5408 "fun add(a: Int, b: Int): Int {
5409 val result = a + b
5410 return result
5411 }",
5412 "foo.kt",
5413 |metric| {
5414 insta::assert_json_snapshot!(
5415 metric.halstead,
5416 @r#"
5417 {
5418 "unique_operators": 9,
5419 "total_operators": 11,
5420 "unique_operands": 5,
5421 "total_operands": 10,
5422 "length": 21,
5423 "estimated_program_length": 40.13896548741762,
5424 "purity_ratio": 1.9113793089246487,
5425 "vocabulary": 14,
5426 "volume": 79.9544533632097,
5427 "difficulty": 9.0,
5428 "level": 0.1111111111111111,
5429 "effort": 719.5900802688873,
5430 "time": 39.97722668160485,
5431 "bugs": 0.026767153565498338
5432 }
5433 "#
5434 );
5435 },
5436 );
5437 }
5438
5439 #[cfg(feature = "kotlin")]
5440 #[test]
5441 fn kotlin_string_template_no_double_count() {
5442 // Re-anchored for issue #454. The pre-#454 comment claimed
5443 // kotlin-ng emits an `identifier` node for the short `$name`
5444 // form whose bytes include the leading `$`. That is factually
5445 // false: AST dump shows the short form produces bare
5446 // `string_content` tokens (`$`, then `name`) with **no**
5447 // structured node. The old assertion (u_operands = 4, N2 = 5)
5448 // passed for the wrong reason (lesson 6): the wrapping literal
5449 // was counted (+1) and the inner `name` was dropped (-1), and
5450 // the two errors cancelled. The `$name!` it used also defeats
5451 // recovery because the grammar glues the trailing `!` onto the
5452 // name token.
5453 //
5454 // Correct mechanism (clean end-of-segment short form):
5455 // `fun greet(name: String): String {\n return "Hi $name"\n}\n`
5456 // operators: fun, (, ), :, {}, return → as classified.
5457 // operands by token text:
5458 // `greet` × 1, `name` × 2 (param + recovered short-interp),
5459 // `String` × 2 (param type + return type).
5460 // The wrapping `"Hi $name"` literal is suppressed and the
5461 // inner `name` recovered → u_operands = 3 (`greet`, `name`,
5462 // `String`), N2 = 5. Pre-#454: wrapper counted, inner dropped
5463 // → u_operands = 4, N2 = 6.
5464 check_metrics::<KotlinParser>(
5465 "fun greet(name: String): String {\n return \"Hi $name\"\n}\n",
5466 "foo.kt",
5467 |metric| {
5468 assert_eq!(metric.halstead.unique_operands(), 3);
5469 assert_eq!(metric.halstead.total_operands(), 5);
5470 },
5471 );
5472 // Lesson 4: the ops store agrees on n2 and the exact operand set
5473 // (inner `name` present, wrapper absent).
5474 assert_ops_operands::<KotlinParser>(
5475 "fun greet(name: String): String {\n return \"Hi $name\"\n}\n",
5476 "foo.kt",
5477 3,
5478 vec!["greet", "name", "String"],
5479 );
5480 }
5481
5482 #[cfg(feature = "kotlin")]
5483 #[test]
5484 fn kotlin_short_interpolation_counts_inner_not_wrapper() {
5485 // Issue #454: the short `$name` template — distinct from the
5486 // long `${expr}` form, which the kotlin-ng grammar gives a
5487 // structured `interpolation` node (see
5488 // `kotlin_string_template_long_form_no_double_count`). The short
5489 // form has no such node; the variable arrives as a bare
5490 // `string_content` token preceded by a `$` `string_content`.
5491 // The fix recovers the clean-identifier variable as an operand
5492 // and suppresses the opaque wrapper.
5493 //
5494 // `fun f() { val x = 1; println("v=$x") }\n`
5495 // operands by token text: `f`, `x` × 2 (decl + recovered),
5496 // `println`, `1`. The wrapping `"v=$x"` is suppressed →
5497 // u_operands = 4 (`f`, `x`, `println`, `1`), N2 = 5.
5498 // Pre-#454 the wrapper `"v=$x"` counted and the inner `x` was
5499 // dropped → u_operands = 4 but the wrapper, not `x`, was the
5500 // fourth operand, and N2 = 5 with the wrong member — the ops
5501 // assertion below pins the exact set so the cancellation cannot
5502 // hide it.
5503 let src = "fun f() { val x = 1; println(\"v=$x\") }\n";
5504 check_metrics::<KotlinParser>(src, "foo.kt", |metric| {
5505 assert_eq!(metric.halstead.unique_operands(), 4);
5506 assert_eq!(metric.halstead.total_operands(), 5);
5507 });
5508 assert_ops_operands::<KotlinParser>(src, "foo.kt", 4, vec!["f", "x", "println", "1"]);
5509 }
5510
5511 #[cfg(feature = "kotlin")]
5512 #[test]
5513 fn kotlin_short_interpolation_space_separated() {
5514 // Issue #454 follow-up: tree-sitter-kotlin-ng splits the literal
5515 // only at each `$`, so a `$name` segment's name token absorbs any
5516 // trailing inter-segment text into its byte range. For `"$a $b"`
5517 // the token after the first `$` is `"a "` (with the trailing
5518 // space). Pre-fix `kotlin_is_identifier("a ")` returned false and
5519 // the leading variable `a` was silently dropped, yielding
5520 // operands `{b, f, s}` (verified: `a` missing) — breaking parity
5521 // with the long form `"${a} ${b}"`, which recovers `{a, b, f, s}`.
5522 //
5523 // The fix takes the maximal leading-identifier prefix of the name
5524 // token, recovering `a` and keying it as the bare `"a"` (not
5525 // `"a "`). Short and long forms must now agree exactly.
5526 //
5527 // `fun f() { val s = "$a $b" }\n`
5528 // operands by token text: `f`, `s`, `a` (recovered), `b`
5529 // (recovered). Wrapper suppressed → u_operands = 4, N2 = 4.
5530 let short = "fun f() { val s = \"$a $b\" }\n";
5531 let long = "fun f() { val s = \"${a} ${b}\" }\n";
5532 check_metrics::<KotlinParser>(short, "foo.kt", |metric| {
5533 assert_eq!(metric.halstead.unique_operands(), 4);
5534 assert_eq!(metric.halstead.total_operands(), 4);
5535 });
5536 // Both `a` and `b` present, wrapper absent, n2 == dedupe(operands).
5537 assert_ops_operands::<KotlinParser>(short, "foo.kt", 4, vec!["f", "s", "a", "b"]);
5538 // Exact parity with the long `${a} ${b}` form.
5539 assert_ops_operands::<KotlinParser>(long, "foo.kt", 4, vec!["f", "s", "a", "b"]);
5540
5541 // Comma after the name (`"$a, $b"`): the first name token is
5542 // `"a, "`; its leading identifier prefix is `a`.
5543 let comma = "fun f() { val s = \"$a, $b\" }\n";
5544 assert_ops_operands::<KotlinParser>(comma, "foo.kt", 4, vec!["f", "s", "a", "b"]);
5545
5546 // Name preceded by literal text and at end-of-segment (`"x=$a"`):
5547 // the `a` token has no trailing text, so recovery is unchanged.
5548 let prefixed = "fun f() { val s = \"x=$a\" }\n";
5549 assert_ops_operands::<KotlinParser>(prefixed, "foo.kt", 3, vec!["f", "s", "a"]);
5550
5551 // Mid-prose `"$x is "`: the name token is `"x is "`. The leading
5552 // identifier prefix is `x`, matching the long form `"${x} is "`,
5553 // which also recovers `x` and treats `" is "` as literal text.
5554 let prose_short = "fun f() { val s = \"$x is \" }\n";
5555 let prose_long = "fun f() { val s = \"${x} is \" }\n";
5556 assert_ops_operands::<KotlinParser>(prose_short, "foo.kt", 3, vec!["f", "s", "x"]);
5557 assert_ops_operands::<KotlinParser>(prose_long, "foo.kt", 3, vec!["f", "s", "x"]);
5558 }
5559
5560 #[cfg(feature = "kotlin")]
5561 #[test]
5562 fn kotlin_dollar_non_identifier_stays_literal() {
5563 // Issue #454 boundary: a `$` not followed by a clean identifier
5564 // is literal text, not an interpolation. `"price: $5"` (digit
5565 // after `$`) must keep the wrapping literal as a single operand
5566 // and recover nothing.
5567 //
5568 // `fun f() { val a = "price: $5" }\n`
5569 // operands: `f`, `a`, `"price: $5"` → u_operands = 3, N2 = 3.
5570 let src = "fun f() { val a = \"price: $5\" }\n";
5571 check_metrics::<KotlinParser>(src, "foo.kt", |metric| {
5572 assert_eq!(metric.halstead.unique_operands(), 3);
5573 assert_eq!(metric.halstead.total_operands(), 3);
5574 });
5575 assert_ops_operands::<KotlinParser>(src, "foo.kt", 3, vec!["f", "a", "\"price: $5\""]);
5576 }
5577
5578 #[cfg(feature = "kotlin")]
5579 #[test]
5580 fn kotlin_string_template_long_form_no_double_count() {
5581 // The `${expr}` long form of a Kotlin string template also
5582 // produces an `Interpolation` child. The fix must apply to it
5583 // identically.
5584 //
5585 // Source: `fun f(x: Int): String { return "v=${x}" }\n`
5586 // Operands by source-byte key:
5587 // `f` × 1, `x` × 2 (param + inside `${x}`),
5588 // `Int` × 1, `String` × 1.
5589 // With the fix u_operands = 4 (`f`, `x`, `Int`, `String`),
5590 // N2 = 5. Without the fix the wrapping `"v=${x}"` would also
5591 // count → u_operands = 5, N2 = 6.
5592 check_metrics::<KotlinParser>(
5593 "fun f(x: Int): String { return \"v=${x}\" }\n",
5594 "foo.kt",
5595 |metric| {
5596 assert_eq!(metric.halstead.unique_operands(), 4);
5597 assert_eq!(metric.halstead.total_operands(), 5);
5598 },
5599 );
5600 }
5601
5602 #[cfg(feature = "kotlin")]
5603 #[test]
5604 fn kotlin_plain_string_still_operand() {
5605 // The fix for #191 only skips wrapping templates that contain
5606 // an `Interpolation` child; a plain `"hello"` (no `$` interp)
5607 // must still contribute exactly one operand.
5608 //
5609 // Source: `fun f(): String { return "hello" }\n`
5610 // Operands: `f` × 1, `String` × 1, `"hello"` × 1 →
5611 // u_operands = 3, N2 = 3.
5612 check_metrics::<KotlinParser>(
5613 "fun f(): String { return \"hello\" }\n",
5614 "foo.kt",
5615 |metric| {
5616 assert_eq!(metric.halstead.unique_operands(), 3);
5617 assert_eq!(metric.halstead.total_operands(), 3);
5618 },
5619 );
5620 }
5621
5622 // Kotlin half of #1380 — see
5623 // `java_self_and_super_references_are_operands` for the structural
5624 // argument.
5625 #[cfg(feature = "kotlin")]
5626 #[test]
5627 fn kotlin_self_and_super_references_are_operands() {
5628 let source = "class T {\n fun f() = this.x + super.y\n}";
5629 // Operators (n1=7, N1=8): class, {}, fun, (), =, . x2, +
5630 // Operands (n2=6, N2=6): T, f, this, x, super, y
5631 // Before the fix this read (9, 10, 4, 4).
5632 assert_halstead_counts::<KotlinParser>(
5633 source,
5634 "foo.kt",
5635 [7, 8, 6, 6],
5636 "kotlin self/super references",
5637 );
5638 assert_keywords_are_operands_only::<KotlinParser>(source, "foo.kt", &["this", "super"]);
5639 }
5640
5641 // The label-qualified spellings are their own leaf kinds — `this@`
5642 // (106) and `super@` (107) — carrying no `this` / `super` leaf, so
5643 // listing only the two bare kinds would score every `this@Outer`
5644 // reference zero (grammar-dispatch section 1). The labels `Outer`
5645 // and `Inner` stay separate operands: the `this_expression` wrapper
5646 // whose span would have swallowed them is deliberately unclassified
5647 // (section 5).
5648 #[cfg(feature = "kotlin")]
5649 #[test]
5650 fn kotlin_labelled_self_and_super_references_are_operands() {
5651 let source = "class Outer {\n inner class Inner : A() {\n \
5652 fun f() = this@Outer.x + super@Inner.y\n }\n}";
5653 assert_keywords_are_operands_only::<KotlinParser>(source, "foo.kt", &["this@", "super@"]);
5654 assert_ops_operands::<KotlinParser>(
5655 source,
5656 "foo.kt",
5657 8,
5658 vec!["A", "Inner", "Outer", "f", "super@", "this@", "x", "y"],
5659 );
5660 // The metrics store is a second, independent walk — it keys
5661 // operands by `get_operand_id` where `ops_inner` keys by text —
5662 // so `assert_ops_operands` on its own pins the ops store against
5663 // a literal rather than against the metric (lesson 4).
5664 //
5665 // `N2` is what makes the "the labels stay separate operands"
5666 // claim above testable. Both labels duplicate an enclosing class
5667 // name, so dropping their contribution entirely leaves `n2` at 8
5668 // with the same eight strings; only `N2` falls, 10 to 8.
5669 check_metrics::<KotlinParser>(source, "foo.kt", |metric| {
5670 assert_eq!(metric.halstead.unique_operands(), 8);
5671 assert_eq!(metric.halstead.total_operands(), 10);
5672 });
5673 }
5674
5675 // The keeper question of grammar-dispatch section 6: a
5676 // `constructor_delegation_call` emits a bare `this` / `super` leaf
5677 // with no `this_expression` / `super_expression` wrapper around it,
5678 // so classifying the wrapper instead would score constructor
5679 // delegation zero. This is the only spelling that can tell the two
5680 // choices apart — every other `this` carries both nodes — so it is
5681 // the independent path grammar-dispatch section 11 asks for.
5682 #[cfg(feature = "kotlin")]
5683 #[test]
5684 fn kotlin_constructor_delegation_self_reference_is_an_operand() {
5685 let source = "class C(val n: Int) {\n constructor() : this(0)\n}";
5686 assert_keywords_are_operands_only::<KotlinParser>(source, "foo.kt", &["this"]);
5687 assert_ops_operands::<KotlinParser>(
5688 source,
5689 "foo.kt",
5690 5,
5691 vec!["0", "C", "Int", "n", "this"],
5692 );
5693 check_metrics::<KotlinParser>(source, "foo.kt", |metric| {
5694 assert_eq!(metric.halstead.unique_operands(), 5);
5695 assert_eq!(metric.halstead.total_operands(), 5);
5696 });
5697 }
5698
5699 /// One `this` / `super` per container the pinned
5700 /// `tree-sitter-kotlin-ng` can put them in: a bare expression, a
5701 /// call argument, a navigation receiver, a type-argument-qualified
5702 /// `super<P>`, both label-qualified forms, and the
5703 /// `constructor_delegation_call` that carries no wrapper.
5704 #[cfg(feature = "kotlin")]
5705 const KOTLIN_SELF_POSITIONS: &str = "class P {
5706 fun h() = 1
5707 }
5708 class Outer : P() {
5709 val x = 1
5710 fun c() = super.h()
5711 fun d() = this
5712 fun e(o: Any): Any = e(this)
5713 inner class Inner : P() {
5714 fun a() = this@Outer.x
5715 fun b() = this.hashCode()
5716 fun f() = super@Inner.h()
5717 fun g() = super<P>.h()
5718 }
5719 }
5720 class Del(val n: Int) {
5721 constructor() : this(0)
5722 }
5723";
5724
5725 #[cfg(feature = "kotlin")]
5726 #[test]
5727 fn kotlin_self_and_super_leaves_are_unaliased_and_unconditional() {
5728 assert_self_reference_leaves::<KotlinCode>(
5729 KOTLIN_SELF_POSITIONS,
5730 &[
5731 ("this", Kotlin::This as u16, 4),
5732 ("super", Kotlin::Super as u16, 2),
5733 ("this@", Kotlin::ThisAT as u16, 1),
5734 ("super@", Kotlin::SuperAT as u16, 1),
5735 ],
5736 "kotlin",
5737 );
5738 }
5739
5740 #[cfg(feature = "python")]
5741 #[test]
5742 fn python_fstring_no_double_count() {
5743 // Regression: issue #191. A Python f-string (`f"Hi {name}!"`)
5744 // wraps an `Interpolation` child whose inner identifier
5745 // `name` is walked and counted as its own operand. Without
5746 // the `is_child(Interpolation)` guard the wrapping `String`
5747 // would also count, double-counting `name`'s contribution to
5748 // `N2`. Same pattern as #180 (Bash/Elixir) and #184 (PHP).
5749 //
5750 // Source: `def greet(name):\n return f"Hi {name}!"\n`
5751 // Operands by source-byte key:
5752 // `greet` × 1, `name` × 2 (param + inside `{name}`).
5753 // With the fix the wrapping `f"Hi {name}!"` is skipped →
5754 // u_operands = 2 (`greet`, `name`), N2 = 3. Without the fix
5755 // the wrapping literal would also count → u_operands = 3,
5756 // N2 = 4.
5757 check_metrics::<PythonParser>(
5758 "def greet(name):\n return f\"Hi {name}!\"\n",
5759 "foo.py",
5760 |metric| {
5761 assert_eq!(metric.halstead.unique_operands(), 2);
5762 assert_eq!(metric.halstead.total_operands(), 3);
5763 },
5764 );
5765 }
5766
5767 #[cfg(feature = "python")]
5768 #[test]
5769 fn python_plain_string_still_operand() {
5770 // The fix for #191 only skips wrapping `String` nodes that
5771 // contain an `Interpolation` child; a plain `"hi"` must still
5772 // contribute exactly one operand.
5773 //
5774 // Source: `def f():\n return "hi"\n`
5775 // Operands: `f` × 1, `"hi"` × 1 → u_operands = 2, N2 = 2.
5776 // (The previous documentation-string filter is preserved:
5777 // a bare `"hi"` as a top-level `expression_statement` would
5778 // be skipped, but here it appears as `return "hi"`.)
5779 check_metrics::<PythonParser>("def f():\n return \"hi\"\n", "foo.py", |metric| {
5780 assert_eq!(metric.halstead.unique_operands(), 2);
5781 assert_eq!(metric.halstead.total_operands(), 2);
5782 });
5783 }
5784
5785 #[cfg(feature = "python")]
5786 #[test]
5787 fn python_concatenated_docstring_suppressed() {
5788 // Regression for #695. An implicit-concatenation docstring
5789 // (`"""doc""" "more"`) parses as `expression_statement >
5790 // concatenated_string > [string, string]`. The single-literal
5791 // docstring guard (`parent == expression_statement &&
5792 // child_count == 1`) never fired here, so each fragment counted
5793 // as a separate operand and the docstring's N2 contribution
5794 // depended on how many literals it was split into. With the fix,
5795 // every fragment of such a docstring is suppressed.
5796 //
5797 // Source: `def f():\n """doc""" "more"\n return 1\n`
5798 // Operands: `f`, `1` only — both docstring fragments suppressed →
5799 // u_operands = 2, N2 = 2.
5800 check_metrics::<PythonParser>(
5801 "def f():\n \"\"\"doc\"\"\" \"more\"\n return 1\n",
5802 "foo.py",
5803 |metric| {
5804 assert_eq!(metric.halstead.unique_operands(), 2);
5805 assert_eq!(metric.halstead.total_operands(), 2);
5806 },
5807 );
5808 }
5809
5810 #[cfg(feature = "python")]
5811 #[test]
5812 fn python_concatenated_non_docstring_still_counts() {
5813 // The #695 fix must only suppress concatenated literals in the
5814 // *docstring* position (sole child of an `expression_statement`).
5815 // A concatenated string used as a value (`x = "a" "b"`) is not a
5816 // docstring — its `concatenated_string` parent's grandparent is
5817 // an assignment, not a single-child statement — so both fragments
5818 // must still be operands.
5819 //
5820 // Source: `def f():\n x = "a" "b"\n return x\n`
5821 // Operands: `f`, `x` (twice: assign + return), `"a"`, `"b"` →
5822 // u_operands = 4, N2 = 5.
5823 check_metrics::<PythonParser>(
5824 "def f():\n x = \"a\" \"b\"\n return x\n",
5825 "foo.py",
5826 |metric| {
5827 assert_eq!(metric.halstead.unique_operands(), 4);
5828 assert_eq!(metric.halstead.total_operands(), 5);
5829 },
5830 );
5831 }
5832
5833 #[cfg(feature = "python")]
5834 #[test]
5835 fn python_empty_file_halstead() {
5836 check_metrics::<PythonParser>("", "empty.py", |metric| {
5837 let h = &metric.halstead;
5838 assert_eq!(h.unique_operators(), 0);
5839 assert_eq!(h.total_operands(), 0);
5840 assert_eq!(h.estimated_program_length(), 0.0);
5841 assert_eq!(h.purity_ratio(), 0.0);
5842 assert_eq!(h.volume(), 0.0);
5843 assert_eq!(h.difficulty(), 0.0);
5844 assert_eq!(h.level(), 0.0);
5845 assert_eq!(h.effort(), 0.0);
5846 assert_eq!(h.time(), 0.0);
5847 assert_eq!(h.bugs(), 0.0);
5848 });
5849 }
5850
5851 /// Regression #413, sub-fix (1): `await` was double-counted because the
5852 /// operator arm listed both the await-expression node (Await=237) and the
5853 /// nested `await` keyword token (Await2=95). Only the node should count,
5854 /// mirroring how `yield` counts only the Yield node.
5855 #[cfg(feature = "python")]
5856 #[test]
5857 fn python_await_counted_once_per_use() {
5858 check_metrics::<PythonParser>(
5859 "async def f():\n await a()\n await b()\n await c()\n",
5860 "foo.py",
5861 |metric| {
5862 // expected operators: async, def, (), :, await (5 unique)
5863 // N1: async(1) def(1) await(3) = 5, plus the four call
5864 // and parameter-list `(` and the block `:` (#1486) = 10.
5865 // Before #413, Await + Await2 both matched, so `await` was a
5866 // distinct operator twice: one more unique and three more total.
5867 assert_eq!(metric.halstead.unique_operators(), 5);
5868 assert_eq!(metric.halstead.total_operators(), 10);
5869 },
5870 );
5871 }
5872
5873 /// Regression #413, sub-fix (3): `lambda` was dropped entirely. Only the
5874 /// `lambda` keyword token (Lambda3=73) is classified, not the wrapping
5875 /// Lambda/Lambda2 expression nodes, to avoid an await-style double count.
5876 #[cfg(feature = "python")]
5877 #[test]
5878 fn python_lambda_counted_once() {
5879 check_metrics::<PythonParser>("g = lambda x: x + 1\n", "foo.py", |metric| {
5880 // expected operators: =, lambda, :, + (4 unique, each used
5881 // once; the parameter-list `:` since #1486).
5882 // Before #413, lambda was absent: only =, + were counted.
5883 assert_eq!(metric.halstead.unique_operators(), 4);
5884 assert_eq!(metric.halstead.total_operators(), 4);
5885 });
5886 }
5887
5888 /// Regression #413, sub-fix (2): `match` / `case` keyword tokens
5889 /// (Match=26, Case=27) were dropped. Each should now count as an operator,
5890 /// matching the cyclomatic metric which already counts every `case`.
5891 #[cfg(feature = "python")]
5892 #[test]
5893 fn python_match_case_counted() {
5894 check_metrics::<PythonParser>(
5895 "match x:\n case 1:\n pass\n case _:\n pass\n",
5896 "foo.py",
5897 |metric| {
5898 // expected operators: match, case, pass, : (4 unique)
5899 // match(1) + case(2) + pass(2) + one block `:` per
5900 // header (3, #1486) = 8 total occurrences.
5901 // Before #413, neither match nor case was counted (only pass).
5902 assert_eq!(metric.halstead.unique_operators(), 4);
5903 assert_eq!(metric.halstead.total_operators(), 8);
5904 },
5905 );
5906 }
5907
5908 /// Regression #413, sub-fix (2): `nonlocal` (Nonlocal=41) was dropped while
5909 /// `global` was already classified. Both should count, for parity.
5910 #[cfg(feature = "python")]
5911 #[test]
5912 fn python_nonlocal_and_global_counted() {
5913 check_metrics::<PythonParser>(
5914 "def f():\n global a\n nonlocal b\n",
5915 "foo.py",
5916 |metric| {
5917 // expected operators: def, (), :, global, nonlocal
5918 // (5 unique, each once; `()` and `:` since #1486)
5919 // Before #413, nonlocal was absent: only def, global counted.
5920 assert_eq!(metric.halstead.unique_operators(), 5);
5921 assert_eq!(metric.halstead.total_operators(), 5);
5922 },
5923 );
5924 }
5925
5926 /// Regression #413, sub-fix (4): `not in` (Notin=193) and `is not`
5927 /// (Isnot=194) are single compound operators. The parent-guard suppresses
5928 /// the inner Not/In/Is leaves only under those compounds, so standalone
5929 /// `not x`, `a in b`, `a is b`, and `for x in y` still count their leaves.
5930 #[cfg(feature = "python")]
5931 #[test]
5932 fn python_not_in_is_not_counted_as_single_operator() {
5933 check_metrics::<PythonParser>(
5934 "a not in b\na is not b\nnot c\nd in e\nf is g\nfor h in i:\n pass\n",
5935 "foo.py",
5936 |metric| {
5937 // expected operators (8 unique):
5938 // "not in" (compound, once), "is not" (compound, once),
5939 // "not" (standalone `not c`, once),
5940 // "in" (standalone `d in e` + `for h in i` = twice),
5941 // "is" (standalone `f is g`, once),
5942 // "for" (once), "pass" (once), and the `for` header's
5943 // block ":" (once, #1486)
5944 // Total occurrences: 1+1+1+2+1+1+1+1 = 9.
5945 // Before #413, `a not in b` counted not+in (two) and
5946 // `a is not b` counted is+not (two); the compounds were
5947 // never classified.
5948 assert_eq!(metric.halstead.unique_operators(), 8);
5949 assert_eq!(metric.halstead.total_operators(), 9);
5950 },
5951 );
5952 }
5953
5954 /// Asserts `count(halstead)` for each `(label, source, expected)`
5955 /// row, one parse per row so a row's counts are its own.
5956 #[cfg(any(
5957 feature = "c",
5958 feature = "cpp",
5959 feature = "groovy",
5960 feature = "java",
5961 feature = "javascript",
5962 feature = "kotlin",
5963 feature = "mozcpp",
5964 feature = "mozjs",
5965 feature = "objc",
5966 feature = "python",
5967 feature = "rust",
5968 feature = "typescript"
5969 ))]
5970 fn assert_halstead_rows<T: crate::MetricSuite>(
5971 file: &str,
5972 rows: &[(&str, &str, [u64; 2])],
5973 count: fn(&Stats) -> [u64; 2],
5974 ) {
5975 for (label, source, expected) in rows {
5976 crate::test_support::check_func_space_only::<T, _>(
5977 source,
5978 file,
5979 &[crate::Metric::Halstead],
5980 |space| {
5981 assert_eq!(
5982 count(&space.metrics.halstead),
5983 *expected,
5984 "{label}: {source:?}"
5985 );
5986 },
5987 );
5988 }
5989 }
5990
5991 /// Asserts `[n1, N1]` for each row.
5992 #[cfg(any(
5993 feature = "c",
5994 feature = "cpp",
5995 feature = "groovy",
5996 feature = "java",
5997 feature = "javascript",
5998 feature = "kotlin",
5999 feature = "mozcpp",
6000 feature = "mozjs",
6001 feature = "objc",
6002 feature = "python",
6003 feature = "rust",
6004 feature = "typescript"
6005 ))]
6006 fn assert_operator_rows<T: crate::MetricSuite>(file: &str, rows: &[(&str, &str, [u64; 2])]) {
6007 assert_halstead_rows::<T>(file, rows, |h| [h.unique_operators(), h.total_operators()]);
6008 }
6009
6010 /// Asserts `[n2, N2]` for each row.
6011 #[cfg(any(feature = "java", feature = "typescript"))]
6012 fn assert_operand_rows<T: crate::MetricSuite>(file: &str, rows: &[(&str, &str, [u64; 2])]) {
6013 assert_halstead_rows::<T>(file, rows, |h| [h.unique_operands(), h.total_operands()]);
6014 }
6015
6016 /// #1486: Python's bracket openers, `;` and `:` are vocabulary, as
6017 /// they are in every other getter (#1395). Each row isolates one
6018 /// position, so the expected `[n1, N1]` is derivable from the row
6019 /// alone; a bracket pair bills its opener once, as `()` / `[]` / `{}`.
6020 #[cfg(feature = "python")]
6021 #[test]
6022 fn python_punctuation_is_vocabulary_1486() {
6023 const ROWS: &[(&str, &str, [u64; 2])] = &[
6024 // `()`
6025 ("call", "f(x)\n", [1, 1]),
6026 // `()`, `=`
6027 ("keyword argument", "f(a=1)\n", [2, 2]),
6028 // `()`, `*`, `,`, `**`
6029 ("splat arguments", "f(*a, **k)\n", [4, 4]),
6030 // `[]`
6031 ("index", "a[1]\n", [1, 1]),
6032 // `[]`, `:` twice
6033 ("slice", "a[1:2:3]\n", [2, 3]),
6034 // `{}`, `:`
6035 ("dict", "{k: v}\n", [2, 2]),
6036 // `{}`, `,`
6037 ("set", "{1, 2}\n", [2, 2]),
6038 // `[]`, `,` twice, `()`
6039 ("list and tuple", "[1, (2, 3)]\n", [3, 4]),
6040 // `()`, `for`, `in`
6041 ("generator", "(x for x in y)\n", [3, 3]),
6042 // `{}`, `:`, `for`, `in`
6043 ("dict comprehension", "{k: v for k in y}\n", [4, 4]),
6044 // `()`, `:=` — the walrus is its own token, not a `:`
6045 ("walrus", "(n := 3)\n", [2, 2]),
6046 // `lambda`, `:`
6047 ("lambda", "lambda x: x\n", [2, 2]),
6048 // `def`, `()`, `:` twice, `->`, `pass`
6049 ("annotations", "def f(x: int) -> int:\n pass\n", [5, 6]),
6050 // `:`, `=`
6051 ("annotated assignment", "x: int = 1\n", [2, 2]),
6052 // `@`, `def`, `()`, `:`, `pass`
6053 ("decorator", "@d\ndef f():\n pass\n", [5, 5]),
6054 // `if`, `:`, `pass`
6055 ("if block", "if x:\n pass\n", [3, 3]),
6056 // `while`, `:`, `pass`
6057 ("while block", "while x:\n pass\n", [3, 3]),
6058 // `for`, `in`, `:`, `pass`
6059 ("for block", "for i in y:\n pass\n", [4, 4]),
6060 // `with`, `:`, `pass`
6061 ("with block", "with o:\n pass\n", [3, 3]),
6062 // `try`, `except`, `:` twice, `pass` twice
6063 ("try block", "try:\n pass\nexcept E:\n pass\n", [4, 6]),
6064 // `class`, `:`, `pass` — the keyword is billed like `def`
6065 // (#1550)
6066 ("class block", "class C:\n pass\n", [3, 3]),
6067 // `class` twice, `()`, `:` twice, `pass` — one `class` per
6068 // keyword, nothing for the `ClassDefinition` wrapper
6069 (
6070 "nested class",
6071 "class C(B):\n class D:\n pass\n",
6072 [4, 6],
6073 ),
6074 // `;`
6075 ("statement separator", "a; b\n", [1, 1]),
6076 ];
6077 assert_operator_rows::<PythonParser>("foo.py", ROWS);
6078 }
6079
6080 /// #1486: an f-string replacement field's `{` and format-spec `:` are
6081 /// spelling, not operations, like every other language's
6082 /// interpolation opener (#1314) — the f-string rows bill nothing.
6083 /// #1551 adds the self-documenting `=` (`f"{z=}"`), a directive of
6084 /// the same field. The guard reads the token's *parent*, so a dict,
6085 /// a keyword argument or a walrus written inside a replacement field
6086 /// keeps its own operators.
6087 #[cfg(feature = "python")]
6088 #[test]
6089 fn python_f_string_delimiters_are_not_operators_1486() {
6090 const ROWS: &[(&str, &str, [u64; 2])] = &[
6091 ("interpolation opener", "f\"{x}\"\n", [0, 0]),
6092 ("format spec colon", "f\"{x:>10}\"\n", [0, 0]),
6093 ("nested format expression", "f\"{a:{w}}\"\n", [0, 0]),
6094 // `{}`, `:`, `[]` of the dict and its subscript, nothing of
6095 // the replacement field around them
6096 ("dict inside a field", "f\"{ {k: v}[k] }\"\n", [3, 3]),
6097 ("self-documenting =", "f\"{z=}\"\n", [0, 0]),
6098 // the `=` keeps its spaces in the output; `!r` and `:` follow it
6099 ("spaced = with conversion", "f\"{z = !r:>10}\"\n", [0, 0]),
6100 // a `=` in a nested format expression (Python 3.12+)
6101 ("nested self-documenting =", "f\"{a:{w=}}\"\n", [0, 0]),
6102 // `()`, `=` — a keyword argument's `=` is a real operator
6103 ("keyword argument inside a field", "f\"{g(a=1)}\"\n", [2, 2]),
6104 // `()`, `:=`
6105 ("walrus inside a field", "f\"{(y := 1)}\"\n", [2, 2]),
6106 ];
6107 assert_operator_rows::<PythonParser>("foo.py", ROWS);
6108 }
6109
6110 /// #1552: a JS-family `class` keyword bills as `function` does, once
6111 /// per keyword. Shared by the four grammars of the macro; each runs
6112 /// its own call so a grammar whose `Class2` stops being the keyword
6113 /// leaf fails alone. Before #1552 every row billed one fewer.
6114 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
6115 const JS_FAMILY_CLASS_ROWS: &[(&str, &str, [u64; 2])] = &[
6116 // `class`, `{}`
6117 ("declaration", "class C {}\n", [2, 2]),
6118 // `const`, `=`, `class`, `{}`, `;` — the class *expression*
6119 // node wraps the keyword and bills nothing of its own
6120 ("expression", "const D = class {};\n", [5, 5]),
6121 // `class` twice, `extends`, `{}` twice
6122 ("two classes", "class A {}\nclass B extends A {}\n", [3, 5]),
6123 ];
6124
6125 #[cfg(any(feature = "javascript", feature = "mozjs"))]
6126 #[test]
6127 fn js_class_keyword_is_an_operator_1552() {
6128 #[cfg(feature = "javascript")]
6129 assert_operator_rows::<JavascriptParser>("foo.js", JS_FAMILY_CLASS_ROWS);
6130 #[cfg(feature = "mozjs")]
6131 assert_operator_rows::<MozjsParser>("foo.js", JS_FAMILY_CLASS_ROWS);
6132 }
6133
6134 /// #1554: a function *expression* bills its `function` keyword once,
6135 /// as a declaration does — the `function_expression` wrapper bills
6136 /// nothing of its own. Before #1554 the plain and `async` rows billed
6137 /// one extra unique and total operator, and the nested row one extra
6138 /// unique and two extra totals. The declaration and generator rows
6139 /// are controls: neither node was ever in the operator arm.
6140 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
6141 const JS_FAMILY_FUNCTION_ROWS: &[(&str, &str, [u64; 2])] = &[
6142 // `function`, `()`, `{}`
6143 ("declaration", "function h() {}\n", [3, 3]),
6144 // `const`, `=`, `function`, `()`, `{}`, `;`
6145 ("expression", "const f = function () {};\n", [6, 6]),
6146 // `const`, `=`, `async`, `function`, `()`, `{}`, `;`
6147 (
6148 "async expression",
6149 "const f = async function () {};\n",
6150 [7, 7],
6151 ),
6152 // `const`, `=`, `function`, `*`, `()`, `{}`, `;` — a generator
6153 // expression is its own `generator_function` node, never billed
6154 (
6155 "generator expression",
6156 "const f = function* () {};\n",
6157 [7, 7],
6158 ),
6159 // `const`, `=`, `return`, `;` x2, and `function`, `()`, `{}`
6160 // twice each
6161 (
6162 "nested expressions",
6163 "const f = function () { return function () {}; };\n",
6164 [7, 11],
6165 ),
6166 ];
6167
6168 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
6169 #[test]
6170 fn js_family_function_expression_bills_function_once_1554() {
6171 #[cfg(feature = "javascript")]
6172 assert_operator_rows::<JavascriptParser>("foo.js", JS_FAMILY_FUNCTION_ROWS);
6173 #[cfg(feature = "mozjs")]
6174 assert_operator_rows::<MozjsParser>("foo.js", JS_FAMILY_FUNCTION_ROWS);
6175 #[cfg(feature = "typescript")]
6176 {
6177 assert_operator_rows::<TypescriptParser>("foo.ts", JS_FAMILY_FUNCTION_ROWS);
6178 assert_operator_rows::<TsxParser>("foo.tsx", JS_FAMILY_FUNCTION_ROWS);
6179 }
6180 }
6181
6182 /// #1552 for the two TypeScript grammars: the shared `class` rows,
6183 /// plus their own `interface` and `enum` declaration keywords.
6184 #[cfg(feature = "typescript")]
6185 #[test]
6186 fn typescript_declaration_keywords_are_operators_1552() {
6187 const ROWS: &[(&str, &str, [u64; 2])] = &[
6188 // `interface`, `{}`
6189 ("interface", "interface I {}\n", [2, 2]),
6190 // `enum`, `{}`
6191 ("enum", "enum E { A }\n", [2, 2]),
6192 ];
6193 assert_operator_rows::<TypescriptParser>("foo.ts", JS_FAMILY_CLASS_ROWS);
6194 assert_operator_rows::<TypescriptParser>("foo.ts", ROWS);
6195 assert_operator_rows::<TsxParser>("foo.tsx", JS_FAMILY_CLASS_ROWS);
6196 assert_operator_rows::<TsxParser>("foo.tsx", ROWS);
6197 }
6198
6199 /// #1552: Java's type-declaration keywords are operators, as C#'s
6200 /// are. The class-literal row pins the guard: `C.class` is one
6201 /// operand, and its `class` leaf bills nothing of its own.
6202 #[cfg(feature = "java")]
6203 #[test]
6204 fn java_declaration_keywords_are_operators_1552() {
6205 const ROWS: &[(&str, &str, [u64; 2])] = &[
6206 // `class`, `{}`
6207 ("class", "class C {}\n", [2, 2]),
6208 // `interface`, `{}`
6209 ("interface", "interface I {}\n", [2, 2]),
6210 // `enum`, `{}`
6211 ("enum", "enum E { A }\n", [2, 2]),
6212 // `record`, `()`, `int`, `{}`
6213 ("record", "record R(int x) {}\n", [4, 4]),
6214 // `@interface`, `{}`
6215 ("annotation type", "@interface N {}\n", [2, 2]),
6216 // `class`, `{}`, `=`, `.`, `;` — one `class`: the literal's
6217 // is part of the `C.class` operand
6218 ("class literal", "class U { Object o = C.class; }\n", [5, 5]),
6219 ];
6220 assert_operator_rows::<JavaParser>("Foo.java", ROWS);
6221 }
6222
6223 /// #1560: a Java type name is an operand, as in C#, Rust, the C
6224 /// family, Go, Groovy and TypeScript. Each row isolates one position
6225 /// of the single `type_identifier` kind. The class-literal rows pin
6226 /// that a type inside `Foo.class` is not billed again on top of the
6227 /// literal's own operand, whether it sits there directly or under an
6228 /// array or scoped type; the `var` rows, that the inferred-type
6229 /// keyword bills nothing while a variable named `var` stays an
6230 /// operand.
6231 #[cfg(feature = "java")]
6232 #[test]
6233 fn java_type_names_are_operands_1560() {
6234 const ROWS: &[(&str, &str, [u64; 2])] = &[
6235 // `C`, `I` x2, `Foo`, `f`
6236 (
6237 "field type and implements",
6238 "class C implements I { Foo f; }\ninterface I {}\n",
6239 [4, 5],
6240 ),
6241 // `C`, `java`, `util`, `List`, `Foo`, `l`
6242 (
6243 "scoped and generic",
6244 "class C { java.util.List<Foo> l; }\n",
6245 [6, 6],
6246 ),
6247 // `C`, `T` x2, `Comparable`
6248 (
6249 "type parameter",
6250 "class C<T extends Comparable<T>> {}\n",
6251 [3, 4],
6252 ),
6253 // `C`, `m`, `Ex`
6254 ("throws", "class C { void m() throws Ex {} }\n", [3, 3]),
6255 // `C`, `Object`, `a`, `Foo.class`
6256 (
6257 "class literal",
6258 "class C { Object a = Foo.class; }\n",
6259 [4, 4],
6260 ),
6261 // `C`, `Object`, `a`, `Foo[].class`
6262 (
6263 "array class literal",
6264 "class C { Object a = Foo[].class; }\n",
6265 [4, 4],
6266 ),
6267 // `C`, `Object`, `a`, `java.util.List.class`
6268 (
6269 "scoped class literal",
6270 "class C { Object a = java.util.List.class; }\n",
6271 [4, 4],
6272 ),
6273 // `C`, `m`, `x`, `1` — no `var`
6274 (
6275 "inferred type",
6276 "class C { void m() { var x = 1; } }\n",
6277 [4, 4],
6278 ),
6279 // `C`, `m`, `var`, `1` — the variable's name
6280 (
6281 "variable named var",
6282 "class C { void m() { int var = 1; } }\n",
6283 [4, 4],
6284 ),
6285 ];
6286 assert_operand_rows::<JavaParser>("Foo.java", ROWS);
6287 }
6288
6289 /// #1552: Groovy's type-declaration keywords are operators, as
6290 /// Java's are. `String.class` is a `field_access` whose `class` is a
6291 /// plain identifier, so it stays an operand with no guard needed.
6292 #[cfg(feature = "groovy")]
6293 #[test]
6294 fn groovy_declaration_keywords_are_operators_1552() {
6295 const ROWS: &[(&str, &str, [u64; 2])] = &[
6296 // `class`, `{}`
6297 ("class", "class C {}\n", [2, 2]),
6298 // `interface`, `{}`
6299 ("interface", "interface I {}\n", [2, 2]),
6300 // `trait`, `{}`
6301 ("trait", "trait T {}\n", [2, 2]),
6302 // `enum`, `{}`
6303 ("enum", "enum E { A }\n", [2, 2]),
6304 // `record`, `()`, `{}`
6305 ("record", "record R(int x) {}\n", [3, 3]),
6306 // `def`, `=`, `.` — no `class`
6307 ("class reference", "def a = String.class\n", [3, 3]),
6308 // `=` twice: a script-level `record` / `trait` used as a name
6309 // falls into error recovery with the keyword leaf, which is
6310 // not a declaration and bills nothing
6311 ("contextual names", "record = 3\ntrait = 4\n", [1, 2]),
6312 ];
6313 assert_operator_rows::<GroovyParser>("foo.groovy", ROWS);
6314 }
6315
6316 /// #1552 rows shared by the two C++ clones. The template-parameter
6317 /// rows pin the synonym: `class T` is `typename T`, and the two
6318 /// spellings score alike. Both were unbilled until #1557 billed
6319 /// `typename`, and the parameter `class` with it.
6320 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
6321 const CPP_DECLARATION_KEYWORD_ROWS: &[(&str, &str, [u64; 2])] = &[
6322 // `class`, `{}`, `;`
6323 ("class", "class C {};\n", [3, 3]),
6324 // `struct`, `{}`, `;`
6325 ("struct", "struct S {};\n", [3, 3]),
6326 // `union`, `{}`, `int`, `;` twice
6327 ("union", "union U { int a; };\n", [4, 5]),
6328 // `enum`, `{}`, `;`
6329 ("enum", "enum E { A };\n", [3, 3]),
6330 // `enum`, `class`, `{}`, `;`
6331 ("enum class", "enum class F { B };\n", [4, 4]),
6332 // `struct`, `;` — an elaborated type bills its keyword
6333 ("elaborated type", "struct S s;\n", [2, 2]),
6334 // `template`, `<>`, `class`, `struct`, `{}`, `;` — the parameter
6335 // list's brackets are one pair (#1559)
6336 (
6337 "class parameter",
6338 "template <class T> struct W {};\n",
6339 [6, 6],
6340 ),
6341 // as above, `typename` in place of `class`
6342 (
6343 "typename parameter",
6344 "template <typename T> struct W {};\n",
6345 [6, 6],
6346 ),
6347 // `template`, `<>`, `class`, `=`, `int`, `struct`, `{}`, `;`
6348 (
6349 "optional class parameter",
6350 "template <class T = int> struct W {};\n",
6351 [8, 8],
6352 ),
6353 // `template`, `<>`, `class`, `struct`, `{}`, `;` — `...` is an
6354 // operand
6355 (
6356 "variadic class parameter",
6357 "template <class... T> struct W {};\n",
6358 [6, 6],
6359 ),
6360 ];
6361
6362 /// One call per clone, so reverting either arm fails its own row;
6363 /// Mozcpp owns no file extension, so this is its only coverage.
6364 #[cfg(all(feature = "cpp", feature = "mozcpp"))]
6365 #[test]
6366 fn cpp_declaration_keywords_are_operators_1552() {
6367 assert_operator_rows::<CppParser>("foo.cpp", CPP_DECLARATION_KEYWORD_ROWS);
6368 assert_operator_rows::<MozcppParser>("foo.cpp", CPP_DECLARATION_KEYWORD_ROWS);
6369 }
6370
6371 /// #1552: C bills `struct` / `union` / `enum` as C++ does, so a
6372 /// header scores the same whichever grammar parses it.
6373 #[cfg(feature = "c")]
6374 #[test]
6375 fn c_declaration_keywords_are_operators_1552() {
6376 const ROWS: &[(&str, &str, [u64; 2])] = &[
6377 // `struct`, `{}`, `int`, `;` twice
6378 ("struct", "struct S { int a; };\n", [4, 5]),
6379 // `union`, `{}`, `int`, `;` twice
6380 ("union", "union U { int a; };\n", [4, 5]),
6381 // `enum`, `{}`, `;`
6382 ("enum", "enum E { A };\n", [3, 3]),
6383 // `struct`, `;`
6384 ("elaborated type", "struct S s;\n", [2, 2]),
6385 ];
6386 assert_operator_rows::<CParser>("foo.c", ROWS);
6387 }
6388
6389 /// #1552: ObjC bills C's `struct` and its own class and protocol
6390 /// declaration keywords. `@class` already billed its `@` and stays
6391 /// as it was.
6392 #[cfg(feature = "objc")]
6393 #[test]
6394 fn objc_declaration_keywords_are_operators_1552() {
6395 const ROWS: &[(&str, &str, [u64; 2])] = &[
6396 // `@interface`, `:`
6397 ("interface", "@interface C : NSObject\n@end\n", [2, 2]),
6398 // `@implementation`
6399 ("implementation", "@implementation C\n@end\n", [1, 1]),
6400 // `@protocol`
6401 ("protocol", "@protocol P\n@end\n", [1, 1]),
6402 // `struct`, `{}`, `int`, `;` twice
6403 ("struct", "struct S { int a; };\n", [4, 5]),
6404 // `@`, `;` — unchanged by #1552
6405 ("forward class", "@class Fwd;\n", [2, 2]),
6406 ];
6407 assert_operator_rows::<ObjcParser>("foo.m", ROWS);
6408 }
6409
6410 /// #1556: Kotlin's `interface` bills as its `class` and `object`
6411 /// do. The modifier rows score one above the bare keyword since
6412 /// #1558: `enum` in `enum class` is a `class_modifier` like `data`
6413 /// and `sealed`, and Kotlin bills its modifiers as C# does.
6414 #[cfg(feature = "kotlin")]
6415 #[test]
6416 fn kotlin_declaration_keywords_are_operators_1556() {
6417 const ROWS: &[(&str, &str, [u64; 2])] = &[
6418 // `class`, `{}`
6419 ("class", "class C {}\n", [2, 2]),
6420 // `interface`, `{}`
6421 ("interface", "interface I {}\n", [2, 2]),
6422 // `object`, `{}`
6423 ("object", "object O {}\n", [2, 2]),
6424 // `enum`, `class`, `{}`
6425 ("enum class", "enum class E { A }\n", [3, 3]),
6426 // `data`, `class`, `{}`
6427 ("data class", "data class D {}\n", [3, 3]),
6428 // `sealed`, `interface`, `{}`
6429 ("sealed interface", "sealed interface S {}\n", [3, 3]),
6430 // `interface`, `class`, `:`, `{}` twice
6431 (
6432 "implementing class",
6433 "interface J {}\nclass K : J {}\n",
6434 [4, 5],
6435 ),
6436 ];
6437 assert_operator_rows::<KotlinParser>("foo.kt", ROWS);
6438 }
6439
6440 /// #1558: Kotlin bills its modifier keywords as C# bills its own,
6441 /// one family per row, each keyword the leaf and never
6442 /// its `*_modifier` wrapper. The "names" row is the control: the same
6443 /// words used as names are operands, and add no operator. The rows
6444 /// after it are names the grammar fails to parse, whose recovery keeps
6445 /// the keyword leaf, and their control.
6446 #[cfg(feature = "kotlin")]
6447 #[test]
6448 fn kotlin_modifiers_are_operators_1558() {
6449 const ROWS: &[(&str, &str, [u64; 2])] = &[
6450 // `public`, `private`, `protected`, `internal`, `val` x4,
6451 // `=` x4, `class`, `{}`
6452 (
6453 "visibility",
6454 "class C {\n public val a = 1\n private val b = 1\n \
6455 protected val c = 1\n internal val d = 1\n}\n",
6456 [8, 14],
6457 ),
6458 // `abstract` x2, `open`, `final`, `override`, `class`, `{}` x2,
6459 // `fun` x3, `()` x3, `=`
6460 (
6461 "inheritance and member",
6462 "abstract class A {\n abstract fun f()\n open fun g() {}\n \
6463 final override fun toString() = \"\"\n}\n",
6464 [9, 15],
6465 ),
6466 // `inner`, `value`, `annotation`, `class` x3, `()`, `val`, `:`
6467 (
6468 "class",
6469 "inner class I\nvalue class V(val x: Int)\nannotation class N\n",
6470 [7, 9],
6471 ),
6472 // `lateinit`, `companion`, `const`, `class`, `object`, `var`,
6473 // `val`, `:`, `=`, `{}` x2
6474 (
6475 "property and companion",
6476 "class C {\n lateinit var a: String\n companion object {\n \
6477 const val b = 1\n }\n}\n",
6478 [10, 11],
6479 ),
6480 // six modifiers, `fun` x6, `()` x6, `{}` x5
6481 (
6482 "function",
6483 "tailrec fun a() {}\noperator fun b() {}\ninfix fun c() {}\n\
6484 inline fun d() {}\nexternal fun e()\nsuspend fun f() {}\n",
6485 [9, 23],
6486 ),
6487 // `inline`, `noinline`, `crossinline`, `vararg`, `fun`,
6488 // `()` x3, `:` x3, `->` x2, `,` x2, `{}`
6489 (
6490 "parameter",
6491 "inline fun f(noinline a: () -> Unit, crossinline b: () -> Unit, \
6492 vararg c: Int) {}\n",
6493 [10, 16],
6494 ),
6495 // `inline`, `reified`, `in`, `out`, `fun`, `class`, `<>` x2,
6496 // `,`, `()`, `{}`
6497 (
6498 "type parameter",
6499 "inline fun <reified T> f() {}\nclass Box<in A, out B>\n",
6500 [10, 11],
6501 ),
6502 // `expect`, `actual`, `fun` x2, `()` x2, `{}`
6503 ("platform", "expect fun f()\nactual fun g() {}\n", [5, 7]),
6504 // `fun`, `()`, `{}`: an `@N` use is the `annotation` node, not
6505 // the keyword, so it bills no `annotation` operator
6506 ("annotation use", "@N fun k() {}\n", [3, 3]),
6507 // `val` x3, `=` x3: `open`, `out` and `value` are names here
6508 (
6509 "names",
6510 "val open = 1\nval out = 2\nval value = 3\n",
6511 [2, 6],
6512 ),
6513 // The rows below are names the grammar fails to parse. Its
6514 // recovery keeps the keyword leaf, which must bill nothing;
6515 // each expected value is what the source scored before #1558.
6516 // `class`, `{}` x2, `var`, `=` x2, `fun`, `()`: `open = !open`
6517 // recovers into an `ERROR` holding `inheritance_modifier`.
6518 (
6519 "name stranded in an ERROR",
6520 "class Drawer {\n var open = false\n fun toggle() { open = !open }\n}\n",
6521 [6, 8],
6522 ),
6523 // `val`: `suspend` never parses as a name.
6524 ("suspend as a name", "val suspend = 1\n", [1, 1]),
6525 // `fun`, `()`, `:` x2, `=`: a modifier list followed by an
6526 // inserted zero-width parameter name.
6527 (
6528 "parameter named vararg",
6529 "fun f(vararg: Int): Int = vararg\n",
6530 [4, 5],
6531 ),
6532 // The same row with a comment before the inserted name: an
6533 // extra, so the name is still the next token. It scored
6534 // [5, 6], a phantom `vararg` operator.
6535 (
6536 "parameter named vararg, commented",
6537 "fun f(vararg /*note*/: Int): Int = vararg\n",
6538 [4, 5],
6539 ),
6540 // Control: a real `vararg` before a commented parameter keeps
6541 // its operator. `vararg`, `fun`, `()`, `:`, `{}`.
6542 (
6543 "vararg modifier, commented",
6544 "fun f(vararg /*note*/ x: Int) {}\n",
6545 [5, 5],
6546 ),
6547 // `class`, `()`, `:`
6548 ("class parameter named open", "class C(open: Int)\n", [3, 3]),
6549 // `val`, `=`, `{}`, `:`
6550 (
6551 "lambda parameter named open",
6552 "val l = { open: Int -> open }\n",
6553 [4, 4],
6554 ),
6555 // Control for the gate: a real `suspend` in a function type's
6556 // `type_modifiers` and in a declaration's `modifiers` bills
6557 // twice. `val`, `:`, `suspend` x2, `()` x2, `->`, `=`, `{}` x2,
6558 // `fun`.
6559 (
6560 "suspend modifiers",
6561 "val f: suspend () -> Unit = {}\nsuspend fun g() {}\n",
6562 [8, 11],
6563 ),
6564 ];
6565 assert_operator_rows::<KotlinParser>("foo.kt", ROWS);
6566 }
6567
6568 /// #1558: `reified` is the one Kotlin modifier spelled as an aliased
6569 /// named kind. `KotlinCode::is_primitive` keys it by its text, so
6570 /// `bca ops` lists the word written, not the grammar's
6571 /// `reification_modifier`. `n1` cannot see the difference — the kind
6572 /// has one spelling either way — so the vocabulary is the only place
6573 /// this is observable.
6574 #[cfg(feature = "kotlin")]
6575 #[test]
6576 fn kotlin_reified_is_listed_by_its_text_1558() {
6577 let ops = ops_of::<KotlinParser>("fun <reified T> f() {}\n", "foo.kt");
6578 assert!(
6579 ops.operators.iter().any(|op| op == "reified"),
6580 "{:?}",
6581 ops.operators
6582 );
6583 assert!(
6584 !ops.operators.iter().any(|op| op == "reification_modifier"),
6585 "{:?}",
6586 ops.operators
6587 );
6588 }
6589
6590 /// #1557 rows shared by the two C++ clones: the declaration keywords
6591 /// #1552 left out, one per row, plus the two disambiguator positions
6592 /// (`typename T::x`, `x.template f`) that bill the same leaf.
6593 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
6594 const CPP_MORE_DECLARATION_KEYWORD_ROWS: &[(&str, &str, [u64; 2])] = &[
6595 // `namespace`, `{}`
6596 ("namespace", "namespace N {}\n", [2, 2]),
6597 // `typedef`, `int`, `;`
6598 ("typedef", "typedef int T;\n", [3, 3]),
6599 // `using`, `=`, `int`, `;`
6600 ("alias declaration", "using U = int;\n", [4, 4]),
6601 // `using`, `namespace`, `;`
6602 ("using directive", "using namespace std;\n", [3, 3]),
6603 // `template`, `<>`, `typename`, `;` and the `struct` of `S`
6604 (
6605 "template declaration",
6606 "template <typename T> struct S;\n",
6607 [5, 5],
6608 ),
6609 // `typename`, `::`, `;`
6610 ("dependent type", "typename T::type x;\n", [3, 3]),
6611 // `void`, `()` twice, `{}`, `.`, `template`, `<>`, `int`, `;`
6612 (
6613 "template disambiguator",
6614 "void g() { x.template f<int>(); }\n",
6615 [8, 9],
6616 ),
6617 ];
6618
6619 /// One call per clone, as for the #1552 rows.
6620 #[cfg(all(feature = "cpp", feature = "mozcpp"))]
6621 #[test]
6622 fn cpp_more_declaration_keywords_are_operators_1557() {
6623 assert_operator_rows::<CppParser>("foo.cpp", CPP_MORE_DECLARATION_KEYWORD_ROWS);
6624 assert_operator_rows::<MozcppParser>("foo.cpp", CPP_MORE_DECLARATION_KEYWORD_ROWS);
6625 }
6626
6627 /// #1557: `typedef` is a C keyword, so C and ObjC bill it as C++
6628 /// does — a header scores alike whichever grammar parses it.
6629 #[cfg(any(feature = "c", feature = "objc"))]
6630 const C_TYPEDEF_ROWS: &[(&str, &str, [u64; 2])] = &[
6631 // `typedef`, `int`, `;`
6632 ("typedef", "typedef int T;\n", [3, 3]),
6633 // `typedef`, `struct`, `{}`, `int`, `;` twice
6634 ("typedef struct", "typedef struct S { int a; } S;\n", [5, 6]),
6635 ];
6636
6637 #[cfg(any(feature = "c", feature = "objc"))]
6638 #[test]
6639 fn c_family_typedef_is_an_operator_1557() {
6640 #[cfg(feature = "c")]
6641 assert_operator_rows::<CParser>("foo.c", C_TYPEDEF_ROWS);
6642 #[cfg(feature = "objc")]
6643 assert_operator_rows::<ObjcParser>("foo.m", C_TYPEDEF_ROWS);
6644 }
6645
6646 /// #1557: the TypeScript declaration keywords #1552 left out. The
6647 /// "keywords as names" row pins that each word, used as a *name*,
6648 /// stays an operand: the grammar emits an `identifier` there. The two
6649 /// exported-binding rows are the one place it emits the `type` leaf
6650 /// for a name, which must bill nothing; the last two rows are their
6651 /// controls, a type-only specifier and re-export whose `type` still
6652 /// bills.
6653 #[cfg(feature = "typescript")]
6654 const TS_DECLARATION_KEYWORD_ROWS: &[(&str, &str, [u64; 2])] = &[
6655 // `type`, `=`, `number`, `;`
6656 ("type alias", "type T = number;\n", [4, 4]),
6657 // `namespace`, `{}`
6658 ("namespace", "namespace N {}\n", [2, 2]),
6659 // `module`, `{}`. Billing the `module` declaration node instead
6660 // of its keyword leaf would score this row the same: every
6661 // spelling probed (bodiless, quoted, dotted, exported, ERROR
6662 // recovery) emits the two together, so only the getter's
6663 // choice of the leaf, not this row, keeps them from both billing.
6664 ("module", "module M {}\n", [2, 2]),
6665 // `declare`, `module`, `{}`
6666 ("ambient module", "declare module \"m\" {}\n", [3, 3]),
6667 // `declare`, `global`, `{}`
6668 ("global augmentation", "declare global {}\n", [3, 3]),
6669 // `import`, `type`, `{}`, `from`, `;`
6670 (
6671 "type-only import",
6672 "import type { A } from \"a\";\n",
6673 [5, 5],
6674 ),
6675 // `let`, `=` twice, `;` twice, `.` — no keyword
6676 (
6677 "keywords as names",
6678 "let type = 1;\nmodule.exports = declare;\n",
6679 [4, 6],
6680 ),
6681 // `const`, `=`, `;` twice, `export`, `{}` — no keyword. The
6682 // grammar recovers an exported binding named `type` as the
6683 // keyword leaf, under an `ERROR` or beside one in the specifier.
6684 (
6685 "exported binding named type",
6686 "const type = 1;\nexport { type };\n",
6687 [5, 6],
6688 ),
6689 (
6690 "renamed exported binding named type",
6691 "const type = 1;\nexport { type as kind };\n",
6692 [5, 6],
6693 ),
6694 // `export`, `{}`, `type`, `from`, `;` — a type-only specifier
6695 // parses cleanly, so its keyword still bills.
6696 (
6697 "type-only export specifier",
6698 "export { type Foo } from \"m\";\n",
6699 [5, 5],
6700 ),
6701 // `export`, `type`, `*`, `from`, `;` — the pinned grammar fails
6702 // TS 5.0's type-only re-export into an `ERROR` that hangs from the
6703 // statement, not from an export clause, and its keyword bills.
6704 (
6705 "type-only re-export",
6706 "export type * from \"./types\";\n",
6707 [5, 5],
6708 ),
6709 ];
6710
6711 /// #1557: a TypeScript type name is an operand, as C#, Rust and the
6712 /// C family bill theirs. Each row's names are `type_identifier`
6713 /// leaves except where noted, and before #1557 none of them billed.
6714 #[cfg(feature = "typescript")]
6715 const TS_TYPE_NAME_OPERAND_ROWS: &[(&str, &str, [u64; 2])] = &[
6716 // `T`
6717 ("alias name", "type T = number;\n", [1, 1]),
6718 // `I`
6719 ("interface name", "interface I {}\n", [1, 1]),
6720 // `C`
6721 ("class name", "class C {}\n", [1, 1]),
6722 // `x` (an identifier), `Foo`
6723 ("annotation", "let x: Foo;\n", [2, 2]),
6724 // `m`, `Map`, `K` twice
6725 ("generic", "let m: Map<K, K>;\n", [3, 4]),
6726 // `f`, `a` (identifiers), `U` twice
6727 ("type parameter", "function f<U>(a: U) {}\n", [3, 4]),
6728 // `q`, `ns` (identifiers), `T` — the `nested_type_identifier`
6729 // wrapper is unlisted, so its text is not a fourth operand
6730 ("qualified type", "let q: ns.T;\n", [3, 3]),
6731 // `C`, `I`
6732 ("implements", "class C implements I {}\n", [2, 2]),
6733 ];
6734
6735 #[cfg(feature = "typescript")]
6736 #[test]
6737 fn typescript_declaration_keywords_and_type_names_1557() {
6738 assert_operator_rows::<TypescriptParser>("foo.ts", TS_DECLARATION_KEYWORD_ROWS);
6739 assert_operator_rows::<TsxParser>("foo.tsx", TS_DECLARATION_KEYWORD_ROWS);
6740 assert_operand_rows::<TypescriptParser>("foo.ts", TS_TYPE_NAME_OPERAND_ROWS);
6741 assert_operand_rows::<TsxParser>("foo.tsx", TS_TYPE_NAME_OPERAND_ROWS);
6742 }
6743
6744 /// #1561: TypeScript's type-level operator keywords bill as their
6745 /// value-level siblings `as`, `typeof` and `extends` do, each as its
6746 /// keyword leaf under an unbilled wrapper. The `asserts a is Baz` row
6747 /// pins that the `asserts` node and its keyword token bill once
6748 /// between them, and the last row that the same words used as names
6749 /// are operands. Both TypeScript grammars carry the change.
6750 #[cfg(feature = "typescript")]
6751 #[test]
6752 fn typescript_type_operator_keywords_1561() {
6753 const ROWS: &[(&str, &str, [u64; 2])] = &[
6754 // `let`, `=`, `satisfies`, `;`
6755 ("satisfies", "let s = w satisfies Sat;\n", [4, 4]),
6756 // `type`, `=`, `keyof`, `;`
6757 ("keyof", "type K = keyof T;\n", [4, 4]),
6758 // `type`, `<>` x2, `=`, `extends`, `infer`, `?`, `:`, `;`
6759 (
6760 "infer",
6761 "type I<X> = X extends Array<infer E> ? E : X;\n",
6762 [8, 9],
6763 ),
6764 // `function`, `()`, `:` x2, `unknown`, `is`, `{}`, `return`, `;`
6765 (
6766 "type predicate",
6767 "function g(a: unknown): a is Baz { return true; }\n",
6768 [8, 9],
6769 ),
6770 // `function`, `()`, `:` x2, `unknown`, `asserts`, `is`, `{}`
6771 (
6772 "asserts predicate",
6773 "function h(a: unknown): asserts a is Baz {}\n",
6774 [7, 8],
6775 ),
6776 // `function`, `()`, `:` x2, `unknown`, `asserts`, `{}`
6777 (
6778 "bare asserts",
6779 "function k(a: unknown): asserts a {}\n",
6780 [6, 7],
6781 ),
6782 // `const`, `=` and `;` five times each: the words are names
6783 (
6784 "names",
6785 "const keyof = 1;\nconst infer = 2;\nconst is = 3;\nconst asserts = 4;\n\
6786 const satisfies = 5;\n",
6787 [3, 15],
6788 ),
6789 ];
6790 assert_operator_rows::<TypescriptParser>("foo.ts", ROWS);
6791 assert_operator_rows::<TsxParser>("foo.tsx", ROWS);
6792 }
6793
6794 #[cfg(feature = "bash")]
6795 #[test]
6796 fn bash_operators_and_operands() {
6797 check_metrics::<BashParser>(
6798 "#!/bin/bash
6799f() {
6800 local x=1
6801 if [ $x -eq 1 ]; then
6802 echo 'one'
6803 fi
6804}",
6805 "foo.sh",
6806 |metric| {
6807 // Operators (9 unique, 9 occurrences): the opening
6808 // delimiters `()`/`{}`/`[]` (each folded to one glyph and
6809 // counted once per balanced pair, #695 — the closers no
6810 // longer add a second operator), `local`, `=`, `if`,
6811 // `then`, `fi`, `;`.
6812 // Operands (6 unique, 7 occurrences): `f`, `x` (the
6813 // assignment LHS `variable_name`, kind 160), `1` (twice:
6814 // `=1` and `-eq 1`), `$x` (the `simple_expansion` — its
6815 // inner `variable_name` leaf is now suppressed so `$x`
6816 // counts once, #695), `echo`, `'one'`.
6817 // N2 was 8 before #1351: `echo` counted twice, once as the
6818 // `command_name` wrapper and once as the `word` it wraps.
6819 assert_eq!(metric.halstead.unique_operators(), 9);
6820 assert_eq!(metric.halstead.total_operators(), 9);
6821 assert_eq!(metric.halstead.unique_operands(), 6);
6822 assert_eq!(metric.halstead.total_operands(), 7);
6823 insta::assert_json_snapshot!(metric.halstead);
6824 },
6825 );
6826 }
6827
6828 #[cfg(feature = "bash")]
6829 #[test]
6830 fn bash_interpolated_string_no_double_count() {
6831 // Regression: issue #180. A double-quoted Bash string containing
6832 // `$name`, `${name[…]}`, or `$(cmd)` used to be classified as a
6833 // Halstead operand AND have its inner `simple_expansion` /
6834 // `expansion` / `command_substitution` children classified as
6835 // operands too. We now skip the wrapping literal when it has an
6836 // expansion child so only the inner expansion contributes.
6837 //
6838 // expected: operands across `a="plain"\nb="$x"\n` —
6839 // line 1: variable_name `a`, plain string `"plain"` (no
6840 // expansion, still operand) → 2.
6841 // line 2: variable_name `b`, wrapping `"$x"` skipped (has
6842 // expansion), `simple_expansion` `$x` (its inner
6843 // variable_name `x` leaf is suppressed under #695) → 2.
6844 // Total unique operands: 4 (`a`, `b`, `"plain"`, `$x`), each
6845 // appearing once → N2 = 4. Before #695 the inner `x` leaf of
6846 // `$x` was also counted (u_operands = 5, N2 = 5); before the
6847 // earlier #180 fix the wrapping `"$x"` literal was counted too.
6848 // The `=` is the only operator; appears twice (N1 = 2, n1 = 1).
6849 check_metrics::<BashParser>("a=\"plain\"\nb=\"$x\"\n", "foo.sh", |metric| {
6850 assert_eq!(metric.halstead.unique_operators(), 1);
6851 assert_eq!(metric.halstead.total_operators(), 2);
6852 assert_eq!(metric.halstead.unique_operands(), 4);
6853 assert_eq!(metric.halstead.total_operands(), 4);
6854 insta::assert_json_snapshot!(metric.halstead);
6855 });
6856 }
6857
6858 #[cfg(feature = "elixir")]
6859 #[test]
6860 fn elixir_interpolated_string_no_double_count() {
6861 // Regression: issue #180. Without the fix, an interpolated
6862 // Elixir `String` was classified as a single operand while its
6863 // inner `interpolation` identifier was also walked and
6864 // classified as its own operand — double-counting the
6865 // interpolated identifier's contribution to `N2`.
6866 //
6867 // expected: operand contributions for
6868 // `def greet(name) do\n msg = "Hi #{name}"\nend\n` —
6869 // `def`, `greet`, `name` (param), `msg`, and the inner `name`
6870 // (inside `#{...}`). With the fix, the wrapping
6871 // `"Hi #{name}"` literal is skipped (has `Interpolation`
6872 // child), so `name` is the only repeated operand:
6873 // u_operands = 4 (def, greet, name, msg), N2 = 5. Without the
6874 // fix, the wrapping literal would also count → u_operands = 5,
6875 // N2 = 6. Operators: `do`, `end`, `(`, `=` → u = N = 4.
6876 // Only the *opening* delimiters count after #695, so the `)`
6877 // and the `}` interpolation closer add no operator; #1314 then
6878 // dropped the `#{` opener too, on the rule that an
6879 // interpolation opener is spelling rather than an operation
6880 // (was 5 here, and 7 before #695).
6881 check_metrics::<ElixirParser>(
6882 "def greet(name) do\n msg = \"Hi #{name}\"\nend\n",
6883 "foo.ex",
6884 |metric| {
6885 assert_eq!(metric.halstead.unique_operators(), 4);
6886 assert_eq!(metric.halstead.total_operators(), 4);
6887 assert_eq!(metric.halstead.unique_operands(), 4);
6888 assert_eq!(metric.halstead.total_operands(), 5);
6889 insta::assert_json_snapshot!(metric.halstead);
6890 },
6891 );
6892 }
6893
6894 #[cfg(feature = "elixir")]
6895 #[test]
6896 fn elixir_plain_string_still_operand() {
6897 // The fix for #180 only skips wrapping literals that contain
6898 // interpolation; a plain `"hello"` must still contribute exactly
6899 // one operand. expected: `def`, `f`, `"hello"` → 3 unique
6900 // operands (n2 = 3), each appearing once (N2 = 3).
6901 check_metrics::<ElixirParser>("def f do\n \"hello\"\nend\n", "foo.ex", |metric| {
6902 assert_eq!(metric.halstead.unique_operands(), 3);
6903 assert_eq!(metric.halstead.total_operands(), 3);
6904 });
6905 }
6906
6907 #[cfg(feature = "elixir")]
6908 #[test]
6909 fn elixir_boolean_and_nil_literals_count_once() {
6910 // Regression: issue #1253. `boolean: choice("true", "false")`
6911 // and `nil: "nil"` each wrap a keyword leaf, and both the
6912 // wrapper and the leaf sat in the operand arm — so every
6913 // literal occurrence added +1 to N2. Operands are keyed by
6914 // source text, so the duplicate collapsed into the same
6915 // vocabulary entry and n2 stayed correct, which is why nothing
6916 // caught it.
6917 //
6918 // Source is the issue's reproducer plus a repeat of `true` and
6919 // `nil`, so N2 exceeds n2 and the assertions can tell "counted
6920 // once per occurrence" from "deduplicated into the vocabulary".
6921 // All three keywords appear, so restoring any one of `True`,
6922 // `False`, or `Nil2` to the operand arm trips this test.
6923 //
6924 // Operands by text key: `x`, `y`, `z`, `w`, `v`, `true` × 2,
6925 // `nil` × 2, `false` ⇒ n2 = 8, N2 = 10. Before the fix each of
6926 // the five literals counted twice ⇒ N2 = 15.
6927 //
6928 // This also guards the drift in the other direction. Elixir
6929 // classifies the wrapper and drops the leaf outright rather
6930 // than parent-guarding it, so a grammar bump that stopped
6931 // emitting `boolean` / `nil` would leave the leaves unclassified
6932 // and the literals would vanish from N2 entirely (⇒ 5 / 5)
6933 // rather than merely being miscounted.
6934 check_metrics::<ElixirParser>(
6935 "x = true\ny = nil\nz = false\nw = true\nv = nil\n",
6936 "foo.ex",
6937 |metric| {
6938 assert_eq!(metric.halstead.unique_operands(), 8);
6939 assert_eq!(metric.halstead.total_operands(), 10);
6940 },
6941 );
6942 }
6943
6944 #[cfg(feature = "elixir")]
6945 #[test]
6946 fn elixir_reserved_word_after_a_dot_stays_an_operand() {
6947 // Companion to the test above (#1253). Elixir drops `True` /
6948 // `False` / `Nil2` from the operand arm outright, which is only
6949 // safe because the one grammar position that accepts a reserved
6950 // word outside the `boolean` / `nil` wrapper — the right-hand
6951 // side of a remote dot — aliases it to `identifier`. This pins
6952 // that alias: if a grammar bump emitted the bare keyword there
6953 // instead, `Foo.nil` and `Foo.true` would silently stop
6954 // contributing an operand.
6955 //
6956 // Source: a = Foo.nil / b = Foo.true / c = nil
6957 //
6958 // Operands by text key: `a`, `Foo` × 2, `nil` × 2 (the aliased
6959 // identifier and the real literal, which share a text key),
6960 // `b`, `true`, `c` ⇒ n2 = 6, N2 = 8. Losing the alias drops the
6961 // two dotted references ⇒ N2 = 6.
6962 check_metrics::<ElixirParser>("a = Foo.nil\nb = Foo.true\nc = nil\n", "foo.ex", |metric| {
6963 assert_eq!(metric.halstead.unique_operands(), 6);
6964 assert_eq!(metric.halstead.total_operands(), 8);
6965 });
6966 }
6967
6968 #[cfg(feature = "elixir")]
6969 #[test]
6970 fn elixir_interpolated_sigil_no_double_count() {
6971 // Sigils mirror strings under #180. For `~r/foo#{name}/`, the
6972 // wrapping `Sigil` is skipped, but `SigilName` (`r`) and the
6973 // inner `name` identifier each contribute one operand.
6974 // expected: `def`, `f`, `name` (param), `re`, `r` (sigil name),
6975 // `name` (inside `#{...}`) → u_operands = 5, N2 = 6 (`name`
6976 // twice).
6977 check_metrics::<ElixirParser>(
6978 "def f(name) do\n re = ~r/foo#{name}/\nend\n",
6979 "foo.ex",
6980 |metric| {
6981 assert_eq!(metric.halstead.unique_operands(), 5);
6982 assert_eq!(metric.halstead.total_operands(), 6);
6983 },
6984 );
6985 }
6986
6987 #[cfg(feature = "elixir")]
6988 #[test]
6989 fn elixir_interpolated_charlist_no_double_count() {
6990 // Charlists mirror strings and sigils under #180. The
6991 // `E::String | E::Charlist | E::Sigil` arm in `get_op_type`
6992 // skips any wrapping literal that has an `Interpolation`
6993 // child; this test exercises the `Charlist` branch
6994 // specifically.
6995 //
6996 // expected: for `def f(name) do\n cl = 'Hi #{name}'\nend\n` —
6997 // `def`, `f`, `name` (param), `cl`, and the inner `name`
6998 // (inside `#{...}`). With the fix, the wrapping
6999 // `'Hi #{name}'` is skipped → u_operands = 4 (def, f, name,
7000 // cl), N2 = 5 (`name` twice).
7001 check_metrics::<ElixirParser>(
7002 "def f(name) do\n cl = 'Hi #{name}'\nend\n",
7003 "foo.ex",
7004 |metric| {
7005 assert_eq!(metric.halstead.unique_operands(), 4);
7006 assert_eq!(metric.halstead.total_operands(), 5);
7007 },
7008 );
7009 }
7010
7011 #[cfg(feature = "elixir")]
7012 #[test]
7013 fn elixir_sigil_delimiters_are_not_operators() {
7014 // Regression: issue #1256. Sigil delimiter tokens share their
7015 // kind ids with real operators (`SLASH`, `LPAREN`, `LBRACE`,
7016 // …) and were classified unconditionally, so `~r/abc/`
7017 // fabricated two division operators and the author's delimiter
7018 // choice moved n1/N1. The parent guard suppresses them under
7019 // `Sigil`; `~` stays the single per-sigil operator.
7020 //
7021 // expected: operators `=` × 3 and `~` × 3 → n1 = 2, N1 = 6.
7022 // Without the guard the delimiters added `/` × 2, `(`, `{` →
7023 // n1 = 5, N1 = 10. Operands: `a`, `~r/abc/i`, `r`, `i` (sigil
7024 // modifiers), `b`, `~w(one two)`, `w`, `c`, `~s{hi}`, `s` →
7025 // n2 = N2 = 10.
7026 check_metrics::<ElixirParser>(
7027 "a = ~r/abc/i\nb = ~w(one two)\nc = ~s{hi}\n",
7028 "foo.ex",
7029 |metric| {
7030 assert_eq!(metric.halstead.unique_operators(), 2);
7031 assert_eq!(metric.halstead.total_operators(), 6);
7032 assert_eq!(metric.halstead.unique_operands(), 10);
7033 assert_eq!(metric.halstead.total_operands(), 10);
7034 },
7035 );
7036 }
7037
7038 #[cfg(feature = "elixir")]
7039 #[test]
7040 fn elixir_sigil_delimiter_choice_is_invariant() {
7041 // Companion to the test above (#1256): two sigils differing
7042 // only in delimiter are the same literal, so every delimiter
7043 // choice must produce identical Halstead counts. `(` `[` `{`
7044 // `<` `/` `|` are the operator-kind delimiters the guard
7045 // covers; `"` and `'` never had an operator arm and pin the
7046 // already-correct path.
7047 //
7048 // expected per variant: operators `=`, `~` → n1 = 2, N1 = 2;
7049 // operands `x`, the sigil literal text, `w` (sigil name) →
7050 // n2 = 3, N2 = 3.
7051 for (open, close) in [
7052 ("(", ")"),
7053 ("[", "]"),
7054 ("{", "}"),
7055 ("<", ">"),
7056 ("/", "/"),
7057 ("|", "|"),
7058 ("\"", "\""),
7059 ("'", "'"),
7060 ] {
7061 assert_halstead_counts::<ElixirParser>(
7062 &format!("x = ~w{open}one two{close}\n"),
7063 "foo.ex",
7064 [2, 2, 3, 3],
7065 &format!("delimiter pair {open} {close}"),
7066 );
7067 }
7068 }
7069
7070 #[cfg(feature = "elixir")]
7071 #[test]
7072 fn elixir_standalone_operators_survive_the_sigil_guard() {
7073 // Control for #1256: the guard is parent-scoped, so the same
7074 // token kinds outside a sigil still count. Covers every guarded
7075 // kind standalone: `/` (division), `<` / `>` (comparison), `[`
7076 // and `|` (list cons), `(` (call), `{` (map literal, with its
7077 // `%`).
7078 //
7079 // expected: operators `=` × 6, `/`, `<`, `>`, `[`, `|`, `(`,
7080 // `%`, `{` → n1 = 9, N1 = 14. Operands: `x`, `a`, `b`, `y`,
7081 // `c`, `d`, `z`, `e`, `f`, `q`, `h`, `t`, `p`, `g`, `1`, `m`,
7082 // the `k:` keyword, `2` → n2 = N2 = 18.
7083 check_metrics::<ElixirParser>(
7084 "x = a / b\ny = c < d\nz = e > f\nq = [h | t]\np = g(1)\nm = %{k: 2}\n",
7085 "foo.ex",
7086 |metric| {
7087 assert_eq!(metric.halstead.unique_operators(), 9);
7088 assert_eq!(metric.halstead.total_operators(), 14);
7089 assert_eq!(metric.halstead.unique_operands(), 18);
7090 assert_eq!(metric.halstead.total_operands(), 18);
7091 },
7092 );
7093 }
7094
7095 #[cfg(feature = "elixir")]
7096 #[test]
7097 fn elixir_interpolated_sigil_keeps_inner_nodes_counting() {
7098 // Interpolation inside a sigil after #1256: the `{` delimiter
7099 // is suppressed (its parent is the `Sigil`), while the
7100 // `interpolation` child is a separate node whose inner
7101 // identifier must still count — the guard must not reach past
7102 // the delimiter tokens.
7103 //
7104 // expected: operators `=`, `~` → n1 = N1 = 2. Operands:
7105 // `v`, `s` (sigil name), `b` (interpolated identifier); the
7106 // wrapping sigil is skipped (`Interpolation` child, #180) and
7107 // `quoted_content` is unclassified → n2 = N2 = 3. The `#{`
7108 // marker was a third operator until #1314 dropped it.
7109 check_metrics::<ElixirParser>("v = ~s{a#{b} c}\n", "foo.ex", |metric| {
7110 assert_eq!(metric.halstead.unique_operators(), 2);
7111 assert_eq!(metric.halstead.total_operators(), 2);
7112 assert_eq!(metric.halstead.unique_operands(), 3);
7113 assert_eq!(metric.halstead.total_operands(), 3);
7114 });
7115
7116 // A guarded kind *inside* the interpolation: the `/` in
7117 // `#{a / b}` has `binary_operator` as its parent but the
7118 // `Sigil` as a further ancestor, so this input is the one
7119 // discriminator between the correct parent-scoped guard and a
7120 // wrong ancestor-scoped one that would swallow it.
7121 //
7122 // expected: operators `=`, `~`, `/` → n1 = N1 = 3 (the `#{`
7123 // opener stopped counting with #1314); operands `v`, `s`, `a`,
7124 // `b` → n2 = N2 = 4. The division is what this row is for, and
7125 // it still counts — the ancestor-scoped mutant drops it.
7126 check_metrics::<ElixirParser>("v = ~s{x #{a / b} y}\n", "foo.ex", |metric| {
7127 assert_eq!(metric.halstead.unique_operators(), 3);
7128 assert_eq!(metric.halstead.total_operators(), 3);
7129 assert_eq!(metric.halstead.unique_operands(), 4);
7130 assert_eq!(metric.halstead.total_operands(), 4);
7131 });
7132 }
7133
7134 #[cfg(feature = "bash")]
7135 #[test]
7136 fn bash_all_expansion_kinds_skip_wrapper() {
7137 // Exercises every node kind tested by
7138 // `bash_string_has_expansion`: `simple_expansion` (`$v`),
7139 // `expansion` (`${v[0]}`), `command_substitution` (`$(date)`),
7140 // and `arithmetic_expansion` (`$((1+2))`). A typo replacing
7141 // one kind with an aliased neighbour in `language_bash.rs`
7142 // (e.g., `ExpansionBody` instead of `Expansion`) would leave
7143 // the corresponding wrapping string counted as an operand and
7144 // shift the totals.
7145 //
7146 // expected: operands across the four lines —
7147 // line 1 `a="$v"`: var_name `a`, simple_expansion `$v` (its
7148 // inner var_name `v` leaf is suppressed under #695; wrapper
7149 // skipped) → 2
7150 // line 2 `b="${v[0]}"`: var_name `b`, var_name `v` (inside
7151 // subscript — parent is `expansion`, not `simple_expansion`,
7152 // so it still counts), number `0` (wrapper skipped,
7153 // `expansion` itself is not in the operand list) → 3
7154 // line 3 `c="$(date)"`: var_name `c`, the `word` `date` under
7155 // the `command_name` (wrapper skipped, `command_substitution`
7156 // not in operand list, and since #1351 the `command_name`
7157 // wrapper is not either) → 2
7158 // line 4 `d="$((1+2))"`: var_name `d`, numbers `1` and `2`
7159 // (wrapper skipped, `arithmetic_expansion` not in operand
7160 // list) → 3
7161 // Unique operands: a, b, c, d, $v, v, 0, date, 1, 2 → 10. Total
7162 // occurrences: 10 (`v` appears once — only line 2's subscript
7163 // leaf; line 1's `$v` inner leaf is suppressed — and `date` once,
7164 // as the `word`; before #1351 the `command_name` wrapping it
7165 // added a second `date` and N2 was 11). Operators after
7166 // #695: only the openers `[` (folded `[]`) and `+`, plus `=` four
7167 // times — the `}`/`)`/`))`/`]` closers no longer count.
7168 check_metrics::<BashParser>(
7169 "a=\"$v\"\nb=\"${v[0]}\"\nc=\"$(date)\"\nd=\"$((1+2))\"\n",
7170 "foo.sh",
7171 |metric| {
7172 assert_eq!(metric.halstead.unique_operators(), 3);
7173 assert_eq!(metric.halstead.total_operators(), 6);
7174 assert_eq!(metric.halstead.unique_operands(), 10);
7175 assert_eq!(metric.halstead.total_operands(), 10);
7176 },
7177 );
7178 }
7179
7180 /// Regression for #695. A bare `$x` (outside any string) parses as a
7181 /// `simple_expansion` wrapping a `variable_name` leaf — and `$?` / `$1`
7182 /// as a `simple_expansion` wrapping a `special_variable_name` leaf. Both
7183 /// the wrapper and the inner leaf used to be classified as operands, so
7184 /// each bare variable reference double-counted (the same hazard Tcl
7185 /// guards with its `Id2` exclusion and iRules with a parent check). The
7186 /// `variable_name` / `special_variable_name` arm now yields `Unknown`
7187 /// when its parent is a `simple_expansion`, so `$x` contributes exactly
7188 /// one operand while the assignment LHS `variable_name` (`x` in `x=…`,
7189 /// parent is `variable_assignment`) still counts.
7190 #[cfg(feature = "bash")]
7191 #[test]
7192 fn bash_bare_variable_no_double_count() {
7193 let source = "x=1\necho $x\necho $?\n";
7194 let path = PathBuf::from("foo.sh");
7195 let parser = BashParser::new(source.as_bytes().to_vec(), &path, None);
7196 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
7197 let bare_x = ops.operands.iter().filter(|o| o.as_str() == "$x").count();
7198 let special = ops.operands.iter().filter(|o| o.as_str() == "$?").count();
7199 // Each bare reference is exactly one operand; the inner leaf is not
7200 // double-counted. If the guard regressed, the inner `variable_name`
7201 // `x` would add a second `x` occurrence (text-colliding with the
7202 // assignment LHS) and the inner `special_variable_name` `?` would
7203 // appear as a standalone `?` operand.
7204 assert_eq!(
7205 bare_x, 1,
7206 "bare $x must be one operand; operands were {:?}",
7207 ops.operands
7208 );
7209 assert_eq!(
7210 special, 1,
7211 "bare $? must be one operand; operands were {:?}",
7212 ops.operands
7213 );
7214 assert!(
7215 !ops.operands.iter().any(|o| o.as_str() == "?"),
7216 "the inner special_variable_name `?` leaf must be suppressed; operands were {:?}",
7217 ops.operands
7218 );
7219 // The assignment LHS `variable_name` `x` (parent `variable_assignment`,
7220 // not `simple_expansion`) must still be an operand.
7221 assert!(
7222 ops.operands.iter().any(|o| o.as_str() == "x"),
7223 "assignment LHS `x` must still be an operand; operands were {:?}",
7224 ops.operands
7225 );
7226 }
7227
7228 /// Drives one row of a Bash wrapper/leaf double-count table: a grammar
7229 /// wrapper whose entire contribution to `N2` was its single,
7230 /// already-counted child (`command_name`, #1351; `translated_string`,
7231 /// #1358).
7232 ///
7233 /// `expected` is the post-fix `[n1, N1, n2, N2]`. `(n2_before,
7234 /// N2_before)` is what the same source scored while `wrapper` was still
7235 /// classified as an operand. Both columns are checked against the
7236 /// *current* parse under the model "one spurious operand per wrapper
7237 /// node" — they are not measured against the old code, which no longer
7238 /// exists to run:
7239 ///
7240 /// - `N2_before - N2` must equal the number of `wrapper` nodes. That
7241 /// identity *is* the defect: one spurious operand per wrapper.
7242 /// - `n2_before` must equal the post-fix operand vocabulary unioned
7243 /// with the wrapper spellings. It exceeds `n2` wherever the wrapper's
7244 /// whole text is not already an operand in its own right — either
7245 /// because it differs from its single child's (`"$cmd"`, `${cmd}`,
7246 /// `$"ls"`) or because it spans several (`foo$x`, `$(which ls)`,
7247 /// `{1..3}`).
7248 ///
7249 /// So a mistyped or stale column fails rather than misinforming the next
7250 /// reader — one did, during review of #1351 — but neither check can
7251 /// catch a *production* regression, because both hold whatever
7252 /// `get_op_type` answers. `assert_halstead_counts` below is the only
7253 /// assertion here that fails when the wrapper arm comes back; measured
7254 /// by neutralising it under the pre-fix arm, which leaves both tables
7255 /// green.
7256 #[cfg(feature = "bash")]
7257 #[track_caller]
7258 fn assert_bash_wrapper_sheds_one_operand(
7259 source: &str,
7260 expected: [u64; 4],
7261 (n2_before, total_before): (u64, u64),
7262 wrapper: Bash,
7263 ) {
7264 let name: &'static str = wrapper.clone().into();
7265 let kind = wrapper as u16;
7266 let code = source.as_bytes();
7267 let parser = BashParser::new(code.to_vec(), &PathBuf::from("foo.sh"), None);
7268 let spellings: Vec<&str> = parser
7269 .root()
7270 .preorder()
7271 .filter(|node| node.kind_id() == kind)
7272 .filter_map(|node| node.utf8_text(code))
7273 .collect();
7274 assert!(
7275 !spellings.is_empty(),
7276 "row {source:?} parses without a {name}, so it witnesses nothing",
7277 );
7278 // Phrased as an addition rather than `total_before - expected[3]`: a
7279 // future edit that inverts the two would underflow `u64` and panic
7280 // with a raw overflow message instead of the one below.
7281 assert_eq!(
7282 total_before,
7283 expected[3] + spellings.len() as u64,
7284 "row {source:?} must shed exactly one operand per {name}; \
7285 recorded N2_before {total_before}",
7286 );
7287
7288 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
7289 let mut vocabulary: HashSet<&str> = ops.operands.iter().map(String::as_str).collect();
7290 vocabulary.extend(spellings);
7291 assert_eq!(
7292 vocabulary.len() as u64,
7293 n2_before,
7294 "row {source:?}: n2 before the fix is the post-fix vocabulary \
7295 plus the {name} spellings; got {vocabulary:?}",
7296 );
7297
7298 assert_halstead_counts::<BashParser>(source, "foo.sh", expected, source);
7299 }
7300
7301 /// Regression for #1351, the command-name sibling of #695's bare
7302 /// `$x`. `command_name` is a pure wrapper: the grammar gives it
7303 /// exactly one required child (a `_primary_expression` or a
7304 /// `concatenation`) and it adds no text of its own, so classifying it
7305 /// as an operand *and* letting the walk reach the child counted every
7306 /// command name twice in `N2`.
7307 ///
7308 /// The table is the grammar-dispatch §6 evidence that deleting the arm
7309 /// zeroes nothing: it names every kind `command_name` can wrap, and in
7310 /// every row the command name still contributes at least one operand
7311 /// once the trailing `arg` is discounted.
7312 ///
7313 /// `n2_before` / `N2_before` are what each row scores with the
7314 /// `command_name` arm restored *against today's tree*, not a historical
7315 /// measurement — the `$"ls" arg` row has since lost a
7316 /// `translated_string` wrapper to #1358 as well.
7317 /// `assert_bash_wrapper_sheds_one_operand` re-derives both from the
7318 /// current parse rather than trusting them.
7319 #[cfg(feature = "bash")]
7320 #[test]
7321 fn bash_command_name_wrapper_no_double_count() {
7322 // (source, [n1, N1, n2, N2], (n2_before, N2_before))
7323 let cases: [(&str, [u64; 4], (u64, u64)); 14] = [
7324 // word
7325 ("ls bar\n", [0, 0, 2, 2], (2, 3)),
7326 // number
7327 ("1 arg\n", [0, 0, 2, 2], (2, 3)),
7328 // string, inert
7329 ("\"ls\" arg\n", [0, 0, 2, 2], (2, 3)),
7330 // string wrapping an expansion: the wrapper string is already
7331 // skipped (#180), so before #1351 the `command_name` was the
7332 // only thing counting the quoted spelling — which also planted
7333 // a spurious `"$cmd"` entry in n2 beside `$cmd`.
7334 ("\"$cmd\" arg\n", [0, 0, 2, 2], (3, 3)),
7335 // raw_string
7336 ("'ls' arg\n", [0, 0, 2, 2], (2, 3)),
7337 // ansi_c_string
7338 ("$'ls' arg\n", [0, 0, 2, 2], (2, 3)),
7339 // translated_string. Two wrappers stacked here — `command_name`
7340 // over `translated_string` — so the pre-#1351 column below is
7341 // measured against a tree #1358 has already thinned to
7342 // `"ls"` + `arg`.
7343 ("$\"ls\" arg\n", [0, 0, 2, 2], (3, 3)),
7344 // simple_expansion
7345 ("$cmd arg\n", [0, 0, 2, 2], (2, 3)),
7346 // brace expansion: counts through its inner `variable_name`,
7347 // whose `SimpleExpansion` parent guard does not apply here.
7348 ("${cmd} arg\n", [0, 0, 2, 2], (3, 3)),
7349 // command_substitution: counts through the nested command.
7350 // Two command names here — the outer one and `which`.
7351 ("$(which ls) arg\n", [0, 0, 3, 3], (4, 5)),
7352 // process_substitution, likewise two command names.
7353 ("<(ls) arg\n", [0, 0, 2, 2], (3, 4)),
7354 // arithmetic_expansion
7355 ("$((1+1)) arg\n", [1, 1, 2, 3], (3, 4)),
7356 // brace_expression
7357 ("{1..3} arg\n", [1, 1, 3, 3], (4, 4)),
7358 // concatenation
7359 ("foo$x arg\n", [0, 0, 3, 3], (4, 4)),
7360 ];
7361 for (source, expected, before) in cases {
7362 assert!(
7363 expected[3] > 1,
7364 "row {source:?} must leave the command name at least one \
7365 operand beside `arg`; a zero here means the deleted arm \
7366 was load-bearing for this spelling",
7367 );
7368 assert_bash_wrapper_sheds_one_operand(source, expected, before, Bash::CommandName);
7369 }
7370 }
7371
7372 /// Regression for #1358, the sibling of #1351 in the same `match`.
7373 /// `translated_string: $ => seq('$', $.string)` carries exactly one
7374 /// required `string` child and no text of its own beyond the `$`, so
7375 /// classifying the wrapper *and* letting the walk reach the child
7376 /// counted every `$"…"` twice in `N2`.
7377 ///
7378 /// The rows assert the total, `N2`. `n2` alone cannot witness this
7379 /// double count: the wrapper and its child spell *different* text
7380 /// (`$"x"` against `"x"`), so the pre-fix tree split one operand into
7381 /// two vocabulary entries rather than repeating one — which is why the
7382 /// `n2_before` column is carried too, and checked against the wrapper
7383 /// spellings the row actually parses to.
7384 #[cfg(feature = "bash")]
7385 #[test]
7386 fn bash_translated_string_wrapper_no_double_count() {
7387 // (source, [n1, N1, n2, N2], (n2_before, N2_before))
7388 let cases: [(&str, [u64; 4], (u64, u64)); 7] = [
7389 // Assignment RHS.
7390 ("a=$\"x\"\n", [1, 1, 2, 2], (3, 3)),
7391 // Empty translated string. The grammar makes the inner `string`
7392 // required, so the wrapper is never childless — the condition
7393 // grammar-dispatch §6 warns that deletion would regress.
7394 ("a=$\"\"\n", [1, 1, 2, 2], (3, 3)),
7395 // The one spelling the deleted arm *was* load-bearing for: the
7396 // inner `string` wraps an operandless brace `expansion`, so the
7397 // whole `$"…"` now scores zero. That is #1351's residue one
7398 // level down rather than a new gap — a plain `a="${#}"` already
7399 // scored `[1, 1, 1, 1]` — and
7400 // `bash_translated_string_scores_alike_in_both_positions` pins
7401 // that both positions agree on it.
7402 ("a=$\"${#}\"\n", [1, 1, 1, 1], (2, 2)),
7403 // `${!}` is the same residue plus a `!`, which the operator arm
7404 // counts; it must not turn into an operand along the way.
7405 ("a=$\"${!}\"\n", [2, 2, 1, 1], (2, 2)),
7406 // `case` subject.
7407 ("case $\"y\" in *) ;; esac\n", [4, 4, 1, 1], (2, 2)),
7408 // Command-name position, where #1351's wrapper stacks on top of
7409 // this one; the `_before` columns here price the
7410 // `translated_string` alone.
7411 ("$\"ls\" arg\n", [0, 0, 2, 2], (3, 3)),
7412 // Interpolating form. `bash_string_has_expansion` inspects the
7413 // node's own children, and a `translated_string` has only `$`
7414 // and `string`, so it never saw the `simple_expansion` one level
7415 // down: the wrapper reintroduced exactly the double count #180
7416 // removed, for the `$"…"` spelling alone. Only `$y` and `b` may
7417 // survive.
7418 ("b=$\"$y\"\n", [1, 1, 2, 2], (3, 3)),
7419 ];
7420 for (source, expected, before) in cases {
7421 assert_bash_wrapper_sheds_one_operand(source, expected, before, Bash::TranslatedString);
7422 }
7423 }
7424
7425 /// The load-bearing half of #1358's grammar-dispatch §6 argument, which
7426 /// nothing else asserts. In ordinary argument position the grammar emits
7427 /// no `translated_string` at all — just a `$` token and a `string` — so
7428 /// the `string` is the node present for *every* spelling, and dropping
7429 /// the wrapper makes the wrapper-bearing positions agree with argument
7430 /// position rather than newly disagree. If a grammar bump starts
7431 /// emitting the wrapper here, the deletion has to be re-derived.
7432 #[cfg(feature = "bash")]
7433 #[test]
7434 fn bash_translated_string_scores_alike_in_both_positions() {
7435 // `${#}` carries the residue: it contributes no operand of its own,
7436 // so the second row pins that the deletion left *both* positions at
7437 // zero rather than only the wrapper-bearing one.
7438 //
7439 // Only the command-name half moves under the pre-fix arm; the
7440 // argument-position half is the reference the parity is measured
7441 // against, and `cmd $"…"` is asserted nowhere else. Measured — with
7442 // the argument assertions dropped this test still fails pre-fix,
7443 // with the command-name ones dropped it passes — so do not
7444 // "simplify" it down to the failing half.
7445 for (spelling, expected) in [("$\"hi\"", [0, 0, 2, 2]), ("$\"${#}\"", [0, 0, 1, 1])] {
7446 let as_command_name = format!("{spelling} arg\n");
7447 let as_argument = format!("cmd {spelling}\n");
7448 let parse = |source: &str| {
7449 BashParser::new(source.as_bytes().to_vec(), &PathBuf::from("foo.sh"), None)
7450 };
7451 assert!(
7452 !ast_has_kind_id(&parse(&as_argument), Bash::TranslatedString as u16),
7453 "argument position must still parse {spelling} as a bare `$` \
7454 plus a `string`; a grammar bump that emits the wrapper here \
7455 invalidates the parity this test rests on",
7456 );
7457 assert!(
7458 ast_has_kind_id(&parse(&as_command_name), Bash::TranslatedString as u16),
7459 "command-name position must still emit the wrapper, or the \
7460 pair witnesses nothing",
7461 );
7462 assert_halstead_counts::<BashParser>(
7463 &as_command_name,
7464 "foo.sh",
7465 expected,
7466 &as_command_name,
7467 );
7468 assert_halstead_counts::<BashParser>(&as_argument, "foo.sh", expected, &as_argument);
7469 }
7470 }
7471
7472 /// Pins the one residue of #1351: a brace `expansion` with no
7473 /// `variable_name` leaf. `${#}` (positional-parameter count) and `${!}`
7474 /// (last background PID) hold only anonymous tokens, and
7475 /// `Bash::Expansion` is not an operand, so the whole expansion
7476 /// contributes nothing.
7477 ///
7478 /// That was already true in *argument* position before the fix, which
7479 /// is the reason grammar-dispatch §6's gate-don't-delete rule did not
7480 /// apply to the `command_name` arm: deleting it made command-name
7481 /// position agree with argument position rather than newly disagree.
7482 /// The parity is the load-bearing half of that argument and nothing
7483 /// else asserts it, so if a future arm starts classifying these the
7484 /// two positions have to move together.
7485 #[cfg(feature = "bash")]
7486 #[test]
7487 fn bash_operandless_expansion_scores_alike_in_both_positions() {
7488 // `${!}` carries a `!`, which the operator arm counts; `${#}`'s `#`
7489 // is not an operator. Both positions must agree per spelling.
7490 for (spelling, expected) in [("${#}", [0, 0, 1, 1]), ("${!}", [1, 1, 1, 1])] {
7491 let as_command_name = format!("{spelling} arg\n");
7492 let as_argument = format!("cmd {spelling}\n");
7493 assert_halstead_counts::<BashParser>(
7494 &as_command_name,
7495 "foo.sh",
7496 expected,
7497 &as_command_name,
7498 );
7499 assert_halstead_counts::<BashParser>(&as_argument, "foo.sh", expected, &as_argument);
7500 }
7501 }
7502
7503 /// Drift marker for #1351 (lesson 34 / grammar-dispatch §2). `_concat`
7504 /// (`Bash::Concat`) is a hidden zero-width external token the scanner
7505 /// emits between `concatenation` parts; the parser never surfaces it as
7506 /// a node, and it spells no operand, so `BashCode::get_op_type` lists
7507 /// neither it nor the visible `concatenation` wrapper. If a grammar
7508 /// bump starts emitting it, this fails and the classification must be
7509 /// re-derived rather than assumed still absent.
7510 ///
7511 /// Measured, not assumed: putting `Bash::Concat` back in the operand
7512 /// arm fails no test in the suite, because the token is unreachable.
7513 /// Unreachability is the only coverage such an arm can have, which is
7514 /// why this test asserts it directly instead of asserting a count.
7515 #[cfg(feature = "bash")]
7516 #[test]
7517 fn bash_hidden_concat_token_is_unreachable() {
7518 let source = "a=foo$x\nb=pre\"$y\"post\ncmd bar$z\n";
7519 let path = PathBuf::from("foo.sh");
7520 let parser = BashParser::new(source.as_bytes().to_vec(), &path, None);
7521 // Non-vacuity: the visible `concatenation` this source is written
7522 // to produce must actually be in the parse, so the negative below
7523 // is about `_concat` and not about a source that concatenates
7524 // nothing.
7525 assert!(
7526 ast_has_kind_id(&parser, Bash::Concatenation as u16),
7527 "expected a visible `concatenation` node in the parse",
7528 );
7529 assert!(
7530 !ast_has_kind_id(&parser, Bash::Concat as u16),
7531 "the hidden `_concat` token surfaced; re-derive its \
7532 classification in BashCode::get_op_type against the new grammar",
7533 );
7534 }
7535
7536 #[cfg(feature = "tcl")]
7537 #[test]
7538 fn tcl_operators_and_operands() {
7539 check_metrics::<TclParser>(
7540 "proc f {a b} {
7541 set x [expr {$a + $b}]
7542 if {$x > 0 && $x != 0} {
7543 return $x
7544 }
7545 return 0
7546}",
7547 "foo.tcl",
7548 |metric| {
7549 // Anchored per the snapshot policy in AGENTS.md, which
7550 // this call predates. Operators `proc`, `set`, `[]`,
7551 // `{}`, `expr`, `+`, `if`, `>`, `&&`, `!=` → n1 = 10,
7552 // N1 = 14 (`{}` × 4 for the proc parameter list, the
7553 // proc body, the two `expr`/`if` conditions and the
7554 // `if` body — the `expr` braces are an `Expr`, the
7555 // bodies a `BracedWord`). Operands `f`, `a`, `b`, `x`,
7556 // `$a`, `$b`, `$x`, `0` and `return` → n2 = 9,
7557 // N2 = 14. Before #1354 the proc body and the `if`
7558 // body were operands too → 11 / 16.
7559 assert_eq!(metric.halstead.unique_operators(), 10);
7560 assert_eq!(metric.halstead.total_operators(), 14);
7561 assert_eq!(metric.halstead.unique_operands(), 9);
7562 assert_eq!(metric.halstead.total_operands(), 14);
7563 insta::assert_json_snapshot!(metric.halstead);
7564 },
7565 );
7566 }
7567
7568 #[cfg(feature = "tcl")]
7569 #[test]
7570 fn tcl_bitwise_ternary_string_ops() {
7571 // Exercises operator families not covered by tcl_operators_and_operands:
7572 // bitwise (&, |, ^, ~, <<, >>), ternary (?), and string-comparison (eq, ne, in, ni).
7573 check_metrics::<TclParser>(
7574 "proc f {a b} {
7575 set bits [expr {$a & $b | $a ^ ~$b}]
7576 set sh [expr {$a << 1 | $b >> 1}]
7577 set t [expr {$a > 0 ? $a : $b}]
7578 if {$a eq {x} || $a ne {y}} {
7579 return $a
7580 }
7581 return $b
7582}",
7583 "foo.tcl",
7584 |metric| {
7585 // Anchored per the snapshot policy in AGENTS.md, which
7586 // this call predates. N1 fell 33 → 31 with #1314: the
7587 // `if` condition's `{x}` and `{y}` are braced *words*,
7588 // so their openers stopped fabricating a `{}` operator.
7589 // n1 is unchanged at 18 because the `{}` entry survives
7590 // on the proc body and the `expr` braces — which is
7591 // exactly why the fabrication was invisible in n1 and
7592 // is the reason to assert N1 as well (#1294).
7593 //
7594 // The operand columns fell 17 / 30 → 13 / 26 with
7595 // #1354, and the operator columns did not move: the two
7596 // script bodies (the proc's and the `if`'s) stopped
7597 // being operands, and the `x` / `y` inside the braced
7598 // words `{x}` and `{y}` are now part of the one operand
7599 // each word contributes.
7600 assert_eq!(metric.halstead.unique_operators(), 18);
7601 assert_eq!(metric.halstead.total_operators(), 31);
7602 assert_eq!(metric.halstead.unique_operands(), 13);
7603 assert_eq!(metric.halstead.total_operands(), 26);
7604 insta::assert_json_snapshot!(metric.halstead);
7605 },
7606 );
7607 }
7608
7609 #[cfg(feature = "tcl")]
7610 #[test]
7611 fn tcl_array_reference_bills_the_reference_and_the_index() {
7612 // `$arr($i)` is the reference plus the index Tcl substitutes
7613 // inside the parens, and `arr(k)` as a `set` target is the name
7614 // plus the literal index; the `array_index` wrapper is neither.
7615 // The quoted spelling is deliberate — the vendored grammar
7616 // mis-parses a bare `$arr(k)` in command-word position. Pinned
7617 // per dialect and as a parity in `tests/parity/`, because the
7618 // iRules twin listed the wrapper as a third operand.
7619 assert_ops_operands::<TclParser>(
7620 "set arr(k) 1\nset z \"$arr($i)\"\n",
7621 "foo.tcl",
7622 6,
7623 vec!["arr", "k", "1", "z", "$arr($i)", "$i"],
7624 );
7625 }
7626
7627 #[cfg(feature = "tcl")]
7628 #[test]
7629 fn tcl_bare_variable_operand() {
7630 // Bare `$varname` produces a VariableSubstitution node (already an operand).
7631 // Its anonymous Id2 child must NOT be counted separately; each reference is 1 operand.
7632 check_metrics::<TclParser>(
7633 "proc f {x} {
7634 return $x
7635}",
7636 "foo.tcl",
7637 |metric| {
7638 // Anchored per the snapshot policy in AGENTS.md, which
7639 // this call predates. Operators `proc` and `{}` × 2
7640 // (the parameter list and the body) → n1 = 2, N1 = 3.
7641 // Operands `f`, the parameter `x`, `return` and `$x` —
7642 // one occurrence each, so a re-counted `x` leaf inside
7643 // `$x` would show up in N2 even though it collides with
7644 // the parameter in n2. Before #1354 the proc body was a
7645 // fifth operand.
7646 assert_eq!(metric.halstead.unique_operators(), 2);
7647 assert_eq!(metric.halstead.total_operators(), 3);
7648 assert_eq!(metric.halstead.unique_operands(), 4);
7649 assert_eq!(metric.halstead.total_operands(), 4);
7650 insta::assert_json_snapshot!(metric.halstead);
7651 },
7652 );
7653 }
7654
7655 #[cfg(feature = "tcl")]
7656 #[test]
7657 fn tcl_inert_quoted_word_counts_as_operand() {
7658 // Regression for #277. A `"..."` literal with no `$var` / `[cmd]`
7659 // interpolation must contribute exactly one operand (the wrapping
7660 // `QuotedWord`). The string content `hello world` is exposed as a
7661 // single `_quoted_word_content` token (not itself classified by
7662 // `get_op_type`), so the only operands here are `f`, `s`, and the
7663 // quoted string. `set` is the anonymous `Set2` keyword and is
7664 // classified as an operator, not an operand.
7665 check_metrics::<TclParser>(
7666 "proc f {} {
7667 set s \"hello world\"
7668}",
7669 "foo.tcl",
7670 |metric| {
7671 // Operands: `f`, the `set` target `s`, `"hello world"` —
7672 // 3 unique, 3 total. Before #1294 this read 3/3 for a
7673 // different reason, with `s` missing and the proc-body
7674 // `braced_word` making the count coincidentally
7675 // plausible; #1354 removed that body operand, so the
7676 // three named here are now the whole list. The wrapping
7677 // `QuotedWord` must still contribute exactly one operand
7678 // when it carries no interpolation children; dropping to 2
7679 // would mean the inert case was over-guarded.
7680 assert_eq!(metric.halstead.unique_operands(), 3);
7681 assert_eq!(metric.halstead.total_operands(), 3);
7682 insta::assert_json_snapshot!(metric.halstead);
7683 },
7684 );
7685 }
7686
7687 #[cfg(feature = "tcl")]
7688 #[test]
7689 fn tcl_interpolated_quoted_word_no_double_count() {
7690 // Regression for #277. Before the fix, `"$x is $y"` produced an
7691 // extra operand for the wrapping `QuotedWord` on top of the two
7692 // inner `VariableSubstitution` operands (`$x`, `$y`), giving 7.
7693 // After the fix, the wrapper is `TokenRole::Unknown` whenever
7694 // it carries an interpolation child, so operand attribution
7695 // belongs solely to the inner substitutions.
7696 check_metrics::<TclParser>(
7697 "proc f {x y} {
7698 set s \"$x is $y\"
7699}",
7700 "foo.tcl",
7701 |metric| {
7702 // Operands: `f`, `x`, `y` (proc args), the `set` target
7703 // `s`, `$x`, `$y` — 6 unique, 6 total. The wrapping
7704 // `QuotedWord` contributes nothing, and since #1354
7705 // neither does the proc-body `braced_word`. Before #277
7706 // the wrapper double-counted.
7707 assert_eq!(metric.halstead.unique_operands(), 6);
7708 assert_eq!(metric.halstead.total_operands(), 6);
7709 insta::assert_json_snapshot!(metric.halstead);
7710 },
7711 );
7712 }
7713
7714 #[cfg(feature = "tcl")]
7715 #[test]
7716 fn tcl_command_substitution_quoted_word_no_double_count() {
7717 // Regression for #277. A `"...[cmd]..."` literal exposes the
7718 // bracketed command as a `command_substitution` child whose inner
7719 // identifiers/literals contribute their own operands. The wrapping
7720 // `QuotedWord` must not also be classified as an operand, or the
7721 // command's identifier would be counted alongside a phantom
7722 // wrapper operand.
7723 check_metrics::<TclParser>(
7724 "proc f {} {
7725 set s \"result: [foo]\"
7726}",
7727 "foo.tcl",
7728 |metric| {
7729 // Operands: `f`, the `set` target `s`, `foo` — 3 unique,
7730 // 3 total. The wrapping `QuotedWord` and the inert text
7731 // `result: ` do not contribute extra operands, and since
7732 // #1354 neither does the proc-body `braced_word`. Before
7733 // #277 the wrapper double-counted.
7734 assert_eq!(metric.halstead.unique_operands(), 3);
7735 assert_eq!(metric.halstead.total_operands(), 3);
7736 insta::assert_json_snapshot!(metric.halstead);
7737 },
7738 );
7739 }
7740
7741 /// Regression for #1294. The `set` target parses as the anonymous
7742 /// `id` token (`Tcl::Id2`) — the same kind as the leaf inside a
7743 /// `variable_substitution` — and the getter used to exclude that kind
7744 /// wholesale, so every variable a Tcl script assigned was absent from
7745 /// n2/N2. The guard is now parent-scoped: a target `id` counts, a
7746 /// var-sub leaf does not. Exact occurrence counts distinguish this
7747 /// fix from a regression in either direction: a re-blanketed
7748 /// exclusion drops `s`/`t` (total 2), while losing the guard
7749 /// double-counts the `$s` leaf as a second `s` (total 5).
7750 #[cfg(feature = "tcl")]
7751 #[test]
7752 fn tcl_set_target_is_operand() {
7753 let source = "set s 1\nset t $s\n";
7754 let path = PathBuf::from("foo.tcl");
7755 let parser = TclParser::new(source.as_bytes().to_vec(), &path, None);
7756 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
7757 // expected operands: targets `s` and `t`, literal `1`, reference
7758 // `$s` (wrapper only) — 4 total, each exactly once.
7759 for operand in ["s", "t", "1", "$s"] {
7760 assert_eq!(
7761 ops.operands
7762 .iter()
7763 .filter(|o| o.as_str() == operand)
7764 .count(),
7765 1,
7766 "`{operand}` must be exactly one operand; operands were {:?}",
7767 ops.operands
7768 );
7769 }
7770 assert_eq!(
7771 ops.operands.len(),
7772 4,
7773 "operands must be exactly s, t, 1, $s; got {:?}",
7774 ops.operands
7775 );
7776
7777 check_metrics::<TclParser>(source, "foo.tcl", |metric| {
7778 // expected: n2 = 4 (s, t, 1, $s), N2 = 4; operators are the
7779 // two `set` keywords — n1 = 1, N1 = 2.
7780 assert_eq!(metric.halstead.unique_operands(), 4);
7781 assert_eq!(metric.halstead.total_operands(), 4);
7782 assert_eq!(metric.halstead.unique_operators(), 1);
7783 assert_eq!(metric.halstead.total_operators(), 2);
7784 });
7785 }
7786
7787 /// Drift marker for #1294 (lesson 34 / grammar-dispatch §2): the
7788 /// *named* `id` rule (`Tcl::Id`, kind_id 84) never surfaces at the
7789 /// pinned tree-sitter-tcl — the parser emits the anonymous `Id2` in
7790 /// both positions the getter guards (the `set` target and the
7791 /// var-sub leaf). The `Tcl::Id` arm in `get_op_type` is therefore
7792 /// defensive; if a grammar bump starts emitting 84 this fails and
7793 /// the arm's classification must be re-derived instead of trusted.
7794 #[cfg(feature = "tcl")]
7795 #[test]
7796 fn tcl_named_id_variant_is_unreachable() {
7797 let source = "proc f {x} {\n set s $x\n foreach v {1 2} { puts \"$v\" }\n}\n";
7798 let path = PathBuf::from("foo.tcl");
7799 let parser = TclParser::new(source.as_bytes().to_vec(), &path, None);
7800 // Non-vacuity: the anonymous token must be present in this parse
7801 // (both the `set` target and the `$x` / `$v` leaves emit it).
7802 assert!(
7803 ast_has_kind_id(&parser, Tcl::Id2 as u16),
7804 "expected the anonymous Tcl::Id2 token to appear in the parse",
7805 );
7806 assert!(
7807 !ast_has_kind_id(&parser, Tcl::Id as u16),
7808 "the named Tcl::Id rule surfaced; re-derive the defensive \
7809 `Tcl::Id` arm in TclCode::get_op_type against the new grammar",
7810 );
7811 }
7812
7813 #[cfg(feature = "tcl")]
7814 #[test]
7815 fn tcl_braced_word_delimiter_is_not_an_operator() {
7816 // Regression: issue #1314, the Tcl sibling of Elixir #1256 and
7817 // Ruby/Perl #1312. A braced *word* — a literal value, not a
7818 // script — carries its `{` as an `LBRACE` child, the kind id a
7819 // real block uses, so `set a {braced word}` reported a `{}`
7820 // operator with no block in the source.
7821 //
7822 // expected: operator `set` × 3 → n1 = 1, N1 = 3. Operands
7823 // `a`, `b`, `c`, `$a` and `{braced word}` × 2 → n2 = 5,
7824 // N2 = 6. Before the guard the two openers added `{}` →
7825 // n1 = 2, N1 = 5; before #1354 the inner `braced` / `word`
7826 // counted alongside the word containing them → n2 = 7,
7827 // N2 = 10.
7828 check_metrics::<TclParser>(
7829 "set a {braced word}\nset b {braced word}\nset c $a\n",
7830 "foo.tcl",
7831 |metric| {
7832 assert_eq!(metric.halstead.unique_operators(), 1);
7833 assert_eq!(metric.halstead.total_operators(), 3);
7834 assert_eq!(metric.halstead.unique_operands(), 5);
7835 assert_eq!(metric.halstead.total_operands(), 6);
7836 },
7837 );
7838 }
7839
7840 #[cfg(feature = "tcl")]
7841 #[test]
7842 fn tcl_script_bodies_keep_their_braces() {
7843 // Control for #1314, and the reason a kind-scoped guard is safe
7844 // in Tcl where it would not be elsewhere: the grammar gives the
7845 // literal and the block *different* kinds. A `proc` body, an
7846 // `if` body and an `if` condition are `BracedWord` (88) and
7847 // `Expr` (97); only the value form is `BracedWordSimple` (89).
7848 // This fixture nests a braced word inside a real script body,
7849 // so a guard that keyed on the brace alone would drop the
7850 // block's `{}` and fail here.
7851 //
7852 // expected: operators `proc`, `set` × 2, `if`, `>`, and `{}`
7853 // × 4 (the proc parameter list, the proc body, the `if`
7854 // condition, the `if` body) → n1 = 5, N1 = 9. Operands, all
7855 // distinct → n2 = N2 = 8: `p`, `x`, `a`, `$x`, `1`, `b` and
7856 // the two braced *words* `{v w}` and `{y}`, each one operand
7857 // rather than one per inner word.
7858 //
7859 // Before #1354 this read 13 / 13. The five extra entries were
7860 // the inner words `v`, `w`, `y` and the two *script* bodies,
7861 // which were `BracedWord` operands in their own right — so a
7862 // block counted twice over, once as the operand and once as
7863 // the `{}` operator this test is about. Both halves are gone;
7864 // the operator columns are what this test guards and they did
7865 // not move.
7866 check_metrics::<TclParser>(
7867 "proc p {x} {\n set a {v w}\n if {$x > 1} { set b {y} }\n}\n",
7868 "foo.tcl",
7869 |metric| {
7870 assert_eq!(metric.halstead.unique_operators(), 5);
7871 assert_eq!(metric.halstead.total_operators(), 9);
7872 assert_eq!(metric.halstead.unique_operands(), 8);
7873 assert_eq!(metric.halstead.total_operands(), 8);
7874 },
7875 );
7876 }
7877
7878 #[cfg(feature = "tcl")]
7879 #[test]
7880 fn tcl_braced_word_guard_is_parent_scoped_not_ancestor_scoped() {
7881 // The input that separates the parent-scoped guard from the
7882 // ancestor-scanning mutant. I first recorded this distinction
7883 // as *unobservable* in Tcl, reasoning that a braced word holds
7884 // only simple words and nested braced words. `bca dump` says
7885 // otherwise: the grammar parses a `[…]` command substitution
7886 // inside a braced word, and the `if` inside it brings an `Expr`
7887 // condition and a `BracedWord` body, each with its own `{`,
7888 // both of them non-immediate descendants of the
7889 // `BracedWordSimple`. An ancestor scan swallows both.
7890 //
7891 // (Real Tcl does not substitute inside braces — this is the
7892 // grammar modelling structure it will not evaluate. What the
7893 // classifier sees is what the metric reports, so it is the
7894 // right fixture regardless.)
7895 //
7896 // expected: operators `set`, `[]`, `if`, and `{}` × 2 (the
7897 // `if` condition's `Expr` and its `BracedWord` body; the outer
7898 // value word's own `{` is suppressed) → n1 = 4, N1 = 5.
7899 // Operands `z`, the whole braced word, and `$q` / `puts` / `w`
7900 // from inside the command substitution → n2 = N2 = 5. Under
7901 // the ancestor-scoped mutant both surviving braces vanish:
7902 // n1 = 3, N1 = 3.
7903 //
7904 // #1354 widened the guard from the `{` alone to every direct
7905 // child of the braced word, which is why `x` and `v` are no
7906 // longer operands and the nested script body no longer is
7907 // either. It did not change the *scope*: the three operands
7908 // from inside the command substitution are grandchildren, and
7909 // an ancestor-scoped guard would drop them too.
7910 check_metrics::<TclParser>("set z {x [if {$q} {puts w}] v}\n", "foo.tcl", |metric| {
7911 assert_eq!(metric.halstead.unique_operators(), 4);
7912 assert_eq!(metric.halstead.total_operators(), 5);
7913 assert_eq!(metric.halstead.unique_operands(), 5);
7914 assert_eq!(metric.halstead.total_operands(), 5);
7915 });
7916 }
7917
7918 #[cfg(feature = "irules")]
7919 #[test]
7920 fn irules_braced_word_guard_is_parent_scoped_not_ancestor_scoped() {
7921 // The iRules twin of the test above — the two getters are
7922 // clones, so the mutant must fail in both.
7923 //
7924 // expected: operators `when`, `set`, `[]`, `if`, `{}` × 3 (the
7925 // handler body, the `if` condition and the `if` body) → n1 = 5,
7926 // N1 = 7. Operands `HTTP_REQUEST`, `z`, the whole braced word,
7927 // and `$q` / `log` / `w` from inside the command substitution →
7928 // n2 = N2 = 6 (10 before #1354, which also took the direct
7929 // children `x` / `v` and the two script bodies out of the
7930 // operand set without moving the operator columns this test
7931 // guards).
7932 check_metrics::<IrulesParser>(
7933 "when HTTP_REQUEST {\n set z {x [if {$q} {log w}] v}\n}\n",
7934 "foo.irule",
7935 |metric| {
7936 assert_eq!(metric.halstead.unique_operators(), 5);
7937 assert_eq!(metric.halstead.total_operators(), 7);
7938 assert_eq!(metric.halstead.unique_operands(), 6);
7939 assert_eq!(metric.halstead.total_operands(), 6);
7940 },
7941 );
7942 }
7943
7944 /// How one dialect of the Tcl family spells the braced-word
7945 /// construct. The two grammars are deliberate clones with different
7946 /// id blocks, so #1354's guard is derived once and instantiated
7947 /// twice — and a fix that landed in only one dialect fails the
7948 /// second instantiation rather than going unnoticed.
7949 struct BracedWordKinds {
7950 /// `braced_word_simple`, the literal *value* form the guard
7951 /// keys on.
7952 wrapper: u16,
7953 /// `braced_word`, the *script* form #1354 gated on holding a
7954 /// command: an operand only when it holds none.
7955 script_body: u16,
7956 /// `comment`, the one named child of a script that is not a
7957 /// command and that the gate must not mistake for one.
7958 comment: u16,
7959 /// Every named kind node-types.json admits directly inside
7960 /// `wrapper`. A child outside this set means the grammar moved
7961 /// and the parent-keyed arm has to be re-derived
7962 /// (grammar-dispatch §1).
7963 children: [u16; 6],
7964 /// The `{` / `}` the wrapper also holds. Suppressing the opener
7965 /// was the whole of #1314's narrower guard; the closer has
7966 /// never been classified, since `get_operator_id_as_str` folds
7967 /// the pair to one `{}` glyph.
7968 delimiters: [u16; 2],
7969 /// Of `children`, the three the operand arm classified
7970 /// unconditionally before the guard.
7971 operand_children: [u16; 3],
7972 /// `quoted_word`, which was an operand only when *inert* —
7973 /// `string_operand_type`'s own rule, replicated here so the
7974 /// shed count below is what `N2` actually shed.
7975 quoted_word: u16,
7976 /// The interpolation kinds that decide that.
7977 interpolation: [u16; 2],
7978 }
7979
7980 #[cfg(feature = "tcl")]
7981 const TCL_BRACED_WORD_KINDS: BracedWordKinds = BracedWordKinds {
7982 wrapper: Tcl::BracedWordSimple as u16,
7983 script_body: Tcl::BracedWord as u16,
7984 comment: Tcl::Comment as u16,
7985 children: [
7986 Tcl::SimpleWord as u16,
7987 Tcl::EscapedCharacter as u16,
7988 Tcl::QuotedWord as u16,
7989 Tcl::VariableSubstitution as u16,
7990 Tcl::CommandSubstitution as u16,
7991 Tcl::BracedWordSimple as u16,
7992 ],
7993 delimiters: [Tcl::LBRACE as u16, Tcl::RBRACE as u16],
7994 operand_children: [
7995 Tcl::SimpleWord as u16,
7996 Tcl::VariableSubstitution as u16,
7997 Tcl::BracedWordSimple as u16,
7998 ],
7999 quoted_word: Tcl::QuotedWord as u16,
8000 interpolation: [
8001 Tcl::VariableSubstitution as u16,
8002 Tcl::CommandSubstitution as u16,
8003 ],
8004 };
8005
8006 #[cfg(feature = "irules")]
8007 const IRULES_BRACED_WORD_KINDS: BracedWordKinds = BracedWordKinds {
8008 wrapper: Irules::BracedWordSimple as u16,
8009 script_body: Irules::BracedWord as u16,
8010 comment: Irules::Comment as u16,
8011 children: [
8012 Irules::SimpleWord as u16,
8013 Irules::EscapedCharacter as u16,
8014 Irules::QuotedWord as u16,
8015 Irules::VariableSubstitution as u16,
8016 Irules::CommandSubstitution as u16,
8017 Irules::BracedWordSimple as u16,
8018 ],
8019 delimiters: [Irules::LBRACE as u16, Irules::RBRACE as u16],
8020 operand_children: [
8021 Irules::SimpleWord as u16,
8022 Irules::VariableSubstitution as u16,
8023 Irules::BracedWordSimple as u16,
8024 ],
8025 quoted_word: Irules::QuotedWord as u16,
8026 interpolation: [
8027 Irules::VariableSubstitution as u16,
8028 Irules::CommandSubstitution as u16,
8029 ],
8030 };
8031
8032 /// The operand occurrences #1354 removed from `source`, as their
8033 /// source texts, plus the set of `braced_word_simple` child kinds
8034 /// the fixture witnessed.
8035 ///
8036 /// Walks with `for_each_node_with_chain`, which maintains the
8037 /// ancestor chain exactly as `spaces::compute` does, so "parent"
8038 /// here means what `Ancestors::parent` means inside the guard.
8039 /// Doubles as the grammar-dispatch §1 / §2 drift marker: a child of
8040 /// the wrapper outside `children` ∪ `delimiters` fails on the spot,
8041 /// which is what makes keying the arm on the parent alone — rather
8042 /// than on an enumerated child list — safe to rely on.
8043 #[cfg(any(feature = "irules", feature = "tcl"))]
8044 fn braced_word_shed<L: crate::LanguageInfo>(
8045 source: &str,
8046 kinds: &BracedWordKinds,
8047 ) -> (Vec<String>, HashSet<u16>) {
8048 let code = source.as_bytes();
8049 let mut shed = Vec::new();
8050 let mut witnessed = HashSet::new();
8051 for_each_node_with_chain::<L>(code, |node, chain| {
8052 let text = || {
8053 node.utf8_text(code)
8054 .expect("fixture is valid UTF-8")
8055 .to_owned()
8056 };
8057 // The script form was an operand of its own wherever it
8058 // appeared until #1354, which now gates it on holding a
8059 // command — a named child that is not a comment. This is not
8060 // a child of the wrapper, so it is counted before the parent
8061 // test below.
8062 if node.kind_id() == kinds.script_body
8063 && node
8064 .children()
8065 .any(|child| child.is_named() && child.kind_id() != kinds.comment)
8066 {
8067 shed.push(text());
8068 }
8069 if chain.last().is_none_or(|p| p.kind_id() != kinds.wrapper) {
8070 return;
8071 }
8072 assert!(
8073 kinds.children.contains(&node.kind_id())
8074 || kinds.delimiters.contains(&node.kind_id()),
8075 "`{source}`: a `{}` inside a braced word is a child this guard \
8076 was not derived against; re-read node-types.json before \
8077 trusting it",
8078 node.kind(),
8079 );
8080 if kinds.children.contains(&node.kind_id()) {
8081 witnessed.insert(node.kind_id());
8082 }
8083 let was_operand = kinds.operand_children.contains(&node.kind_id())
8084 || (node.kind_id() == kinds.quoted_word && !node.wraps_any(&kinds.interpolation));
8085 if was_operand {
8086 shed.push(text());
8087 }
8088 });
8089 (shed, witnessed)
8090 }
8091
8092 /// One row of the #1354 tables: a fixture, what it measures now,
8093 /// what it measured before, and the operand texts behind the counts.
8094 struct BracedWordCase {
8095 source: &'static str,
8096 /// `[n1, N1, n2, N2]` with the guard in place.
8097 counts: [u64; 4],
8098 /// `[n2, N2]` without it. Re-derived by the loop rather than
8099 /// trusted, so a stale row fails instead of misinforming.
8100 before: [u64; 2],
8101 operands: &'static [&'static str],
8102 }
8103
8104 /// Every row is measured in *both* dialects, so a fix applied to
8105 /// one getter and not its clone fails here. `braced_word_shed`'s
8106 /// drift assertion likewise runs against both grammars.
8107 #[cfg(any(feature = "irules", feature = "tcl"))]
8108 fn check_braced_word_cases<T: crate::MetricSuite, L: crate::LanguageInfo>(
8109 cases: &[BracedWordCase],
8110 file: &str,
8111 kinds: &BracedWordKinds,
8112 ) -> HashSet<u16> {
8113 let mut witnessed = HashSet::new();
8114 for case in cases {
8115 let (shed, seen) = braced_word_shed::<L>(case.source, kinds);
8116 witnessed.extend(seen);
8117
8118 // Phrased as an addition rather than a subtraction so a
8119 // future edit that inverts the two underflows nothing.
8120 assert_eq!(
8121 case.before[1],
8122 case.counts[3] + shed.len() as u64,
8123 "{file} `{}`: N2 must shed exactly one occurrence per \
8124 previously-billed part; recorded {}, parts {shed:?}",
8125 case.source,
8126 case.before[1],
8127 );
8128 let mut vocabulary: HashSet<&str> = case.operands.iter().copied().collect();
8129 vocabulary.extend(shed.iter().map(String::as_str));
8130 assert_eq!(
8131 vocabulary.len() as u64,
8132 case.before[0],
8133 "{file} `{}`: n2 before the fix is the post-fix vocabulary \
8134 plus those parts; got {vocabulary:?}",
8135 case.source,
8136 );
8137
8138 assert_halstead_counts::<T>(case.source, file, case.counts, case.source);
8139 assert_ops_operands::<T>(
8140 case.source,
8141 file,
8142 case.operands.len(),
8143 case.operands.to_vec(),
8144 );
8145 }
8146 witnessed
8147 }
8148
8149 /// The rows shared by both dialects: one per named child kind
8150 /// `braced_word_simple` admits, the childless spelling, the
8151 /// repeated-value row that separates `n2` from `N2` (#1294), and
8152 /// the braced/quoted parity pair #1317 asks for.
8153 #[cfg(any(feature = "irules", feature = "tcl"))]
8154 const BRACED_WORD_CASES: [BracedWordCase; 11] = [
8155 // simple_word, the reported fixture. Two words inside one
8156 // value scored two operands beside the value itself.
8157 BracedWordCase {
8158 source: "set x {literal here}\n",
8159 counts: [1, 1, 2, 2],
8160 before: [4, 4],
8161 operands: &["x", "{literal here}"],
8162 },
8163 // The childless spelling, and the reason the wrapper is kept
8164 // rather than dropped in favour of its contents
8165 // (grammar-dispatch §6): with nothing inside, the wrapper is
8166 // the empty string's only carrier. Nothing is shed here, so
8167 // this row asserts the two columns agree.
8168 BracedWordCase {
8169 source: "set y {}\n",
8170 counts: [1, 1, 2, 2],
8171 before: [2, 2],
8172 operands: &["y", "{}"],
8173 },
8174 // braced_word_simple inside braced_word_simple: the nesting
8175 // that makes the over-count unbounded in depth. One value
8176 // spelled six vocabulary entries.
8177 BracedWordCase {
8178 source: "set a {x {y z}}\n",
8179 counts: [1, 1, 2, 2],
8180 before: [6, 6],
8181 operands: &["a", "{x {y z}}"],
8182 },
8183 // quoted_word, inert — an operand in its own right elsewhere,
8184 // and shed here.
8185 BracedWordCase {
8186 source: "set a {x \"q w\" v}\n",
8187 counts: [1, 1, 2, 2],
8188 before: [5, 5],
8189 operands: &["a", "{x \"q w\" v}"],
8190 },
8191 // quoted_word carrying an interpolation, which was *not* an
8192 // operand before the guard either (`string_operand_type` had
8193 // already suppressed it) — so only `x` and `v` are shed. Its
8194 // `$q` is a grandchild of the braced word and still counts,
8195 // which is the parent-scoping this arm inherits from #1314.
8196 BracedWordCase {
8197 source: "set a {x \"$q\" v}\n",
8198 counts: [1, 1, 3, 3],
8199 before: [5, 5],
8200 operands: &["a", "$q", "{x \"$q\" v}"],
8201 },
8202 // escaped_character, never classified — so it sheds nothing
8203 // and the row measures the two `simple_word`s around it.
8204 BracedWordCase {
8205 source: "set a {x \\n y}\n",
8206 counts: [1, 1, 2, 2],
8207 before: [4, 4],
8208 operands: &["a", "{x \\n y}"],
8209 },
8210 // variable_substitution. Tcl substitutes nothing between
8211 // braces, so `{$x}` is the two-character string `$x` — the row
8212 // that makes "the wrapper is the value" more than a tie-break.
8213 BracedWordCase {
8214 source: "set a {$x}\n",
8215 counts: [1, 1, 2, 2],
8216 before: [3, 3],
8217 operands: &["a", "{$x}"],
8218 },
8219 // command_substitution, likewise never an operand itself. Its
8220 // interior is, and stays so: `$q`, `puts` and `w` are
8221 // grandchildren. The nested `{puts w}` is a *script* body and
8222 // sheds under the other half of #1354.
8223 BracedWordCase {
8224 source: "set z {x [if {$q} {puts w}] v}\n",
8225 counts: [4, 5, 5, 5],
8226 before: [8, 8],
8227 operands: &["z", "$q", "puts", "w", "{x [if {$q} {puts w}] v}"],
8228 },
8229 // The same value twice: n2 3 against N2 4, so a row that
8230 // asserted only the vocabulary could not tell the two axes
8231 // apart (#1294).
8232 BracedWordCase {
8233 source: "set a {b c}\nset d {b c}\n",
8234 counts: [1, 2, 3, 4],
8235 before: [5, 8],
8236 operands: &["a", "d", "{b c}"],
8237 },
8238 // The parity pair #1317 named: two spellings of one literal
8239 // value must score alike. They did not before — braced 4 / 4
8240 // against quoted 2 / 2 — which is the spelling sensitivity
8241 // #695, #1312 and #1314 each removed elsewhere.
8242 BracedWordCase {
8243 source: "set a {one two}\n",
8244 counts: [1, 1, 2, 2],
8245 before: [4, 4],
8246 operands: &["a", "{one two}"],
8247 },
8248 BracedWordCase {
8249 source: "set a \"one two\"\n",
8250 counts: [1, 1, 2, 2],
8251 before: [2, 2],
8252 operands: &["a", "\"one two\""],
8253 },
8254 ];
8255
8256 /// The script half of #1354, shared by both dialects: a
8257 /// `braced_word` holding commands is a block whose contents the
8258 /// walk already counts, so it is no longer also an operand
8259 /// spanning the whole block.
8260 ///
8261 /// The three childless rows are the gate, and the reason this is a
8262 /// gate and not a deletion (grammar-dispatch §6). `braced_word` is
8263 /// not only the script kind: it is the value slot of every command
8264 /// the grammar does not special-case, where `lappend l {}` is an
8265 /// empty list whose brace pair is its only carrier. Deleting the
8266 /// kind scored that zero while its `lappend l ""` synonym — the
8267 /// last row, the control — scored one. An empty `proc` body is
8268 /// spelled identically and so also keeps an operand; #1318 tells
8269 /// the two roles apart by the enclosing command, but it bills them
8270 /// alike, so this gate is still what keeps the operand.
8271 ///
8272 /// The rows' *operator* columns moved with #1318 wherever the
8273 /// enclosing command takes a value, which is why the `lappend`
8274 /// rows here read `0` where the `proc` and `if` rows read the
8275 /// block's `{}`. The operand columns are untouched by it — #1318
8276 /// revises the brace and nothing else — so every `before` here
8277 /// still describes #1354 alone.
8278 #[cfg(any(feature = "irules", feature = "tcl"))]
8279 const SCRIPT_BODY_CASES: [BracedWordCase; 9] = [
8280 BracedWordCase {
8281 source: "proc p {} { set b 1 }\n",
8282 counts: [3, 4, 3, 3],
8283 before: [4, 4],
8284 operands: &["p", "b", "1"],
8285 },
8286 BracedWordCase {
8287 source: "proc p {} {}\n",
8288 counts: [2, 3, 2, 2],
8289 before: [2, 2],
8290 operands: &["p", "{}"],
8291 },
8292 // A comment is a named child of the script but not a command,
8293 // and no arm bills it, so a comment-only body scores like the
8294 // empty one above rather than like nothing: the block's whole
8295 // text is its one operand. Gating on "any named child" billed
8296 // it zero — adding a comment to an empty block lowered N2.
8297 BracedWordCase {
8298 source: "proc p {} {\n # only a comment\n}\n",
8299 counts: [2, 3, 2, 2],
8300 before: [2, 2],
8301 operands: &["p", "{\n # only a comment\n}"],
8302 },
8303 BracedWordCase {
8304 source: "if {$q} {\n # noop\n}\n",
8305 counts: [2, 3, 2, 2],
8306 before: [2, 2],
8307 operands: &["$q", "{\n # noop\n}"],
8308 },
8309 // The control: a comment *beside* a command changes nothing, so a
8310 // gate keyed on "contains a comment" would fail this row.
8311 BracedWordCase {
8312 source: "proc p {} {\n # c\n set b 1\n}\n",
8313 counts: [3, 4, 3, 3],
8314 before: [4, 4],
8315 operands: &["p", "b", "1"],
8316 },
8317 // The value-role twin: inside a literal string Tcl performs no
8318 // substitution, so `# x` is not a comment at all, and the word
8319 // is its one operand exactly as `lappend l {}` below is. Its
8320 // operator columns are zero since #1318 — `lappend` takes a
8321 // value, so the brace pair is a quote and not a block.
8322 BracedWordCase {
8323 source: "lappend l {\n # x\n}\n",
8324 counts: [0, 0, 3, 3],
8325 before: [3, 3],
8326 operands: &["lappend", "l", "{\n # x\n}"],
8327 },
8328 // A `braced_word` in *value* position — the #1318 role, where
8329 // the same kind carries a literal list. Its interior words
8330 // counted before #1354 and still do; what #1354 removed is the
8331 // whole-block operand that was double-billing them, and what
8332 // #1318 removed is the `{}` operator (`lappend` takes a value,
8333 // so the brace pair quotes rather than opens).
8334 BracedWordCase {
8335 source: "lappend l {a b}\n",
8336 counts: [0, 0, 4, 4],
8337 before: [5, 5],
8338 operands: &["lappend", "l", "a", "b"],
8339 },
8340 // …and the childless spelling of that value, which sheds
8341 // nothing: the brace pair is the empty list's only carrier.
8342 BracedWordCase {
8343 source: "lappend l {}\nreturn {}\n",
8344 counts: [0, 0, 4, 5],
8345 before: [4, 5],
8346 operands: &["lappend", "l", "return", "{}"],
8347 },
8348 // The control the row above is measured against: the quoted
8349 // spelling of the same empty value, which never depended on
8350 // the arm and must keep scoring one operand. Since #1318 the
8351 // two rows agree on all four columns rather than only on the
8352 // operand pair — an empty list is an empty list however it is
8353 // spelled.
8354 BracedWordCase {
8355 source: "lappend l \"\"\nreturn \"\"\n",
8356 counts: [0, 0, 4, 5],
8357 before: [4, 5],
8358 operands: &["lappend", "l", "return", "\"\""],
8359 },
8360 ];
8361
8362 /// The table must exercise every named child kind the grammar
8363 /// admits inside a braced word, and no other — the other half of
8364 /// the drift marker in `braced_word_shed`, which can only police
8365 /// kinds a fixture actually produces.
8366 #[cfg(any(feature = "irules", feature = "tcl"))]
8367 fn assert_braced_word_children_witnessed(
8368 witnessed: &HashSet<u16>,
8369 kinds: &BracedWordKinds,
8370 dialect: &str,
8371 ) {
8372 let mut got: Vec<u16> = witnessed.iter().copied().collect();
8373 got.sort_unstable();
8374 let mut expected = kinds.children;
8375 expected.sort_unstable();
8376 assert_eq!(
8377 got.as_slice(),
8378 expected.as_slice(),
8379 "{dialect}: the fixtures must witness every child kind \
8380 node-types.json admits inside a braced word",
8381 );
8382 }
8383
8384 /// Regression for #1354 and #1317. `braced_word_simple` is a
8385 /// literal value and `braced_word` a script, and both were
8386 /// operands *beside* the content the walk counts anyway: a braced
8387 /// word was billed once per inner word plus once for itself, and a
8388 /// block once for every command in it plus once for its whole
8389 /// text. `set x {literal here}` scored n2 4 / N2 4 for one value.
8390 /// The name is the invariant either way — the value's content is
8391 /// the literal, the script's is its commands, and each is billed
8392 /// once.
8393 ///
8394 /// Each row's `before` column is re-derived by the loop from the
8395 /// current parse rather than trusted. That derivation is a replica
8396 /// of the pre-#1354 arms and could in principle drift from what
8397 /// they did, so every column was also measured directly against a
8398 /// build of the old getters — `lappend l {a b}` at n2 5 / N2 5,
8399 /// `lappend l {}` at 4 / 5, `proc p {} {}` at 2 / 2 — rather than
8400 /// derived only from the model here.
8401 ///
8402 /// The same walk doubles as the drift marker for the parent-keyed
8403 /// arm: it fails if the grammar ever puts a seventh kind directly
8404 /// inside a braced word, and the union assertion below fails if a
8405 /// bump stops emitting one of the six.
8406 #[cfg(feature = "tcl")]
8407 #[test]
8408 fn tcl_braced_word_bills_its_content_once_1354() {
8409 let mut witnessed = check_braced_word_cases::<TclParser, TclCode>(
8410 &BRACED_WORD_CASES,
8411 "foo.tcl",
8412 &TCL_BRACED_WORD_KINDS,
8413 );
8414 witnessed.extend(check_braced_word_cases::<TclParser, TclCode>(
8415 &SCRIPT_BODY_CASES,
8416 "foo.tcl",
8417 &TCL_BRACED_WORD_KINDS,
8418 ));
8419 assert_braced_word_children_witnessed(&witnessed, &TCL_BRACED_WORD_KINDS, "tcl");
8420 }
8421
8422 /// The iRules twin. The tables are shared, so a fix that reached
8423 /// only `big-code-analysis-ast/src/getter/tcl.rs` fails every row
8424 /// here — the two getters are deliberate clones and #1354 names
8425 /// both.
8426 #[cfg(feature = "irules")]
8427 #[test]
8428 fn irules_braced_word_bills_its_content_once_1354() {
8429 let mut witnessed = check_braced_word_cases::<IrulesParser, IrulesCode>(
8430 &BRACED_WORD_CASES,
8431 "foo.irule",
8432 &IRULES_BRACED_WORD_KINDS,
8433 );
8434 witnessed.extend(check_braced_word_cases::<IrulesParser, IrulesCode>(
8435 &SCRIPT_BODY_CASES,
8436 "foo.irule",
8437 &IRULES_BRACED_WORD_KINDS,
8438 ));
8439 // The `when` handler body is the iRules-only spelling of a
8440 // script body, and the largest instance of the defect: its
8441 // operand text was the entire event handler.
8442 let handlers: [BracedWordCase; 3] = [
8443 BracedWordCase {
8444 source: "when HTTP_REQUEST { set x 1 }\n",
8445 counts: [3, 3, 3, 3],
8446 before: [4, 4],
8447 operands: &["HTTP_REQUEST", "x", "1"],
8448 },
8449 BracedWordCase {
8450 source: "when HTTP_REQUEST {}\n",
8451 counts: [2, 2, 2, 2],
8452 before: [2, 2],
8453 operands: &["HTTP_REQUEST", "{}"],
8454 },
8455 // The comment-only twin of the empty handler above; see the
8456 // shared table for why it scores like it.
8457 BracedWordCase {
8458 source: "when HTTP_REQUEST {\n # only a comment\n}\n",
8459 counts: [2, 2, 2, 2],
8460 before: [2, 2],
8461 operands: &["HTTP_REQUEST", "{\n # only a comment\n}"],
8462 },
8463 ];
8464 witnessed.extend(check_braced_word_cases::<IrulesParser, IrulesCode>(
8465 &handlers,
8466 "foo.irule",
8467 &IRULES_BRACED_WORD_KINDS,
8468 ));
8469 assert_braced_word_children_witnessed(&witnessed, &IRULES_BRACED_WORD_KINDS, "irules");
8470 }
8471
8472 /// One row of the #1318 tables: a fixture and the four Halstead
8473 /// columns plus the operand vocabulary behind them.
8474 ///
8475 /// No `before` column, unlike [`BracedWordCase`]: #1318 is not a
8476 /// guard whose absence a walk can model, it is a *classification*.
8477 /// The pre-fix counts are recorded per row in prose instead, and
8478 /// each was measured against a build without the
8479 /// `get_op_type_with_code` overrides.
8480 struct BracedWordValueCase {
8481 source: &'static str,
8482 /// `[n1, N1, n2, N2]`.
8483 counts: [u64; 4],
8484 operands: &'static [&'static str],
8485 }
8486
8487 /// Runs one #1318 table against one dialect. Both dialects run
8488 /// every shared row, so a fix that reached one getter and not its
8489 /// clone fails here.
8490 #[cfg(any(feature = "irules", feature = "tcl"))]
8491 fn check_braced_word_value_cases<T: crate::MetricSuite>(
8492 cases: &[BracedWordValueCase],
8493 file: &str,
8494 ) {
8495 for case in cases {
8496 assert_halstead_counts::<T>(case.source, file, case.counts, case.source);
8497 assert_ops_operands::<T>(
8498 case.source,
8499 file,
8500 case.operands.len(),
8501 case.operands.to_vec(),
8502 );
8503 }
8504 }
8505
8506 /// The #1318 rows both dialects share. Every independent path
8507 /// through the role test gets a row of its own
8508 /// (`.claude/rules/testing.md`, "give one input to each independent
8509 /// path"): the value default, the script-command list, a modelled
8510 /// construct reached through a `word_list`, the two modelled slots
8511 /// that hold values, an unresolvable command name, and both
8512 /// nesting directions.
8513 ///
8514 /// **Every row asserts the operand columns too, and they are the
8515 /// half that must not move.** #1318 revises one operator and
8516 /// nothing else, so a mutant that suppressed a value word's
8517 /// *contents* — the tidier-looking rule, which collapses an
8518 /// `oo::class create C {…}` body into a single operand — fails
8519 /// here rather than passing as an improvement. #1382 keeps to the
8520 /// same discipline: it withdraws further `{}` operators and moves
8521 /// no operand column in this table.
8522 ///
8523 /// The trailing rows are #1382's. The first four are two *pairs*:
8524 /// a braced value slot beside the bare spelling of the same
8525 /// argument, which must score alike. Before #1382 they did not —
8526 /// the braced spelling billed one extra `{}`. The four after them
8527 /// carry no bare partner: each braces a value with a space in it
8528 /// (`proc {my proc}`, `{a b}`, `{code msg}`), which is the whole
8529 /// reason the braces are there. Their pre-#1382 counts are
8530 /// recorded per row instead.
8531 #[cfg(any(feature = "irules", feature = "tcl"))]
8532 const BRACED_WORD_VALUE_CASES: [BracedWordValueCase; 24] = [
8533 // The rule reaches the opener and *only* the opener. A `;`
8534 // separating two commands is a direct child of the
8535 // `braced_word` — `_terminator` is a hidden rule, so it is
8536 // inlined rather than wrapped — and both dialects classify
8537 // `SEMI` as an operator, so a revision keyed on the parent
8538 // kind alone swallowed it too: this row read n1 0 / N1 0 and
8539 // `halstead.effort` 0.0, against a line that is two commands.
8540 // The pairing with the `eval` row below is the point — same
8541 // brace, same separator, one command name apart.
8542 BracedWordValueCase {
8543 source: "lappend l {puts a ; puts b}\n",
8544 counts: [1, 1, 5, 6],
8545 operands: &["lappend", "l", "puts", "a", "b"],
8546 },
8547 BracedWordValueCase {
8548 source: "eval {puts a ; puts b}\n",
8549 counts: [2, 2, 4, 5],
8550 operands: &["eval", "puts", "a", "b"],
8551 },
8552 // The reported fixture. `lappend` takes a list, so `{a b}`
8553 // quotes rather than opens: no operator. Before #1318 this
8554 // read n1 1 / N1 1 — a `{}` block the line does not contain.
8555 BracedWordValueCase {
8556 source: "lappend l {a b}\n",
8557 counts: [0, 0, 4, 4],
8558 operands: &["lappend", "l", "a", "b"],
8559 },
8560 // The issue's second line, and the one showing the defect
8561 // needs no second argument to appear.
8562 BracedWordValueCase {
8563 source: "puts {c d}\n",
8564 counts: [0, 0, 3, 3],
8565 operands: &["puts", "c", "d"],
8566 },
8567 // The headline: one value, two commands, and until #1318 two
8568 // different operator counts — `set` reported none, `lappend` a
8569 // `{}`. `set` is an operator and `lappend` an operand because
8570 // the grammar models one command and not the other; that
8571 // asymmetry is the grammar's, not this rule's.
8572 BracedWordValueCase {
8573 source: "set x {a b}\nlappend y {a b}\n",
8574 counts: [1, 1, 6, 6],
8575 operands: &["lappend", "x", "y", "{a b}", "a", "b"],
8576 },
8577 // The script side of the same shape, and the reason `LBRACE`
8578 // is gated rather than deleted (grammar-dispatch §6): `eval`
8579 // evaluates its argument, so the block is real and keeps its
8580 // `{}`.
8581 BracedWordValueCase {
8582 source: "eval {puts hi}\n",
8583 counts: [1, 1, 3, 3],
8584 operands: &["eval", "puts", "hi"],
8585 },
8586 // The same command named through the global namespace, which
8587 // is the spelling a `namespace eval` body uses to guarantee the
8588 // core command over a local proc shadowing it. `::eval` *is*
8589 // `eval`, so the two rows must agree on the operator column;
8590 // before the leading `::` was stripped this one fell to the
8591 // value default and lost its block.
8592 BracedWordValueCase {
8593 source: "::eval {puts hi}\n",
8594 counts: [1, 1, 3, 3],
8595 operands: &["::eval", "puts", "hi"],
8596 },
8597 // The control for that strip: only the *leading* qualifier is
8598 // a namespace path to the core command. `ns::eval` is a
8599 // different proc living in `ns`, so it takes the value default
8600 // like any other unrecognised name — a strip of every `::`
8601 // segment would have promoted it to a script.
8602 BracedWordValueCase {
8603 source: "ns::eval {a b}\n",
8604 counts: [0, 0, 3, 3],
8605 operands: &["ns::eval", "a", "b"],
8606 },
8607 // The default for an unrecognised command — a user proc here,
8608 // but equally any package command. Nothing distinguishes it
8609 // from `lappend` structurally, so the row exists to pin which
8610 // way the coin lands. Note the words inside are still
8611 // operands: the rule withdraws the block, never the code.
8612 BracedWordValueCase {
8613 source: "myproc {g h}\n",
8614 counts: [0, 0, 3, 3],
8615 operands: &["myproc", "g", "h"],
8616 },
8617 // `namespace` is the one construct either grammar models that
8618 // still reaches its body through a `word_list`, so it is the
8619 // row that fails if the rule stops at the parent instead of
8620 // reading the grandparent. Its body must stay a script.
8621 BracedWordValueCase {
8622 source: "namespace eval ns {\n set a 1\n}\n",
8623 counts: [3, 3, 4, 4],
8624 operands: &["eval", "ns", "a", "1"],
8625 },
8626 // A literal nested in a literal: neither brace opens a block,
8627 // and both words survive.
8628 BracedWordValueCase {
8629 source: "puts {a {b c}}\n",
8630 counts: [0, 0, 4, 4],
8631 operands: &["puts", "a", "b", "c"],
8632 },
8633 // A `set` inside a literal. The `set` keyword and the `[]`
8634 // substitution are real operators of a command the walk
8635 // descends into, and they stay — the rule reaches exactly the
8636 // one brace whose parent it can classify, never a subtree.
8637 // `{x [foo] v}` is a `braced_word_simple`, so #1354 bills it
8638 // whole and its `[foo]` interior separately, unchanged.
8639 BracedWordValueCase {
8640 source: "lappend l {set a {x [foo] v}}\n",
8641 counts: [2, 2, 5, 5],
8642 operands: &["lappend", "l", "a", "{x [foo] v}", "foo"],
8643 },
8644 // A braced word in the command *name* position. Legal Tcl —
8645 // `{puts} hi` invokes `puts` — and the one generic position
8646 // that is not an argument, so it is answered before the
8647 // command lookup. It must also never reach the `switch`-arm
8648 // rescue, which is what the braced-pattern row in the Tcl
8649 // table checks.
8650 BracedWordValueCase {
8651 source: "{puts} hi\n",
8652 counts: [0, 0, 2, 2],
8653 operands: &["puts", "hi"],
8654 },
8655 // A defaulted `proc` parameter — the other modelled slot that
8656 // holds a value, decided by the parent kind rather than by a
8657 // command name. `{x y}` is data the interpreter assigns, never
8658 // a script it runs, and it billed a fourth `{}` before #1318.
8659 // The three that remain are the parameter list, the parameter
8660 // spec and the body: those two list delimiters group a
8661 // declaration rather than quote a value, the line #1354 drew
8662 // for `Arguments`.
8663 BracedWordValueCase {
8664 source: "proc p {a {b {x y}}} { puts $a }\n",
8665 counts: [2, 4, 7, 7],
8666 operands: &["p", "a", "b", "x", "y", "puts", "$a"],
8667 },
8668 // A literal nested in a script, the direction that must keep
8669 // working: the `proc` body is a block (`{}` twice with the
8670 // parameter list) and the list it appends is not.
8671 BracedWordValueCase {
8672 source: "proc p {} { lappend l {a b} }\n",
8673 counts: [2, 3, 5, 5],
8674 operands: &["p", "lappend", "l", "a", "b"],
8675 },
8676 // A computed command name resolves to nothing, so the value
8677 // default decides. `$c` is the reference's own operand.
8678 BracedWordValueCase {
8679 source: "set c puts\n$c {a b}\n",
8680 counts: [1, 1, 5, 5],
8681 operands: &["c", "puts", "$c", "a", "b"],
8682 },
8683 // #1382's headline, and the pair that names the defect: `after`
8684 // is a script-taking command, but only from its *second*
8685 // argument — the first is a millisecond count. Both spellings
8686 // of that count are one operand and no operator; before #1382
8687 // the braced one billed a `{}` for a block the line does not
8688 // contain, reading N1 2 against the bare form's 1. The `{}`
8689 // both rows do report is the `{puts hi}` script, which is real.
8690 BracedWordValueCase {
8691 source: "after {100} {puts hi}\n",
8692 counts: [1, 1, 4, 4],
8693 operands: &["after", "100", "puts", "hi"],
8694 },
8695 BracedWordValueCase {
8696 source: "after 100 {puts hi}\n",
8697 counts: [1, 1, 4, 4],
8698 operands: &["after", "100", "puts", "hi"],
8699 },
8700 // The same pair for `time script ?count?`, whose value slot is
8701 // the *last* rather than the first — so a rule that hard-coded
8702 // "argument 0 is the value" passes the `after` pair and fails
8703 // this one. The script argument keeps its `{}` in both rows,
8704 // which is what makes the operator column a 1 and not a 0.
8705 //
8706 // The *bare* row is the half that catches it, which is not the
8707 // half it looks like. Measured by giving `time` `after`'s slot
8708 // rule: `time {puts hi} 3` reads n1 0 / N1 0, its one block
8709 // demoted to a value — while the braced row below is unmoved
8710 // at [1, 1, 4, 4], because `{puts hi}` loses the `{}` that
8711 // `{3}` gains and the operand texts are the same either way.
8712 // Do not trim the bare spelling as redundant with the braced
8713 // one; here it is the discriminating row of the two.
8714 BracedWordValueCase {
8715 source: "time {puts hi} {3}\n",
8716 counts: [1, 1, 4, 4],
8717 operands: &["time", "puts", "hi", "3"],
8718 },
8719 BracedWordValueCase {
8720 source: "time {puts hi} 3\n",
8721 counts: [1, 1, 4, 4],
8722 operands: &["time", "puts", "hi", "3"],
8723 },
8724 // A braced `proc` *name*, the one value slot the grammar names
8725 // as a field rather than by position. `{my proc}` is an
8726 // identifier containing a space, not a body, and billed a
8727 // fourth `{}` before #1382. The three operators left are the
8728 // `proc` keyword and the two braces of the parameter list and
8729 // the body — and note the interior words survive, `proc` being
8730 // an operand here and an operator one column over.
8731 BracedWordValueCase {
8732 source: "proc {my proc} {} {}\n",
8733 counts: [2, 3, 3, 3],
8734 operands: &["my", "proc", "{}"],
8735 },
8736 // The two `namespace` subcommands that take values throughout,
8737 // both of them Tcl 8.6 standard-library spellings. Their braced
8738 // argument is a pattern list and a dictionary; neither is
8739 // evaluated, and each billed a `{}` before #1382 — n1 2 / N1 2
8740 // against the 1 / 1 here. The `namespace` keyword is the
8741 // operator that remains, which is what keeps these two rows
8742 // from reading `effort` 0.0.
8743 BracedWordValueCase {
8744 source: "namespace export {a b}\n",
8745 counts: [1, 1, 3, 3],
8746 operands: &["export", "a", "b"],
8747 },
8748 BracedWordValueCase {
8749 source: "namespace ensemble create -map {a b}\n",
8750 counts: [1, 1, 5, 5],
8751 operands: &["ensemble", "create", "-map", "a", "b"],
8752 },
8753 // A `trap` clause written outside a `try`, which is how both
8754 // grammars reach the generic-command path for it. This is the
8755 // `ScriptSlots::LastOf` row — the pattern and the variable list
8756 // are values and the handler is the script — and the only one
8757 // whose slot is counted from the *end* of the list, so a rule
8758 // that read `Only(2)` from the front would need the arity to
8759 // pass it. Before #1382 all three braces billed: N1 3.
8760 BracedWordValueCase {
8761 source: "trap {POSIX} {code msg} {puts hi}\n",
8762 counts: [1, 1, 6, 6],
8763 operands: &["trap", "POSIX", "code", "msg", "puts", "hi"],
8764 },
8765 ];
8766
8767 /// Regression for #1318. `braced_word` carries both a block and a
8768 /// plain literal, and the grammars spell the two identically, so
8769 /// #1314's kind-scoped guard reached only the value slots they
8770 /// special-case: `set x {a b}` reported no operator while
8771 /// `lappend x {a b}` reported a `{}` for a block the line does not
8772 /// contain. The role is recognised out-of-band, by the enclosing
8773 /// command's leading word (grammar-dispatch §9).
8774 ///
8775 /// The Tcl-only rows are the constructs this grammar models with
8776 /// no node of its own (#467, #1264) — `switch` and `for` — plus the
8777 /// braced `switch` *pattern* that must not be mistaken for an arm
8778 /// body, in both the one-arm-per-line and the one-line layouts.
8779 /// Their iRules counterparts are dedicated nodes and are covered by
8780 /// the sibling test.
8781 #[cfg(feature = "tcl")]
8782 #[test]
8783 fn tcl_braced_word_role_follows_the_enclosing_command_1318() {
8784 check_braced_word_value_cases::<TclParser>(&BRACED_WORD_VALUE_CASES, "foo.tcl");
8785 let tcl_only: [BracedWordValueCase; 8] = [
8786 // `switch` arm bodies. The grammar flattens `pat body pat
8787 // body` into a `command` named after the first pattern, so
8788 // the bodies read as arguments of a command called `a` —
8789 // the shape the value default would call a literal. The
8790 // arm list's own `switch` name is what rescues them, and
8791 // `puts` occurring in both bodies is what separates n2
8792 // from N2.
8793 BracedWordValueCase {
8794 source: "switch $v { a { puts A } b { puts B } }\n",
8795 counts: [1, 3, 7, 8],
8796 operands: &["switch", "$v", "a", "b", "puts", "A", "B"],
8797 },
8798 // …and the limit of that rescue. A braced *pattern* is the
8799 // arm command's `name`, not an argument, so it is a
8800 // literal — `switch -regexp $v { {^a.*b$} {…} }` is
8801 // idiomatic and its regex must not be wrapped in a block.
8802 // Rescuing the whole arm command would read N1 4 here.
8803 // The leading `-exact` / `--` options are `simple_word`
8804 // operands of the *`switch`* command and shift nothing:
8805 // the arm list is found as the sole `braced_word` argument,
8806 // not by index (grammar-dispatch §9).
8807 BracedWordValueCase {
8808 source: "switch -exact -- $v {\n {a b} { puts X }\n default { puts Y }\n}\n",
8809 counts: [1, 3, 10, 11],
8810 operands: &[
8811 "switch", "-exact", "--", "$v", "a", "b", "puts", "X", "default", "Y",
8812 ],
8813 },
8814 // The same arms on one line. Only the *first* pattern is
8815 // the arm command's name; `{^b}` is its third argument, and
8816 // the name test alone rescued it as a body — N1 4 here,
8817 // against N1 3 for the identical arms written one per line.
8818 // The arm list is `pattern body pattern body …` by Tcl's
8819 // own definition, so the arguments alternate and the
8820 // even-indexed ones are the bodies.
8821 BracedWordValueCase {
8822 source: "switch -regexp $v { {^a} { puts A } {^b} { puts B } }\n",
8823 counts: [1, 3, 8, 9],
8824 operands: &["switch", "-regexp", "$v", "^a", "puts", "A", "^b", "B"],
8825 },
8826 // The words that can interpose between a pattern and its
8827 // body take one slot each and leave the parity intact: a
8828 // `-` fall-through body and a `default` pattern are both
8829 // `simple_word`s. The two braced bodies are at indexes 2
8830 // and 4 of the run and both keep their `{}`.
8831 BracedWordValueCase {
8832 source: "switch $v { a - b { puts AB } default { puts D } }\n",
8833 counts: [1, 3, 9, 10],
8834 operands: &["switch", "$v", "a", "-", "b", "puts", "AB", "default", "D"],
8835 },
8836 // `for` has four braced arguments and all four are
8837 // evaluated, so all four keep their braces. `i` appears as
8838 // the `set` target and again inside `incr`, and `$i` in
8839 // the condition and the body, which is what separates
8840 // n2 = 8 from N2 = 10. `<` is an *operand* here rather
8841 // than a comparison operator: with no `for` rule there is
8842 // no `expr` slot, so the condition parses as a command and
8843 // its comparison never surfaces as a token — the same
8844 // grammar limitation #1264 recorded for ABC.
8845 BracedWordValueCase {
8846 source: "for {set i 0} {$i < 3} {incr i} { puts $i }\n",
8847 counts: [2, 5, 8, 10],
8848 operands: &["for", "i", "0", "$i", "<", "3", "incr", "puts"],
8849 },
8850 // #1382's guard rows: an arm *pattern* spelled like a
8851 // command whose value slots the rule now withdraws. An arm
8852 // list parses as commands, so the pattern becomes a real
8853 // command name and the arm *body* lands in one of that
8854 // command's value slots — where only `Getter::is_switch_arm`
8855 // keeps it a block. Both rows are `switch` bodies that must
8856 // keep their `{}`, and both were measured with the guard
8857 // short-circuited to `false`.
8858 //
8859 // This one is the positional half: `after`'s slot 0 is the
8860 // millisecond count, so without the guard `{ puts B }` reads
8861 // as that value and the row drops to N1 1.
8862 BracedWordValueCase {
8863 source: "switch $v { after { puts B } }\n",
8864 counts: [1, 2, 5, 5],
8865 operands: &["switch", "$v", "after", "puts", "B"],
8866 },
8867 // …and this is the field-keyed half. `proc` needs three
8868 // words, so the arm patterns must be written one per line
8869 // for the grammar to build a `procedure` at all — on one
8870 // line it recovers into an `ERROR` and no rule fires. Here
8871 // it swallows two arms, taking `{ puts A }` as its `name`
8872 // field and `{ puts B }` as its body; without the guard the
8873 // name's brace goes and the row reads N1 5. The `namespace`
8874 // arm is the third spelling of the same hazard and is not
8875 // discriminating: its subcommand slot holds the braced body
8876 // itself rather than a `simple_word`, so no layout resolves
8877 // and the construct-wide script answer stands either way.
8878 // `proc` and `namespace` are operators here and `after` an
8879 // operand, which is the three patterns' whole difference.
8880 BracedWordValueCase {
8881 source: "switch $v {\n proc { puts A }\n after { puts B }\n namespace { puts C }\n}\n",
8882 counts: [3, 6, 7, 9],
8883 operands: &["switch", "$v", "after", "puts", "A", "B", "C"],
8884 },
8885 // The arity guard on `ScriptSlots::LastOf`, Tcl-only
8886 // because the iRules grammar models `trap_handler` and so
8887 // never reaches the generic-command path (#1382 review).
8888 // Tcl models neither `on` nor `trap`, so the whole tail
8889 // after the `try` body parses as *one* `trap` command with
8890 // five arguments — `{p} {v} {puts b} finally {puts c}`.
8891 // Reading "the script is the last argument" off that names
8892 // the `finally` body and calls `{puts b}` a value, which
8893 // dropped the `{}` of a block the line does contain and
8894 // read N1 3. All five braces are blocks here, which is the
8895 // answer a list the signature cannot explain has to get.
8896 // The `trap` row in the shared table is the same clause at
8897 // its documented arity, where two of the three braces
8898 // really are values — so a guard stuck on "always script"
8899 // fails there and one stuck on "always last" fails here.
8900 BracedWordValueCase {
8901 source: "try {puts a} trap {p} {v} {puts b} finally {puts c}\n",
8902 counts: [2, 6, 8, 10],
8903 operands: &["a", "b", "c", "finally", "p", "puts", "trap", "v"],
8904 },
8905 ];
8906 check_braced_word_value_cases::<TclParser>(&tcl_only, "foo.tcl");
8907 }
8908
8909 /// The iRules twin of the test above. The shared table runs
8910 /// unchanged — the two getters are deliberate clones and #1318
8911 /// names both — and the dialect rows cover the handler bodies,
8912 /// which have no Tcl spelling.
8913 #[cfg(feature = "irules")]
8914 #[test]
8915 fn irules_braced_word_role_follows_the_enclosing_command_1318() {
8916 check_braced_word_value_cases::<IrulesParser>(&BRACED_WORD_VALUE_CASES, "foo.irule");
8917 let irules_only: [BracedWordValueCase; 4] = [
8918 // A `when` handler body is a modelled slot, so the literal
8919 // inside it is classified independently of the block that
8920 // holds it.
8921 BracedWordValueCase {
8922 source: "when HTTP_REQUEST {\n lappend b {x y}\n}\n",
8923 counts: [2, 2, 5, 5],
8924 operands: &["HTTP_REQUEST", "lappend", "b", "x", "y"],
8925 },
8926 // This grammar models `switch` with `switch_arm` children,
8927 // so the arm body fills a modelled slot and never reaches
8928 // the command-name list the Tcl rows need.
8929 BracedWordValueCase {
8930 source: "when HTTP_REQUEST {\n switch $v { a { pool p1 } }\n}\n",
8931 counts: [3, 5, 5, 5],
8932 operands: &["HTTP_REQUEST", "$v", "a", "pool", "p1"],
8933 },
8934 // `on` and `trap` are why the command list carries two
8935 // words no *Tcl* command spells. The grammar emits
8936 // `on_handler` / `trap_handler` only under `try` — pinned
8937 // by `irules_try_handler_kinds_appear_only_under_try` — so
8938 // a statement-level handler is a generic command, and
8939 // without the list entry its body would lose the `{}` that
8940 // the second occurrence here counts.
8941 BracedWordValueCase {
8942 source: "when RULE_INIT { set x 1 }\non error { log local0. oops }\n",
8943 counts: [3, 4, 8, 8],
8944 operands: &[
8945 "RULE_INIT",
8946 "x",
8947 "1",
8948 "on",
8949 "error",
8950 "log",
8951 "local0.",
8952 "oops",
8953 ],
8954 },
8955 // #1382 inside a modelled handler slot: the `when` body is
8956 // a block and keeps its `{}`, and the `after` delay nested
8957 // in it is a value and does not. Before #1382 this read
8958 // N1 4. The shared table's `after` pair already runs for
8959 // this dialect; what this row adds is that the rule reaches
8960 // a command nested inside a handler, where the `{}` billed
8961 // for the handler could otherwise mask a withdrawal.
8962 BracedWordValueCase {
8963 source: "when HTTP_REQUEST {\n after {100} {puts hi}\n}\n",
8964 counts: [2, 3, 5, 5],
8965 operands: &["HTTP_REQUEST", "after", "100", "puts", "hi"],
8966 },
8967 ];
8968 check_braced_word_value_cases::<IrulesParser>(&irules_only, "foo.irule");
8969 }
8970
8971 #[cfg(feature = "php")]
8972 #[test]
8973 fn php_operators_and_operands() {
8974 check_metrics::<PhpParser>(
8975 "<?php
8976 function avg(int $a, int $b, int $c): int {
8977 return ($a + $b + $c) / 3;
8978 }",
8979 "foo.php",
8980 |metric| {
8981 // After #695 only the opening delimiters count: `()` and
8982 // `{}` fold to one operator each per balanced pair, so the
8983 // former `)`/`}` closers no longer inflate n1/N1 (was
8984 // 11 unique / 15 total).
8985 //
8986 // Operands after #1293, tallied by `get_id` (source bytes):
8987 // `avg` × 1, `int` × 4 (the `primitive_type` wrapper at
8988 // all four type positions — its `int` keyword child is
8989 // suppressed under it), `$a` / `$b` / `$c` × 2 each,
8990 // `3` × 1 ⇒ n2 = 6, N2 = 12. Between #1259 and #1293 the
8991 // keyword leaf doubled the type count ⇒ 6 / 16; before
8992 // #1259 each `$v` also contributed its sigil-less `name`
8993 // leaf ⇒ 9 / 22.
8994 assert_eq!(metric.halstead.unique_operators(), 9);
8995 assert_eq!(metric.halstead.total_operators(), 12);
8996 assert_eq!(metric.halstead.unique_operands(), 6);
8997 assert_eq!(metric.halstead.total_operands(), 12);
8998 insta::assert_json_snapshot!(metric.halstead);
8999 },
9000 );
9001 }
9002
9003 #[cfg(feature = "php")]
9004 #[test]
9005 fn php_simple_function() {
9006 check_metrics::<PhpParser>(
9007 "<?php
9008 function inc(int $x): int { return $x + 1; }",
9009 "foo.php",
9010 |metric| {
9011 // After #695 only opening delimiters count: the `)`/`}`
9012 // closers no longer add operators (was 9 unique / 9 total).
9013 //
9014 // Operands after #1293: `inc` × 1, `int` × 2 (the
9015 // `primitive_type` wrapper at both type positions, its
9016 // `int` keyword child suppressed under it), `$x` × 2,
9017 // `1` × 1 ⇒ n2 = 4, N2 = 6. Between #1259 and #1293 the
9018 // keyword leaf doubled the type count ⇒ 4 / 8; before
9019 // #1259 `$x` also contributed its `x` leaf twice ⇒ 5 / 10.
9020 assert_eq!(metric.halstead.unique_operators(), 7);
9021 assert_eq!(metric.halstead.total_operators(), 7);
9022 assert_eq!(metric.halstead.unique_operands(), 4);
9023 assert_eq!(metric.halstead.total_operands(), 6);
9024 insta::assert_json_snapshot!(metric.halstead);
9025 },
9026 );
9027 }
9028
9029 #[cfg(feature = "php")]
9030 #[test]
9031 fn php_variable_reference_counts_once() {
9032 // Regression: issue #1259. `$x` parses as a `variable_name`
9033 // wrapping a `name` leaf, and both kinds were in the operand
9034 // arm — so every variable reference contributed twice to N2 and
9035 // planted a sigil-less twin (`x` beside `$x`) in the n2
9036 // vocabulary. Since `$var` is the most common token class in
9037 // PHP, that roughly doubled N2 for real files.
9038 //
9039 // Source: the issue's reproducer plus a re-reference of `$a` and
9040 // `$b`, so N2 exceeds n2 and the assertions can tell "counted
9041 // once per occurrence" from "deduplicated into the vocabulary".
9042 // <?php $a = null; $b = true; $c = NULL; $a = $b;
9043 //
9044 // Operands by text key: `$a` × 2, `$b` × 2, `$c`, `null`, `true`,
9045 // `NULL` ⇒ n2 = 6, N2 = 8. Before the fix the `a` / `b` / `c`
9046 // leaves added 3 unique and 5 occurrences ⇒ 9 / 13.
9047 check_metrics::<PhpParser>(
9048 "<?php\n$a = null;\n$b = true;\n$c = NULL;\n$a = $b;\n",
9049 "foo.php",
9050 |metric| {
9051 assert_eq!(metric.halstead.unique_operands(), 6);
9052 assert_eq!(metric.halstead.total_operands(), 8);
9053 },
9054 );
9055 }
9056
9057 #[cfg(feature = "php")]
9058 #[test]
9059 fn php_dynamic_variable_name_counts_once_at_any_depth() {
9060 // Regression: issue #1259. Variable-variable syntax nests the
9061 // wrappers, so the double count compounds: `$$a` is a
9062 // `dynamic_variable_name` → `variable_name` → `name` chain that
9063 // scored 3 for one reference, and `$$$b` scored 4. Only the
9064 // outermost wrapper may count.
9065 //
9066 // Source: <?php $$a = 1; $$$b = 2; ${$c} = 3; $$a = 4;
9067 // The trailing re-assignment repeats `$$a` so N2 exceeds n2 and
9068 // the assertions can tell "counted once per occurrence" from
9069 // "deduplicated into the vocabulary".
9070 //
9071 // Operands: `$$a` × 2, `$$$b`, `${$c}`, `1`, `2`, `3`, `4`
9072 // ⇒ n2 = 7, N2 = 8. Before the fix: 14 / 17 (measured), each
9073 // target contributing its whole nesting chain — `$$a` → `$a` →
9074 // `a` is 3 (twice over), `$$$b` → `$$b` → `$b` → `b` is 4, and
9075 // `${$c}` → `$c` → `c` is 3, plus the four integers.
9076 check_metrics::<PhpParser>(
9077 "<?php $$a = 1; $$$b = 2; ${$c} = 3; $$a = 4;",
9078 "foo.php",
9079 |metric| {
9080 assert_eq!(metric.halstead.unique_operands(), 7);
9081 assert_eq!(metric.halstead.total_operands(), 8);
9082 },
9083 );
9084 }
9085
9086 #[cfg(feature = "php")]
9087 #[test]
9088 fn php_dynamic_variable_name_guard_is_parent_scoped() {
9089 // Companion to the two tests above (#1259): the guards fire on
9090 // the *parent* kind, never on the kind alone, so the two
9091 // positions where a nested node is a reference in its own right
9092 // keep counting.
9093 //
9094 // Source: <?php $y = "brace ${z} end"; $s = ${$a . 'b'};
9095 //
9096 // `"${z}"` is a `dynamic_variable_name` whose `name` child is
9097 // suppressed (the wrapper `${z}` carries the reference), while
9098 // `${$a . 'b'}` reaches its `$a` through a `binary_expression`,
9099 // so that `variable_name`'s parent is not a
9100 // `dynamic_variable_name` and it counts normally.
9101 //
9102 // Operands: `$y`, `${z}`, `$s`, `${$a . 'b'}`, `$a`, `'b'` — one
9103 // each ⇒ n2 = 6, N2 = 6. A guard written as a blanket kind
9104 // exclusion instead of a parent check would drop `$a` ⇒ 5 / 5.
9105 check_metrics::<PhpParser>(
9106 "<?php $y = \"brace ${z} end\"; $s = ${$a . 'b'};",
9107 "foo.php",
9108 |metric| {
9109 assert_eq!(metric.halstead.unique_operands(), 6);
9110 assert_eq!(metric.halstead.total_operands(), 6);
9111 },
9112 );
9113 }
9114
9115 #[cfg(feature = "php")]
9116 #[test]
9117 fn php_type_wrappers_count_the_type_once() {
9118 // Regression: issue #1293. A parameter type nests wrapper nodes
9119 // whose text spans the node below them — `primitive_type` around
9120 // the `int` keyword token, `named_type` around a `name`,
9121 // `optional_type` around either — and every level was in the
9122 // operand arm, so `int` scored 2 and `?int` scored 3.
9123 //
9124 // Source: the issue's first reproducer.
9125 // <?php
9126 // function f(int $a, bool $b, float $c, string $d, array $e,
9127 // Foo $g): ?int { return 0; }
9128 //
9129 // Operands by text key: `f`, the five `primitive_type` parameter
9130 // types, `Foo` (the `name` under its `named_type`), `$a`..`$g`,
9131 // the return `int`, and `0`. `int` occurs twice (parameter and
9132 // return) ⇒ n2 = 14, N2 = 15. Before the fix: 15 / 23 — the
9133 // extra vocabulary entry being `?int`, which the `?` operator
9134 // already accounts for.
9135 check_metrics::<PhpParser>(
9136 "<?php\nfunction f(int $a, bool $b, float $c, string $d, \
9137 array $e, Foo $g): ?int { return 0; }\n",
9138 "foo.php",
9139 |metric| {
9140 assert_eq!(metric.halstead.unique_operands(), 14);
9141 assert_eq!(metric.halstead.total_operands(), 15);
9142 },
9143 );
9144 }
9145
9146 #[cfg(feature = "php")]
9147 #[test]
9148 fn php_qualified_name_counts_its_components_once() {
9149 // Regression: issue #1293. `Foo\Bar\Baz` parses as
9150 // `qualified_name` → `namespace_name` → `name` × N, and all
9151 // three kinds were operands, so one three-part path scored 5 and
9152 // planted `Foo`, `Foo\Bar` and `Foo\Bar\Baz` in the vocabulary.
9153 // The components carry the operand and `\` stays an operator,
9154 // matching how PHP's own `::` and `->` already read here.
9155 //
9156 // Source: the issue's second reproducer.
9157 // <?php
9158 // namespace App\Sub;
9159 // use Foo\Bar\Baz;
9160 // $o = new \Vendor\Pkg\Thing();
9161 //
9162 // Operands: `App`, `Sub`, `Foo`, `Bar`, `Baz`, `$o`, `Vendor`,
9163 // `Pkg`, `Thing` — one each ⇒ n2 = 9, N2 = 9. Before the fix:
9164 // 14 / 14.
9165 check_metrics::<PhpParser>(
9166 "<?php\nnamespace App\\Sub;\nuse Foo\\Bar\\Baz;\n\
9167 $o = new \\Vendor\\Pkg\\Thing();\n",
9168 "foo.php",
9169 |metric| {
9170 assert_eq!(metric.halstead.unique_operands(), 9);
9171 assert_eq!(metric.halstead.total_operands(), 9);
9172 },
9173 );
9174 }
9175
9176 #[cfg(feature = "php")]
9177 #[test]
9178 fn php_nested_type_wrappers_count_once_at_any_depth() {
9179 // Companion to the two tests above (#1293): the type and
9180 // qualified-name wrappers compose, so a single annotation can
9181 // stack five levels — `?A\B` is `optional_type` → `named_type` →
9182 // `qualified_name` → `namespace_name` → `name`, which scored 6
9183 // operands for two identifiers. `union_type` and
9184 // `intersection_type` stack the same way over their members;
9185 // their `|` and `&` are already operators.
9186 //
9187 // Source:
9188 // <?php function k(?A\B $p, int|string $q, C&D $r): ?A\B
9189 // { return 0; }
9190 // The return type repeats `?A\B` so N2 exceeds n2 and the
9191 // assertions can tell "counted once per occurrence" from
9192 // "deduplicated into the vocabulary".
9193 //
9194 // Operands: `k`, `A` × 2, `B` × 2, `$p`, `int`, `string`, `$q`,
9195 // `C`, `D`, `$r`, `0` ⇒ n2 = 11, N2 = 13.
9196 check_metrics::<PhpParser>(
9197 "<?php function k(?A\\B $p, int|string $q, C&D $r): ?A\\B { return 0; }",
9198 "foo.php",
9199 |metric| {
9200 assert_eq!(metric.halstead.unique_operands(), 11);
9201 assert_eq!(metric.halstead.total_operands(), 13);
9202 },
9203 );
9204 }
9205
9206 #[cfg(feature = "php")]
9207 #[test]
9208 fn php_childless_primitive_types_still_count() {
9209 // Guards the direction of the #1293 fix for `primitive_type`,
9210 // where — unlike the qualified-name wrappers — the *wrapper*
9211 // carries the operand and the keyword leaf is suppressed. The
9212 // grammar emits no token node under `primitive_type` for
9213 // `callable`, `iterable`, `mixed`, `void`, `false` or `true`
9214 // (verified with `bca dump`), so the other direction would score
9215 // those six types zero — grammar-dispatch §6.
9216 //
9217 // Source:
9218 // <?php function q(callable $a, iterable $b, mixed $c,
9219 // false $d, true $e): void { }
9220 //
9221 // Operands: `q`, `callable`, `iterable`, `mixed`, `false`,
9222 // `true`, `void`, `$a`..`$e` ⇒ n2 = 12, N2 = 12. Dropping
9223 // `PrimitiveType` from the operand arm instead of gating its
9224 // leaf gives 6 / 6.
9225 check_metrics::<PhpParser>(
9226 "<?php function q(callable $a, iterable $b, mixed $c, \
9227 false $d, true $e): void { }",
9228 "foo.php",
9229 |metric| {
9230 assert_eq!(metric.halstead.unique_operands(), 12);
9231 assert_eq!(metric.halstead.total_operands(), 12);
9232 },
9233 );
9234 }
9235
9236 #[cfg(feature = "php")]
9237 #[test]
9238 fn php_primitive_type_keyword_guard_is_parent_scoped() {
9239 // Companion to the test above (#1293): the keyword suppression
9240 // fires on the *parent* kind, never on the kind alone.
9241 // `array` is also the head token of an `array(…)` literal, where
9242 // it is the construct's only operand and must keep counting; a
9243 // `(int)` / `(string)` cast is a childless `cast_type` that
9244 // never reaches the guard at all.
9245 //
9246 // Source: <?php $x = array(1, 2); $y = (int) $x; $z = (string) $x;
9247 //
9248 // Operands: `$x` × 3, `array`, `1`, `2`, `$y`, `int`, `$z`,
9249 // `string` ⇒ n2 = 8, N2 = 10. A blanket kind exclusion instead
9250 // of a parent check would drop the `array` head ⇒ 7 / 9.
9251 check_metrics::<PhpParser>(
9252 "<?php $x = array(1, 2); $y = (int) $x; $z = (string) $x;",
9253 "foo.php",
9254 |metric| {
9255 assert_eq!(metric.halstead.unique_operands(), 8);
9256 assert_eq!(metric.halstead.total_operands(), 10);
9257 },
9258 );
9259 }
9260
9261 #[cfg(feature = "php")]
9262 #[test]
9263 fn php_encapsed_string_interpolation_no_double_count() {
9264 // Regression: issue #184. A PHP `"Hello $name!"` used to be
9265 // classified as a Halstead operand (the wrapping
9266 // `encapsed_string`) AND have its inner `variable_name`
9267 // (`$name`) plus the inner `name` token classified as
9268 // operands too. With the fix, the wrapping literal drops to
9269 // `Unknown` when it carries any `$var` / `${name}` / `{$expr}`
9270 // child, so `$name` is counted exactly once at each text
9271 // occurrence.
9272 //
9273 // Source:
9274 // <?php $name = "world"; echo "Hello $name!";
9275 //
9276 // Inert operand: `"world"` (no interpolation, still operand).
9277 // Operands by text key (`get_id` keys by source bytes):
9278 // `$name` × 2 (assignment LHS and `$name` inside the
9279 // interpolated string), `"world"` × 1.
9280 // u_operands = 2, N2 = 3.
9281 // Without the #184 fix the wrapping `"Hello $name!"` would also
9282 // count → 3 / 4. This test additionally pinned the *inner* `name`
9283 // leaf of each `variable_name` (a further 2 occurrences, 1 unique
9284 // ⇒ the historical 3 / 5) until #1259 recognised that as the same
9285 // double count one level down.
9286 check_metrics::<PhpParser>(
9287 "<?php $name = \"world\"; echo \"Hello $name!\";",
9288 "foo.php",
9289 |metric| {
9290 assert_eq!(metric.halstead.unique_operands(), 2);
9291 assert_eq!(metric.halstead.total_operands(), 3);
9292 },
9293 );
9294 }
9295
9296 #[cfg(feature = "php")]
9297 #[test]
9298 fn php_encapsed_string_no_interpolation_still_operand() {
9299 // The fix for #184 only drops `EncapsedString`/`Heredoc` from
9300 // the operand arm when interpolation is present. An inert
9301 // double-quoted string must still count as exactly one
9302 // operand, identical to the single-quoted equivalent.
9303 //
9304 // Source: `<?php echo "Hello world!";`
9305 // Operands: `"Hello world!"` × 1 → u_operands = 1, N2 = 1.
9306 check_metrics::<PhpParser>("<?php echo \"Hello world!\";", "foo.php", |metric| {
9307 assert_eq!(metric.halstead.unique_operands(), 1);
9308 assert_eq!(metric.halstead.total_operands(), 1);
9309 });
9310 }
9311
9312 #[cfg(feature = "php")]
9313 #[test]
9314 fn php_heredoc_interpolation_no_double_count() {
9315 // Regression: issue #184. A PHP heredoc whose body
9316 // interpolates `$name` previously counted both the wrapping
9317 // `heredoc` node and the inner `$name` as operands; the fix
9318 // drops the wrapper when its `heredoc_body` carries any
9319 // interpolation child.
9320 //
9321 // Source:
9322 // <?php $name = "x"; echo <<<EOT
9323 // hi $name
9324 // EOT;
9325 //
9326 // Operands by text key: `$name` × 2, `"x"` × 1 (inert encapsed
9327 // string, still an operand). With the fix u_operands = 2,
9328 // N2 = 3. Without it the wrapping heredoc text would add one
9329 // more unique operand. The sigil-less `name` leaf inside each
9330 // `variable_name` was counted too until #1259.
9331 check_metrics::<PhpParser>(
9332 "<?php $name = \"x\"; echo <<<EOT\nhi $name\nEOT;\n",
9333 "foo.php",
9334 |metric| {
9335 assert_eq!(metric.halstead.unique_operands(), 2);
9336 assert_eq!(metric.halstead.total_operands(), 3);
9337 },
9338 );
9339 }
9340
9341 #[cfg(feature = "php")]
9342 #[test]
9343 fn php_nowdoc_unaffected() {
9344 // `Nowdoc` (single-quoted heredoc) never interpolates and is
9345 // never matched by `php_string_has_interpolation`. It must
9346 // continue counting as exactly one operand regardless of the
9347 // text inside, mirroring single-quoted `String`.
9348 //
9349 // Source:
9350 // <?php echo <<<'EOT'
9351 // plain $name not interpolated
9352 // EOT;
9353 //
9354 // Operands: the nowdoc literal × 1 → u_operands = 1, N2 = 1.
9355 check_metrics::<PhpParser>(
9356 "<?php echo <<<'EOT'\nplain $name not interpolated\nEOT;\n",
9357 "foo.php",
9358 |metric| {
9359 assert_eq!(metric.halstead.unique_operands(), 1);
9360 assert_eq!(metric.halstead.total_operands(), 1);
9361 },
9362 );
9363 }
9364
9365 #[cfg(feature = "php")]
9366 #[test]
9367 fn php_encapsed_string_bare_member_access_no_double_count() {
9368 // Regression: issue #184 follow-up. The PHP grammar allows
9369 // bare `$obj->prop` interpolation inside `"…"` without
9370 // surrounding `{ … }`; tree-sitter-php emits this as a
9371 // direct `member_access_expression` child of
9372 // `encapsed_string` (kind_id 329 in the current grammar).
9373 // The wrapper must drop to `Unknown` for that form too —
9374 // otherwise the inner `$obj` and `prop` `name` tokens are
9375 // walked as operands while the wrapper also counts,
9376 // double-counting `N2`.
9377 //
9378 // Source:
9379 // <?php $obj = new stdClass; $obj->prop = "x"; echo "Hi $obj->prop!";
9380 //
9381 // Operands tallied by `get_id` (keyed on source bytes):
9382 // `$obj` × 3 (LHS assignment, member-access target,
9383 // inside the interpolated string)
9384 // `prop` (name) × 2 (member-access RHS twice — a bare `name`
9385 // outside any `variable_name`, so #1259's
9386 // guard leaves it an operand)
9387 // `stdClass` × 1
9388 // `"x"` × 1
9389 // ⇒ u_operands = 4, N2 = 7.
9390 // With the bug the wrapping `"Hi $obj->prop!"` text adds one
9391 // more unique operand and one more occurrence ⇒ 5 / 8.
9392 check_metrics::<PhpParser>(
9393 "<?php $obj = new stdClass; $obj->prop = \"x\"; echo \"Hi $obj->prop!\";",
9394 "foo.php",
9395 |metric| {
9396 assert_eq!(metric.halstead.unique_operands(), 4);
9397 assert_eq!(metric.halstead.total_operands(), 7);
9398 },
9399 );
9400 }
9401
9402 #[cfg(feature = "php")]
9403 #[test]
9404 fn php_encapsed_string_bare_subscript_no_double_count() {
9405 // Regression: issue #184 follow-up. Bare `$arr[0]` inside
9406 // `"…"` produces a `subscript_expression` child of
9407 // `encapsed_string` (kind_id 351). The wrapper must drop to
9408 // `Unknown` for that form.
9409 //
9410 // Source:
9411 // <?php $arr = [1]; echo "Hi $arr[0]!";
9412 //
9413 // Operands tallied by `get_id`:
9414 // `$arr` × 2, `1` × 1, `0` × 1.
9415 // ⇒ u_operands = 3, N2 = 4.
9416 // With the bug the wrapping `"Hi $arr[0]!"` text adds 1 / 1.
9417 // The inner `arr` leaf of each `variable_name` added a further
9418 // 1 / 2 until #1259.
9419 check_metrics::<PhpParser>(
9420 "<?php $arr = [1]; echo \"Hi $arr[0]!\";",
9421 "foo.php",
9422 |metric| {
9423 assert_eq!(metric.halstead.unique_operands(), 3);
9424 assert_eq!(metric.halstead.total_operands(), 4);
9425 },
9426 );
9427 }
9428
9429 #[cfg(feature = "php")]
9430 #[test]
9431 fn php_shell_command_expression_inert_is_operand() {
9432 // Regression: issue #288. Backtick command literals (PHP's
9433 // `shell_command_expression`) were filtered as strings by
9434 // `Checker::is_string` and `Alterator::alterate`, but never
9435 // classified as Halstead operands — so they contributed
9436 // nothing to N2 / eta2. An inert backtick literal must now
9437 // count as exactly one operand, matching `EncapsedString`
9438 // and `Heredoc`.
9439 //
9440 // Source: `<?php $out = ` + backtick `ls` + backtick + `;`
9441 // Operands tallied by `get_id`:
9442 // `$out` × 1, backtick literal × 1.
9443 // ⇒ u_operands = 2, N2 = 2.
9444 // Before the fix the backtick literal vanished from the count
9445 // ⇒ u_operands = 1, N2 = 1. (The inner `out` leaf of the
9446 // `variable_name` added another 1 / 1 until #1259.)
9447 check_metrics::<PhpParser>("<?php $out = `ls`;", "foo.php", |metric| {
9448 assert_eq!(metric.halstead.unique_operands(), 2);
9449 assert_eq!(metric.halstead.total_operands(), 2);
9450 });
9451 }
9452
9453 #[cfg(feature = "php")]
9454 #[test]
9455 fn php_shell_command_expression_interpolation_no_double_count() {
9456 // Regression: issue #288. PHP backtick literals DO support
9457 // `$var` interpolation (see tree-sitter-php node-types.json:
9458 // `shell_command_expression` children include `variable_name`,
9459 // `dynamic_variable_name`, `member_access_expression`,
9460 // `subscript_expression`). With the fix the wrapper drops to
9461 // `Unknown` when it carries any interpolation child, exactly
9462 // as `EncapsedString` does.
9463 //
9464 // Source: `<?php $dir = "/tmp"; $out = ` + backtick `ls $dir` +
9465 // backtick + `;`
9466 //
9467 // Operands tallied by `get_id`:
9468 // `$dir` × 2 (assignment LHS, inside backticks),
9469 // `$out` × 1, `"/tmp"` × 1.
9470 // ⇒ u_operands = 3, N2 = 4.
9471 // Without the interpolation guard the wrapping backtick literal
9472 // would also count ⇒ u_operands = 4, N2 = 5. The sigil-less
9473 // `dir` / `out` leaves added a further 2 / 3 until #1259.
9474 check_metrics::<PhpParser>(
9475 "<?php $dir = \"/tmp\"; $out = `ls $dir`;",
9476 "foo.php",
9477 |metric| {
9478 assert_eq!(metric.halstead.unique_operands(), 3);
9479 assert_eq!(metric.halstead.total_operands(), 4);
9480 },
9481 );
9482 }
9483
9484 #[cfg(feature = "php")]
9485 #[test]
9486 fn php_interpolation_opener_is_not_an_operator() {
9487 // Regression: issue #1314. `Php::LBRACE` is *both* the
9488 // compound-statement brace and the complex-interpolation
9489 // opener, so `"dq {$y} end"` reported a `{}` operator — and
9490 // reported it against the same vocabulary entry a real block
9491 // uses, which no other language does.
9492 //
9493 // expected: operators `=` × 2, `;` × 2 → n1 = 2, N1 = 4.
9494 // Operands `$s`, `$t`, `$y` × 2 → n2 = 3, N2 = 4. Before the
9495 // guard the two openers added `{}` → n1 = 3, N1 = 6.
9496 check_metrics::<PhpParser>(
9497 "<?php\n$s = \"dq {$y} end\";\n$t = \"dq {$y} end\";\n",
9498 "foo.php",
9499 |metric| {
9500 assert_eq!(metric.halstead.unique_operators(), 2);
9501 assert_eq!(metric.halstead.total_operators(), 4);
9502 assert_eq!(metric.halstead.unique_operands(), 3);
9503 assert_eq!(metric.halstead.total_operands(), 4);
9504 },
9505 );
9506 }
9507
9508 #[cfg(feature = "php")]
9509 #[test]
9510 fn php_interpolation_opener_guard_covers_every_wrapper() {
9511 // The opener is a direct child of four distinct parents, and
9512 // each is an independent leg of the guard (grammar-dispatch
9513 // section 11) — a fixture covering only `encapsed_string`
9514 // leaves the other three dead.
9515 //
9516 // One row per parent, so a failure names the leg that broke.
9517 // The heredoc's brace hangs off `heredoc_body` rather than
9518 // `heredoc`; the backtick form is `shell_command_expression`;
9519 // and the bare `${$y}` variable-variable is a
9520 // `dynamic_variable_name`, the one position that is not inside
9521 // a string at all.
9522 //
9523 // expected per row: operators `=` × 2, `;` × 2 → n1 = 2,
9524 // N1 = 4; three distinct operands with one repeated → n2 = 3,
9525 // N2 = 4.
9526 for (label, source) in [
9527 (
9528 "encapsed_string",
9529 "<?php\n$s = \"dq {$y} end\";\n$t = \"dq {$y} end\";\n",
9530 ),
9531 (
9532 "heredoc_body",
9533 "<?php\n$h = <<<EOT\na {$y} b\nEOT;\n$i = <<<EOT\na {$y} b\nEOT;\n",
9534 ),
9535 (
9536 "shell_command_expression",
9537 "<?php\n$b = `ls {$y}`;\n$c = `ls {$y}`;\n",
9538 ),
9539 ("dynamic_variable_name", "<?php\n$q = ${$y};\n$r = ${$y};\n"),
9540 ] {
9541 assert_halstead_counts::<PhpParser>(source, "foo.php", [2, 4, 3, 4], label);
9542 }
9543 }
9544
9545 #[cfg(feature = "php")]
9546 #[test]
9547 fn php_every_interpolation_spelling_scores_alike() {
9548 // The policy stated as a test (#1314). PHP writes one
9549 // interpolation three ways; the choice is spelling, so all
9550 // three must score identically. Before the guard the two
9551 // braced forms reported a `{}` the bare `$y` form did not.
9552 //
9553 // `"${y}"` is deprecated as of PHP 8.2 and removed in 9.0, but
9554 // the pinned grammar still parses it and it is still in the
9555 // wild, so it stays a row here.
9556 //
9557 // The fixture deliberately omits a `$y = …` declaration: with
9558 // one, the bare and `{$y}` forms key their operand as `$y` and
9559 // collapse into the declaration's entry while `${y}` keys as
9560 // `${y}` and does not, so n2 would differ for a reason that has
9561 // nothing to do with this guard.
9562 //
9563 // expected per spelling: operators `=` × 2, `;` × 2 → n1 = 2,
9564 // N1 = 4; operands `$s`, `$t`, the interpolated reference × 2
9565 // → n2 = 3, N2 = 4.
9566 for literal in ["\"a $y b\"", "\"a {$y} b\"", "\"a ${y} b\""] {
9567 assert_halstead_counts::<PhpParser>(
9568 &format!("<?php\n$s = {literal};\n$t = {literal};\n"),
9569 "foo.php",
9570 [2, 4, 3, 4],
9571 &format!("interpolation {literal}"),
9572 );
9573 }
9574 }
9575
9576 #[cfg(feature = "php")]
9577 #[test]
9578 fn php_compound_statement_brace_still_counts() {
9579 // Control for #1314: the guard is scoped to the four
9580 // interpolating wrappers, so a real block keeps its `{}`. A
9581 // guard widened to every `LBRACE` would take `{}` out of the
9582 // operator set entirely and fail here.
9583 //
9584 // expected: operators `function`, `()`, `{}` × 2, `if`,
9585 // `return`, `;` → n1 = 6, N1 = 8. Operands `f`, `1`, `2` →
9586 // n2 = N2 = 3.
9587 check_metrics::<PhpParser>(
9588 "<?php\nfunction f() { if (1) { return 2; } }\n",
9589 "foo.php",
9590 |metric| {
9591 assert_eq!(metric.halstead.unique_operators(), 6);
9592 assert_eq!(metric.halstead.total_operators(), 8);
9593 assert_eq!(metric.halstead.unique_operands(), 3);
9594 assert_eq!(metric.halstead.total_operands(), 3);
9595 },
9596 );
9597 }
9598
9599 #[cfg(feature = "php")]
9600 #[test]
9601 fn php_interpolation_guard_is_parent_scoped_not_ancestor_scoped() {
9602 // The input that separates the parent-scoped guard from the
9603 // ancestor-scanning mutant — the mutant #1256's post-mortem
9604 // says survives every ordinary fixture. PHP is one of only two
9605 // languages in #1314 where such an input exists: a closure
9606 // inside a complex interpolation puts a *compound-statement*
9607 // brace under an `encapsed_string` ancestor while its parent is
9608 // the `compound_statement`. An ancestor scan swallows it.
9609 //
9610 // expected: operators `=`, `;` × 2, `->`, `()` × 2, `function`,
9611 // `{}`, `return` → n1 = 7, N1 = 9. Operands `$s`, `$o`, `m`,
9612 // `1` → n2 = N2 = 4. Under the ancestor-scoped mutant the
9613 // closure's brace vanishes: n1 = 6, N1 = 8.
9614 check_metrics::<PhpParser>(
9615 "<?php\n$s = \"{$o->m(function() { return 1; })}\";\n",
9616 "foo.php",
9617 |metric| {
9618 assert_eq!(metric.halstead.unique_operators(), 7);
9619 assert_eq!(metric.halstead.total_operators(), 9);
9620 assert_eq!(metric.halstead.unique_operands(), 4);
9621 assert_eq!(metric.halstead.total_operands(), 4);
9622 },
9623 );
9624 }
9625
9626 #[cfg(feature = "elixir")]
9627 #[test]
9628 fn elixir_operators_and_operands() {
9629 // Exercises every Halstead family classified in Elixir's
9630 // `get_op_type`: control-flow keywords (`do`, `end`, `fn`),
9631 // structural punctuation — only the *opening* delimiters `(`,
9632 // `[` count after #695 (the `)`/`]` closers were dropped), plus
9633 // `,`, `.`, `@`,
9634 // arithmetic (`+`, `-`, `*`, `/`), comparison (`==`, `>`),
9635 // logical (`&&`, `||`, `and`, `or`, `!`), pipe (`|>`), capture
9636 // (`&`), assignment/match (`=`), and the stab arrow (`->`).
9637 // The body mixes identifiers, integers, atoms, and a string.
9638 check_metrics::<ElixirParser>(
9639 "defmodule Foo do\n @doc \"add\"\n def calc(a, b) do\n result = a + b * 2\n flag = result > 0 && a == b\n out = if flag, do: result, else: -result\n [out, a, b]\n end\nend\n",
9640 "foo.ex",
9641 |metric| {
9642 // Positive headline assertions on integer counts. After
9643 // #695 only opening delimiters count: the `)`/`]` closers
9644 // no longer add operators (was 15 unique / 23 total).
9645 assert_eq!(metric.halstead.unique_operators(), 13);
9646 assert_eq!(metric.halstead.total_operators(), 21);
9647 assert_eq!(metric.halstead.unique_operands(), 16);
9648 assert_eq!(metric.halstead.total_operands(), 27);
9649 insta::assert_json_snapshot!(
9650 metric.halstead,
9651 @r#"
9652 {
9653 "unique_operators": 13,
9654 "total_operators": 21,
9655 "unique_operands": 16,
9656 "total_operands": 27,
9657 "length": 48,
9658 "estimated_program_length": 112.10571633583419,
9659 "purity_ratio": 2.3355357569965456,
9660 "vocabulary": 29,
9661 "volume": 233.18308776612344,
9662 "difficulty": 10.96875,
9663 "level": 0.09116809116809117,
9664 "effort": 2557.7269939346666,
9665 "time": 142.09594410748147,
9666 "bugs": 0.062342115670886794
9667 }
9668 "#
9669 );
9670 },
9671 );
9672 }
9673
9674 #[cfg(feature = "ruby")]
9675 #[test]
9676 fn ruby_operators_and_operands() {
9677 // A small Ruby method exercising operators (def/if/end keyword
9678 // tokens, `+`, `==`, `<=`, structural punctuation) and operands
9679 // (`n`, `1`, `factorial`). Anchors the unique/total counts on
9680 // both sides and snapshots the full Halstead derivation.
9681 //
9682 // Lesson 4 invariants: u_operators / u_operands here equal the
9683 // dedupe lengths the `--ops` accessor would emit on the same
9684 // source. Any future grammar bump that adds an aliased kind_id
9685 // to either side will trip this without snapshot drift.
9686 check_metrics::<RubyParser>(
9687 "def factorial(n)\n return 1 if n <= 1\n n * factorial(n - 1)\nend\n",
9688 "foo.rb",
9689 |metric| {
9690 // After #695 only the `(` opener counts (folded `()`); the
9691 // `)` closer — which appeared twice across the two calls —
9692 // no longer adds an operator (was 9 unique / 11 total).
9693 assert_eq!(metric.halstead.unique_operators(), 8);
9694 assert_eq!(metric.halstead.total_operators(), 9);
9695 assert_eq!(metric.halstead.unique_operands(), 3);
9696 assert_eq!(metric.halstead.total_operands(), 9);
9697 insta::assert_json_snapshot!(metric.halstead);
9698 },
9699 );
9700 }
9701
9702 #[cfg(feature = "ruby")]
9703 #[test]
9704 fn ruby_halstead_plain_string_operand() {
9705 // A bare string literal contributes exactly one operand. The
9706 // counterpart to `ruby_halstead_interpolated_string_no_double_count`
9707 // — verifies the "no interpolation" branch of the same arm
9708 // (see `get_op_type`'s `R::String | …` case in
9709 // `big-code-analysis-ast/src/getter.rs`).
9710 // expected: operators = {def, end} = 2; operands = {f, "hello"} = 2.
9711 check_metrics::<RubyParser>("def f\n \"hello\"\nend\n", "foo.rb", |metric| {
9712 assert_eq!(metric.halstead.unique_operators(), 2);
9713 assert_eq!(metric.halstead.total_operators(), 2);
9714 assert_eq!(metric.halstead.unique_operands(), 2);
9715 assert_eq!(metric.halstead.total_operands(), 2);
9716 });
9717 }
9718
9719 #[cfg(feature = "ruby")]
9720 #[test]
9721 fn ruby_halstead_interpolated_string_no_double_count() {
9722 // Regression mirror for #180 (Bash) / #183 (C#): when a Ruby
9723 // string literal carries an `Interpolation` child, the
9724 // wrapping `String` node is intentionally classified as
9725 // `Unknown` so the inner expression's identifiers are not
9726 // double-counted as operands.
9727 //
9728 // expected: for `def f(name)\n "Hi #{name}"\nend\n` —
9729 // operators: def, (, ), #{, }, end → u_operators = 6.
9730 // operands: f, name (param), name (inside `#{name}`). The
9731 // wrapping `"…#{name}"` literal is skipped by the
9732 // `is_child(R::Interpolation)` guard; the operand store
9733 // keys by token text so the two `name` occurrences dedupe
9734 // into one distinct entry → u_operands = 2, operands = 3
9735 // (`f` once, `name` twice).
9736 // Without the guard, the wrapping literal would also count,
9737 // inflating u_operands to 3 and operands to 4.
9738 check_metrics::<RubyParser>("def f(name)\n \"Hi #{name}\"\nend\n", "foo.rb", |metric| {
9739 assert_eq!(metric.halstead.unique_operands(), 2);
9740 assert_eq!(metric.halstead.total_operands(), 3);
9741 });
9742 }
9743
9744 #[cfg(feature = "ruby")]
9745 #[test]
9746 fn ruby_halstead_symbol_literal_operand() {
9747 // `:foo` is a `SimpleSymbol` leaf — counts as a single
9748 // operand, no interpolation guard needed (only
9749 // `DelimitedSymbol` (`:"…#{x}…"`) can interpolate).
9750 // expected: operators = {def, end} = 2; operands = {f, :ok} = 2.
9751 check_metrics::<RubyParser>("def f\n :ok\nend\n", "foo.rb", |metric| {
9752 assert_eq!(metric.halstead.unique_operators(), 2);
9753 assert_eq!(metric.halstead.unique_operands(), 2);
9754 });
9755 }
9756
9757 #[cfg(feature = "ruby")]
9758 #[test]
9759 fn ruby_halstead_regex_operand() {
9760 // `/foo/` parses as a `Regex` node — one operand. Its two
9761 // `SLASH` delimiters used to fall through to the shared
9762 // arithmetic arm and add a `/` operator that is nowhere in the
9763 // source; #1312 parent-guards them to `Unknown`.
9764 // expected: u_operators = {def, (, =~, end} = 4, N1 = 4 (only
9765 // the `(` opener counts after #695 — the `)` closer was
9766 // dropped; was 5 with the fabricated `/`); u_operands =
9767 // {f, s, /foo/} = 3, N2 = 4 (`s` twice: parameter and use).
9768 check_metrics::<RubyParser>("def f(s)\n s =~ /foo/\nend\n", "foo.rb", |metric| {
9769 assert_eq!(metric.halstead.unique_operators(), 4);
9770 assert_eq!(metric.halstead.total_operators(), 4);
9771 assert_eq!(metric.halstead.unique_operands(), 3);
9772 assert_eq!(metric.halstead.total_operands(), 4);
9773 });
9774 }
9775
9776 #[cfg(feature = "ruby")]
9777 #[test]
9778 fn ruby_regex_delimiters_are_not_operators() {
9779 // Regression: issue #1312, the Ruby sibling of Elixir #1256.
9780 // Both of a `Regex` literal's delimiter tokens are `SLASH` —
9781 // the same kind id real division uses — so `x = /abc/`
9782 // reported a `/` operator with no division in the source.
9783 //
9784 // expected: operators `=` → n1 = N1 = 1. Operands `x` and the
9785 // `/abc/` literal → n2 = N2 = 2. Before the guard the two
9786 // delimiters added `/` → n1 = 2, N1 = 3.
9787 check_metrics::<RubyParser>("x = /abc/\n", "foo.rb", |metric| {
9788 assert_eq!(metric.halstead.unique_operators(), 1);
9789 assert_eq!(metric.halstead.total_operators(), 1);
9790 assert_eq!(metric.halstead.unique_operands(), 2);
9791 assert_eq!(metric.halstead.total_operands(), 2);
9792 });
9793 }
9794
9795 #[cfg(feature = "ruby")]
9796 #[test]
9797 fn ruby_regex_delimiter_choice_is_invariant() {
9798 // Companion to the test above (#1312): `%r`-form regexes are
9799 // the same literal spelled differently, so every delimiter
9800 // choice must produce identical counts. tree-sitter-ruby
9801 // aliases all of them to `SLASH` — verified with `bca dump`,
9802 // which shows `%r{`/`}`, `%r(`/`)`, `%r[`/`]`, `%r<`/`>`,
9803 // `%r|`/`|` and `%r!`/`!` every one emitting kind `SLASH` —
9804 // so each row here genuinely exercises the guard rather than
9805 // reaching a different, already-clean path.
9806 //
9807 // expected per variant: operator `=` → n1 = N1 = 1; operands
9808 // `x` and the literal → n2 = N2 = 2.
9809 for literal in [
9810 "/abc/", "%r{abc}", "%r(abc)", "%r[abc]", "%r<abc>", "%r|abc|", "%r!abc!",
9811 ] {
9812 assert_halstead_counts::<RubyParser>(
9813 &format!("x = {literal}\n"),
9814 "foo.rb",
9815 [1, 1, 2, 2],
9816 &format!("regex literal {literal}"),
9817 );
9818 }
9819 }
9820
9821 #[cfg(feature = "ruby")]
9822 #[test]
9823 fn ruby_division_survives_the_regex_guard() {
9824 // Control for #1312: the guard is scoped to a `Regex` parent,
9825 // so real division still counts. Two divisions, so a mutant
9826 // that collapsed repeated hits would move `N1` even though
9827 // `n1` held (the #1294 count-only-anchor lesson).
9828 //
9829 // expected: operators `=` and `/` × 2 → n1 = 2, N1 = 3.
9830 // Operands `z`, `a`, `b`, `c` → n2 = N2 = 4.
9831 check_metrics::<RubyParser>("z = a / b / c\n", "foo.rb", |metric| {
9832 assert_eq!(metric.halstead.unique_operators(), 2);
9833 assert_eq!(metric.halstead.total_operators(), 3);
9834 assert_eq!(metric.halstead.unique_operands(), 4);
9835 assert_eq!(metric.halstead.total_operands(), 4);
9836 });
9837 }
9838
9839 #[cfg(feature = "ruby")]
9840 #[test]
9841 fn ruby_regex_guard_is_parent_scoped_not_ancestor_scoped() {
9842 // The one input that separates the correct parent-scoped guard
9843 // from the ancestor-scoped mutant of it (#1312, mirroring
9844 // #1256's Elixir case): a division *inside* a regex's `#{…}`
9845 // interpolation. Its `/` has `Binary` as its parent but the
9846 // `Regex` as a further ancestor, so an ancestor scan would
9847 // swallow it. Every other fixture in this file passes under
9848 // both spellings. Two interpolations, so the mutant moves both
9849 // n1 (2 → 1) and N1 (3 → 1).
9850 //
9851 // expected: operators `=`, `/` × 2 → n1 = 2, N1 = 3. Operands
9852 // `w`, `p`, `q`, `r`, `t` → n2 = N2 = 5; the wrapping `Regex`
9853 // is skipped because it carries an `Interpolation` child (the
9854 // #180 double-count guard).
9855 //
9856 // Was n1 = 3, N1 = 5 until #1314 dropped `#{` from the operator
9857 // arm. The mutant still moves both axes, so this fixture is as
9858 // discriminating as it was — it just no longer counts the two
9859 // interpolation openers alongside the two divisions.
9860 check_metrics::<RubyParser>("w = /a#{p / q}c#{r / t}b/\n", "foo.rb", |metric| {
9861 assert_eq!(metric.halstead.unique_operators(), 2);
9862 assert_eq!(metric.halstead.total_operators(), 3);
9863 assert_eq!(metric.halstead.unique_operands(), 5);
9864 assert_eq!(metric.halstead.total_operands(), 5);
9865 });
9866 }
9867
9868 #[cfg(feature = "ruby")]
9869 #[test]
9870 fn ruby_regex_start_alias_never_reaches_kind_id() {
9871 // Drift marker for the `R::SLASH2` half of #1312's guard.
9872 // `SLASH2` is the aliased regex-start token: it sits in the
9873 // enum beside the other literal-start aliases (`DQUOTE`,
9874 // `COLONDQUOTE`, `BQUOTE2`, `PERCENTwLPAREN`) and the runtime
9875 // `public_symbol_map` collapses it to `SLASH` before
9876 // `kind_id()`, exactly like `LPAREN2` in #768. It is listed in
9877 // the guard rather than the arithmetic arm because a regex
9878 // delimiter is the only thing it could ever be; this pins that
9879 // it is currently unreachable, so a grammar bump that starts
9880 // emitting it fails here instead of silently changing a metric.
9881 let path = PathBuf::from("foo.rb");
9882 for source in ["x = /abc/\n", "x = %r{abc}\n"] {
9883 let parser = RubyParser::new(source.as_bytes().to_vec(), &path, None);
9884 assert!(
9885 !ast_has_kind_id(&parser, Ruby::SLASH2 as u16),
9886 "Ruby::SLASH2 must stay collapsed to Ruby::SLASH for `{source}`"
9887 );
9888 // Positive control: the id the guard actually fires on is
9889 // present, so the assertion above cannot pass merely
9890 // because no delimiter was parsed at all.
9891 assert!(
9892 ast_has_kind_id(&parser, Ruby::SLASH as u16),
9893 "Ruby::SLASH must be the delimiter kind for `{source}`"
9894 );
9895 }
9896 }
9897
9898 #[cfg(feature = "ruby")]
9899 #[test]
9900 fn ruby_subshell_delimiters_are_not_operators() {
9901 // Regression: issue #1360, the second delimiter family of the
9902 // fabrication #1312 removed for regexes. tree-sitter-ruby
9903 // aliases both ends of a subshell literal to `BQUOTE` — the
9904 // same kind id the `` def ` `` method-name marker uses — so
9905 // `` x = `echo hi` `` reported a `` ` `` operator with no
9906 // backtick method call in the source.
9907 //
9908 // expected: operator `=` → n1 = N1 = 1. Operands `x` and the
9909 // `` `echo hi` `` literal → n2 = N2 = 2. Before the guard the
9910 // two delimiters added `` ` `` → n1 = 2, N1 = 3.
9911 assert_halstead_counts::<RubyParser>(
9912 "x = `echo hi`\n",
9913 "foo.rb",
9914 [1, 1, 2, 2],
9915 "backtick subshell",
9916 );
9917 }
9918
9919 #[cfg(feature = "ruby")]
9920 #[test]
9921 fn ruby_subshell_delimiter_choice_is_invariant() {
9922 // Companion to the test above (#1360): `%x`-form subshells are
9923 // the same literal spelled differently, so every delimiter
9924 // choice must produce identical counts. tree-sitter-ruby
9925 // aliases all of them to `BQUOTE` — verified with `bca dump`,
9926 // which shows `` ` ``/`` ` ``, `%x{`/`}`, `%x(`/`)`, `%x[`/`]`,
9927 // `%x<`/`>`, `%x|`/`|` and `%x!`/`!` every one emitting kind
9928 // `BQUOTE` — so each row here genuinely exercises the guard
9929 // rather than reaching a different, already-clean path. Note
9930 // the `%x!…!` and `%x|…|` closers in particular: they are
9931 // `BQUOTE`, not the `BANG` / `PIPE` operators they look like.
9932 //
9933 // The empty row records that `` `` `` parses to a `Subshell`
9934 // holding nothing but its two delimiters, and still bills that
9935 // wrapper as one operand once both are suppressed. It is
9936 // documentation, not a discriminator: the guard deletes nothing
9937 // from the operand arm, so grammar-dispatch §6's
9938 // childless-variant-regresses-to-zero hazard cannot arise here
9939 // and this row moves with the first row under every mutant.
9940 //
9941 // expected per variant: operator `=` → n1 = N1 = 1; operands
9942 // `x` and the literal → n2 = N2 = 2.
9943 for literal in [
9944 "`echo`", "%x{echo}", "%x(echo)", "%x[echo]", "%x<echo>", "%x|echo|", "%x!echo!", "``",
9945 ] {
9946 assert_halstead_counts::<RubyParser>(
9947 &format!("x = {literal}\n"),
9948 "foo.rb",
9949 [1, 1, 2, 2],
9950 &format!("subshell literal {literal}"),
9951 );
9952 }
9953 }
9954
9955 #[cfg(feature = "ruby")]
9956 #[test]
9957 fn ruby_backtick_method_name_survives_the_subshell_guard() {
9958 // Control for #1360, and the reason the kind is gated rather
9959 // than dropped from the operator arm (grammar-dispatch §6):
9960 // `` ` `` is a legal Ruby method name. `bca dump` at the pinned
9961 // grammar shows `` def `(cmd) `` emitting a `BQUOTE` wrapped in
9962 // a named `operator` node, so its parent is `Operator` and the
9963 // parent-scoped guard leaves it alone. Deleting `BQUOTE` from
9964 // the operator arm would have scored this construct zero.
9965 //
9966 // expected: operators `def`, `` ` ``, `(` and `end` →
9967 // n1 = N1 = 4 (only the `(` opener counts after #695).
9968 // Operand `cmd` twice, parameter and use → n2 = 1, N2 = 2.
9969 assert_halstead_counts::<RubyParser>(
9970 "def `(cmd)\n cmd\nend\n",
9971 "foo.rb",
9972 [4, 4, 1, 2],
9973 "backtick method name",
9974 );
9975 }
9976
9977 #[cfg(feature = "ruby")]
9978 #[test]
9979 fn ruby_subshell_guard_is_parent_scoped_not_ancestor_scoped() {
9980 // The one input that separates the correct parent-scoped guard
9981 // from the ancestor-scoped mutant of it (#1360, mirroring
9982 // #1312's regex case): backtick *methods* used inside a
9983 // subshell's `#{…}` interpolation. Their `BQUOTE` has the named
9984 // `operator` node as its parent but the `Subshell` as a further
9985 // ancestor, so an ancestor scan would swallow both. Every other
9986 // fixture in this file passes under either spelling. Two
9987 // markers — a definition and a call — so the mutant moves both
9988 // n1 (7 → 6) and N1 (10 → 8), per the #1294 count-only-anchor
9989 // lesson.
9990 //
9991 // expected: operators `=` (1), `def` (1), `` ` `` (2), `(` (2),
9992 // `;` (2), `end` (1), `.` (1) → n1 = 7, N1 = 10. Operands `n`,
9993 // `c` × 2, `s`, `"x"` → n2 = 4, N2 = 5; the wrapping `Subshell`
9994 // is skipped because it carries `Interpolation` children (the
9995 // #180 double-count guard), and its `a` / `b` string content is
9996 // classified by no arm.
9997 check_metrics::<RubyParser>(
9998 "n = `a#{def `(c); c; end}b#{s.`(\"x\")}`\n",
9999 "foo.rb",
10000 |metric| {
10001 assert_eq!(metric.halstead.unique_operators(), 7);
10002 assert_eq!(metric.halstead.total_operators(), 10);
10003 assert_eq!(metric.halstead.unique_operands(), 4);
10004 assert_eq!(metric.halstead.total_operands(), 5);
10005 },
10006 );
10007 }
10008
10009 #[cfg(feature = "ruby")]
10010 #[test]
10011 fn ruby_subshell_start_alias_never_reaches_kind_id() {
10012 // Drift marker for #1360's guard, the `BQUOTE2` sibling of
10013 // `ruby_regex_start_alias_never_reaches_kind_id`. `BQUOTE2`
10014 // sits in the enum's literal-start alias block beside `DQUOTE`,
10015 // `COLONDQUOTE` and `SLASH2`, and the runtime
10016 // `public_symbol_map` collapses it to `BQUOTE` before
10017 // `kind_id()`, exactly like `LPAREN2` in #768.
10018 //
10019 // `BQUOTE2` is therefore in no dispatch arm at all — not the
10020 // guard and not the operator arm — which is the asymmetry with
10021 // `SLASH2`, a kind #1312 had to *move* out of the arithmetic
10022 // arm. Adding it to the guard would change nothing, since the
10023 // wildcard already answers `Unknown` for it. What that leaves
10024 // unguarded is the reverse direction: a bump that started
10025 // emitting `BQUOTE2` would drop a subshell opener silently
10026 // (already the wanted answer) but would also stop counting a
10027 // `` def ` `` marker spelled that way. This pins the
10028 // reachability so such a bump fails here rather than moving a
10029 // metric unobserved.
10030 let path = PathBuf::from("foo.rb");
10031 for source in ["x = `echo`\n", "x = %x{echo}\n"] {
10032 let parser = RubyParser::new(source.as_bytes().to_vec(), &path, None);
10033 assert!(
10034 !ast_has_kind_id(&parser, Ruby::BQUOTE2 as u16),
10035 "Ruby::BQUOTE2 must stay collapsed to Ruby::BQUOTE for `{source}`"
10036 );
10037 // Positive control: the id the guard actually fires on is
10038 // present, so the assertion above cannot pass merely
10039 // because no delimiter was parsed at all.
10040 assert!(
10041 ast_has_kind_id(&parser, Ruby::BQUOTE as u16),
10042 "Ruby::BQUOTE must be the delimiter kind for `{source}`"
10043 );
10044 }
10045 }
10046
10047 #[cfg(feature = "ruby")]
10048 #[test]
10049 fn ruby_interpolation_opener_is_not_an_operator() {
10050 // Behaviour change, not a fabrication fix: #1314 drops
10051 // `HASHLBRACE` from Ruby's operator arm. `#{` is a token of its
10052 // own here — unlike PHP's `{`, which aliases the
10053 // compound-statement brace — so nothing was being miscounted;
10054 // the question was whether an interpolation opener is an
10055 // operation at all, and across the five interpolating languages
10056 // three already said no. Ruby Halstead operator counts drop for
10057 // interpolated literals as a result.
10058 //
10059 // Asserted as an invariance: the interpolated and plain
10060 // spellings of one string must now score identically, which is
10061 // the policy rather than a magic number. Before the change the
10062 // interpolated row was n1 = 2, N1 = 3.
10063 //
10064 // expected per row: operator `=` × 2 → n1 = 1, N1 = 2; operands
10065 // `s`, `t`, and the literal's content contribution × 2 →
10066 // n2 = 3, N2 = 4.
10067 for literal in ["\"a #{y} b\"", "\"a b\""] {
10068 assert_halstead_counts::<RubyParser>(
10069 &format!("s = {literal}\nt = {literal}\n"),
10070 "foo.rb",
10071 [1, 2, 3, 4],
10072 &format!("literal {literal}"),
10073 );
10074 }
10075 }
10076
10077 #[cfg(feature = "ruby")]
10078 #[test]
10079 fn ruby_interpolation_opener_drop_covers_every_literal() {
10080 // `HASHLBRACE` is one arm, but it fires under every Ruby
10081 // literal that interpolates, so the drop is not specific to
10082 // double-quoted strings. A symbol and a regex — two literals
10083 // whose `#{…}` reaches the same token — must contribute no
10084 // operator for the opener either.
10085 //
10086 // expected: operator `=` × 3 → n1 = 1, N1 = 3. Operands `y`,
10087 // `1`, `a`, `b`, plus the two interpolated `y` references →
10088 // n2 = 4, N2 = 6. Before the change each `#{` added one →
10089 // n1 = 2, N1 = 5.
10090 check_metrics::<RubyParser>("y = 1\na = :\"s#{y}\"\nb = /r#{y}/\n", "foo.rb", |metric| {
10091 assert_eq!(metric.halstead.unique_operators(), 1);
10092 assert_eq!(metric.halstead.total_operators(), 3);
10093 assert_eq!(metric.halstead.unique_operands(), 4);
10094 assert_eq!(metric.halstead.total_operands(), 6);
10095 });
10096 }
10097
10098 #[cfg(feature = "ruby")]
10099 #[test]
10100 fn ruby_element_containers_count_elements_not_the_composite_1353() {
10101 // #1353. `chained_string`, `string_array` and `symbol_array`
10102 // hold *classified operand* children instead of the raw
10103 // `string_content` every other string-like literal wraps, so
10104 // the arm's shared `Interpolation` guard never fired and the
10105 // wrapper was billed alongside each element. Every row below
10106 // scored n2 4 / N2 4 for three operands, the extra entry being
10107 // the wrapper's whole-span text — which made the vocabulary
10108 // depend on how the author grouped the literals.
10109 //
10110 // `assert_ops_operands` pins the operand *text*, not just the
10111 // count: a wrapper coming back would show up as `"one" "two"` /
10112 // `%w[x y]` / `%i[p q]` rather than as an off-by-one number.
10113 //
10114 // The kind assertions are the grammar-dispatch §1 / §2 drift
10115 // marker — a bump that renumbers a wrapper or an element would
10116 // otherwise leave these counts passing while measuring a
10117 // construct the arm no longer names.
10118 //
10119 // Two of the three rows also pin the arm's *membership*:
10120 // dropping `StringArray` or `SymbolArray` from it fails
10121 // `ruby_empty_word_and_symbol_arrays_still_bill_one_operand_1353`.
10122 // `ChainedString`'s membership is unobservable and no test can
10123 // pin it — `repeat1($.string)` guarantees the guard always
10124 // fires, so `Unknown` and "not in the arm at all" are the same
10125 // answer for every input. It is listed for symmetry with its
10126 // siblings, and correct-by-construction is the only defence
10127 // available there.
10128 for (source, wrapper, element, operands) in [
10129 (
10130 "a = \"one\" \"two\"\n",
10131 Ruby::ChainedString,
10132 Ruby::String,
10133 vec!["a", "\"one\"", "\"two\""],
10134 ),
10135 (
10136 "b = %w[x y]\n",
10137 Ruby::StringArray,
10138 Ruby::BareString,
10139 vec!["b", "x", "y"],
10140 ),
10141 (
10142 "c = %i[p q]\n",
10143 Ruby::SymbolArray,
10144 Ruby::BareSymbol,
10145 vec!["c", "p", "q"],
10146 ),
10147 ] {
10148 let parser =
10149 RubyParser::new(source.as_bytes().to_vec(), &PathBuf::from("foo.rb"), None);
10150 assert!(
10151 ast_has_kind_id(&parser, wrapper as u16),
10152 "the container kind this arm gates on is unreachable for `{source}`"
10153 );
10154 assert!(
10155 ast_has_kind_id(&parser, element as u16),
10156 "the element kind this arm gates on is unreachable for `{source}`"
10157 );
10158
10159 // expected: operator `=` → n1 = N1 = 1; operands are the
10160 // assignment target and the two elements → n2 = N2 = 3.
10161 assert_halstead_counts::<RubyParser>(source, "foo.rb", [1, 1, 3, 3], source);
10162 assert_ops_operands::<RubyParser>(source, "foo.rb", 3, operands);
10163 }
10164 }
10165
10166 #[cfg(feature = "ruby")]
10167 #[test]
10168 fn ruby_empty_word_and_symbol_arrays_still_bill_one_operand_1353() {
10169 // The childless spelling, and the whole reason #1353 gates the
10170 // three container kinds instead of dropping them from the arm
10171 // (grammar-dispatch §6). `%w[]` / `%i[]` parse to a wrapper
10172 // holding nothing but its two delimiter tokens, so the tempting
10173 // "just delete the wrapper" fix — the one #1351 was right to
10174 // take for Bash's `command_name` — would score an empty literal
10175 // zero operands where the source plainly has a literal. This is
10176 // the assertion to watch fail against that alternative.
10177 //
10178 // expected: operator `=` → n1 = N1 = 1; operands the assignment
10179 // target and the empty literal itself → n2 = N2 = 2.
10180 for (source, operands) in [
10181 ("d = %w[]\n", vec!["d", "%w[]"]),
10182 ("e = %i[]\n", vec!["e", "%i[]"]),
10183 ] {
10184 assert_halstead_counts::<RubyParser>(source, "foo.rb", [1, 1, 2, 2], source);
10185 assert_ops_operands::<RubyParser>(source, "foo.rb", 2, operands);
10186 }
10187 }
10188
10189 #[cfg(feature = "ruby")]
10190 #[test]
10191 fn ruby_interpolated_array_elements_are_not_double_counted_1353() {
10192 // `bare_string` and `bare_symbol` are two aliases of a single
10193 // grammar production (`_literal_contents`), one per array form,
10194 // so `%W[…]` and `%I[…]` must score identically. Until #1353
10195 // only `bare_string` carried the interpolation guard and
10196 // `bare_symbol` sat in the plain operand arm, so `%I[a#{n}b c]`
10197 // billed the element `a#{n}b` *and* the `n` inside it (n2 4)
10198 // where `%W[a#{n}b c]` billed 3.
10199 //
10200 // Asserted as an invariance over the two spellings, the way
10201 // `ruby_interpolation_opener_is_not_an_operator` is — the
10202 // policy is the claim, not the magic number. The third row is
10203 // the composed case: a `chained_string` whose own guard defers
10204 // to an element that is itself interpolated.
10205 //
10206 // expected per row: operator `=` → n1 = N1 = 1; operands the
10207 // assignment target, the interpolated expression `n`, and the
10208 // one inert element → n2 = N2 = 3. The operand *text* is pinned
10209 // too, because 3 is also the count an implementation that
10210 // suppressed the inert element instead of the wrapper would
10211 // report.
10212 for (source, operands) in [
10213 ("w = %W[a#{n}b c]\n", vec!["w", "n", "c"]),
10214 ("w = %I[a#{n}b c]\n", vec!["w", "n", "c"]),
10215 ("a = \"x#{n}\" \"y\"\n", vec!["a", "n", "\"y\""]),
10216 ] {
10217 assert_halstead_counts::<RubyParser>(source, "foo.rb", [1, 1, 3, 3], source);
10218 assert_ops_operands::<RubyParser>(source, "foo.rb", 3, operands);
10219 }
10220 }
10221
10222 #[cfg(feature = "ruby")]
10223 #[test]
10224 fn ruby_suffixed_numeric_literals_bill_one_operand_1359() {
10225 // #1359, the wrapper/leaf double count one arm below #1353's.
10226 // `rational` and `complex` wrap the numeral they suffix, and
10227 // `get_op_type` billed every level: `a = 1r` scored n2 3 / N2 3
10228 // for two operands, and `c = 3ri` — a `complex` over a
10229 // `rational` over an `integer` — scored n2 3 / N2 4 for the
10230 // same two.
10231 //
10232 // The rows are ordered by nesting depth on purpose, because the
10233 // half-fix is a plausible one to write. Guard the two leaf
10234 // kinds and not the intermediate `rational` — `R::Integer |
10235 // R::Float` where the arm says `R::Integer | R::Float |
10236 // R::Rational` — and the `1r` and `2i` rows still pass; the
10237 // `ri` rows, whose middle `rational` must be suppressed as well
10238 // as the leaf beneath it, are the two that fail (measured:
10239 // `c = 3ri` reports n2 3 / N2 3).
10240 //
10241 // The trailing rows are the ones that must *not* move. `g = 4`
10242 // has no wrapper at all and pins that the guard is scoped to
10243 // the numerals inside one (grammar-dispatch §6 — nothing
10244 // regresses to zero). `"#{4r}"` and `-3r` put the wrapper
10245 // somewhere other than an assignment right-hand side, under a
10246 // node that guards its own children (`Interpolation`, whose
10247 // enclosing `String` is suppressed by the arm above) and under
10248 // one that does not (`unary`); both must still bill the
10249 // wrapper once. They do *not* pin parent-versus-ancestor
10250 // scoping and nothing can: `complex` and `rational` admit
10251 // numeral children only, so an ancestor-scoped guard would
10252 // classify every Ruby input identically. And `1r + 1r` is the
10253 // row where N2 and n2 come apart, so the `N2` column is
10254 // load-bearing on its own rather than tracking `n2`.
10255 //
10256 // The kind assertions are the grammar-dispatch §1 / §2 drift
10257 // marker: a bump that renumbered `complex` or `rational`, or
10258 // stopped nesting them, would leave the counts passing while
10259 // measuring a construct the guard no longer names. `IntOrFloat`
10260 // is the §2 half — the hidden `_int_or_float` the two wrappers
10261 // are written over, which a bump could promote into the
10262 // numeral's parent and silently stop the guard firing. Not
10263 // every kind a row mentions is one the guard names
10264 // (`Interpolation` is context), and `unary` and `binary` are
10265 // deliberately absent: they carry five and three aliased
10266 // variants here, so naming one would pin the alias rather than
10267 // the shape.
10268 for (source, kinds, counts, operands) in [
10269 (
10270 "a = 1r\n",
10271 &[Ruby::Rational as u16, Ruby::Integer as u16][..],
10272 [1, 1, 2, 2],
10273 vec!["a", "1r"],
10274 ),
10275 (
10276 "b = 2i\n",
10277 &[Ruby::Complex as u16, Ruby::Integer as u16][..],
10278 [1, 1, 2, 2],
10279 vec!["b", "2i"],
10280 ),
10281 (
10282 "c = 3ri\n",
10283 &[
10284 Ruby::Complex as u16,
10285 Ruby::Rational as u16,
10286 Ruby::Integer as u16,
10287 ][..],
10288 [1, 1, 2, 2],
10289 vec!["c", "3ri"],
10290 ),
10291 (
10292 "d = 1.5r\n",
10293 &[Ruby::Rational as u16, Ruby::Float as u16][..],
10294 [1, 1, 2, 2],
10295 vec!["d", "1.5r"],
10296 ),
10297 (
10298 "e = 2.5i\n",
10299 &[Ruby::Complex as u16, Ruby::Float as u16][..],
10300 [1, 1, 2, 2],
10301 vec!["e", "2.5i"],
10302 ),
10303 (
10304 "f = 1.5ri\n",
10305 &[
10306 Ruby::Complex as u16,
10307 Ruby::Rational as u16,
10308 Ruby::Float as u16,
10309 ][..],
10310 [1, 1, 2, 2],
10311 vec!["f", "1.5ri"],
10312 ),
10313 (
10314 "g = 4\n",
10315 &[Ruby::Integer as u16][..],
10316 [1, 1, 2, 2],
10317 vec!["g", "4"],
10318 ),
10319 (
10320 "h = \"#{4r}\"\n",
10321 &[Ruby::Interpolation as u16, Ruby::Rational as u16][..],
10322 [1, 1, 2, 2],
10323 vec!["h", "4r"],
10324 ),
10325 (
10326 "i = -3r\n",
10327 &[Ruby::Rational as u16, Ruby::Integer as u16][..],
10328 [2, 2, 2, 2],
10329 vec!["i", "3r"],
10330 ),
10331 (
10332 "j = 1r + 1r\n",
10333 &[Ruby::Rational as u16, Ruby::Integer as u16][..],
10334 [2, 2, 2, 3],
10335 vec!["j", "1r"],
10336 ),
10337 ] {
10338 let parser =
10339 RubyParser::new(source.as_bytes().to_vec(), &PathBuf::from("foo.rb"), None);
10340 for kind in kinds {
10341 assert!(
10342 ast_has_kind_id(&parser, *kind),
10343 "the kind this row measures is unreachable for `{source}`"
10344 );
10345 }
10346 assert!(
10347 !ast_has_kind_id(&parser, Ruby::IntOrFloat as u16),
10348 "`_int_or_float` is hidden; a grammar that emits it would \
10349 sit between the wrapper and the numeral for `{source}`"
10350 );
10351
10352 assert_halstead_counts::<RubyParser>(source, "foo.rb", counts, source);
10353 assert_ops_operands::<RubyParser>(source, "foo.rb", operands.len(), operands);
10354 }
10355 }
10356
10357 #[cfg(feature = "ruby")]
10358 #[test]
10359 fn ruby_numeric_suffixes_stay_distinct_operands_1359() {
10360 // The reason #1359 keeps the *wrapper* and gates the leaf
10361 // rather than dropping the two wrapper kinds from the arm, as
10362 // #1351 and #1358 did for their Bash wrappers. `1`, `1r`, `1i`
10363 // and `1ri` are four distinct Ruby constants — Integer,
10364 // Rational, Complex and Complex-of-Rational — and only the
10365 // wrapper's span carries the suffix that says which.
10366 //
10367 // This is the assertion to watch fail against the leaf-keeping
10368 // alternative: billing the `integer` would file all four under
10369 // the operand text `1`, reporting n2 5 / N2 8 where the source
10370 // plainly has eight distinct operands. The count alone does not
10371 // say that, so the vocabulary is pinned by text.
10372 //
10373 // expected: operator `=` → n1 1, N1 4; four targets and four
10374 // constants, none repeated → n2 = N2 = 8.
10375 let source = "w = 1\nx = 1r\ny = 1i\nz = 1ri\n";
10376 assert_halstead_counts::<RubyParser>(source, "foo.rb", [1, 4, 8, 8], source);
10377 assert_ops_operands::<RubyParser>(
10378 source,
10379 "foo.rb",
10380 8,
10381 vec!["w", "x", "y", "z", "1", "1r", "1i", "1ri"],
10382 );
10383 }
10384
10385 /// Comprehensive iRules Halstead test exercising every operator family
10386 /// classified in `get_op_type`: declaration/control keywords (`proc`,
10387 /// `set`, `if`, `return`), structural punctuation (`{}` `[]` `()`),
10388 /// arithmetic (`+`), comparison (`>`), the word-form string comparator
10389 /// (`eq`), and short-circuit logical (`&&`). Anchored on the integer
10390 /// `n1`/`N1`/`n2`/`N2` headline values; the float fields are derived and
10391 /// bit-brittle, so they are not pinned.
10392 ///
10393 /// The second half pins the lesson-4 invariant: the independent
10394 /// text-keyed `operands_and_operators` store must dedupe to the same
10395 /// `n1`/`n2`. A classification change that moved one store without the
10396 /// other (e.g. a kind landing in both the operator and operand arms)
10397 /// would break this even though the snapshot stayed green.
10398 #[cfg(feature = "irules")]
10399 #[test]
10400 fn irules_operators_and_operands() {
10401 let source = "proc f { a b } {
10402 set x [expr { $a + $b }]
10403 if { $x > 0 && $a eq \"go\" } {
10404 return $x
10405 }
10406 return 0
10407}
10408";
10409 check_metrics::<IrulesParser>(source, "foo.irule", |metric| {
10410 // After #695 only opening delimiters count: the `}`/`]`
10411 // closers no longer add operators (was 12 unique / 20 total).
10412 assert_eq!(metric.halstead.unique_operators(), 10);
10413 assert_eq!(metric.halstead.total_operators(), 14);
10414 // Operands fell 12 / 16 → 10 / 14 with #1354: the proc body
10415 // and the `if` body are `BracedWord` script kinds and are no
10416 // longer operands beside the commands they contain.
10417 assert_eq!(metric.halstead.unique_operands(), 10);
10418 assert_eq!(metric.halstead.total_operands(), 14);
10419 });
10420
10421 let path = PathBuf::from("foo.irule");
10422 let parser = IrulesParser::new(source.as_bytes().to_vec(), &path, None);
10423 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
10424 let unique_operators: HashSet<&str> = ops.operators.iter().map(String::as_str).collect();
10425 let unique_operands: HashSet<&str> = ops.operands.iter().map(String::as_str).collect();
10426 assert_eq!(
10427 unique_operators.len(),
10428 10,
10429 "dedupe(ops.operators) must equal n1; operators were {:?}",
10430 ops.operators
10431 );
10432 assert_eq!(
10433 unique_operands.len(),
10434 10,
10435 "dedupe(ops.operands) must equal n2; operands were {:?}",
10436 ops.operands
10437 );
10438 }
10439
10440 /// An inert `"hello world"` double-quoted string (no `$var` / `[cmd]`
10441 /// interpolation child) contributes exactly **one** operand — the
10442 /// wrapping `QuotedWord`. Operands are `f`, `s` and `"hello world"` —
10443 /// n2 = 3, the same as Tcl since #1294 restored its `set` target to
10444 /// the operand count and #1354 dropped the proc-body `braced_word`
10445 /// from both. Mirrors `tcl_inert_quoted_word_counts_as_operand`
10446 /// (#277).
10447 #[cfg(feature = "irules")]
10448 #[test]
10449 fn irules_inert_quoted_word_counts_as_operand() {
10450 let source = "proc f {} {\n set s \"hello world\"\n}\n";
10451 check_metrics::<IrulesParser>(source, "foo.irule", |metric| {
10452 // After #695 only the `{` opener counts; the `}` closer no
10453 // longer adds an operator (was 4 unique / 6 total).
10454 assert_eq!(metric.halstead.unique_operators(), 3);
10455 assert_eq!(metric.halstead.total_operators(), 4);
10456 assert_eq!(metric.halstead.unique_operands(), 3);
10457 assert_eq!(metric.halstead.total_operands(), 3);
10458 });
10459
10460 let path = PathBuf::from("foo.irule");
10461 let parser = IrulesParser::new(source.as_bytes().to_vec(), &path, None);
10462 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
10463 // The inert quoted word is present as exactly one operand (not
10464 // dropped, not split): dropping it would mean the inert branch was
10465 // over-guarded.
10466 let quoted = ops
10467 .operands
10468 .iter()
10469 .filter(|o| o.as_str() == "\"hello world\"")
10470 .count();
10471 assert_eq!(quoted, 1, "inert quoted word must be one operand");
10472 let unique_operands: HashSet<&str> = ops.operands.iter().map(String::as_str).collect();
10473 assert_eq!(unique_operands.len(), 3, "operands were {:?}", ops.operands);
10474 }
10475
10476 /// Regression for the `QuotedWord` interpolation guard (the #277 /
10477 /// Bash-#180 / C#-#183 / PHP-#184 pattern). An interpolated
10478 /// `"$x is $y"` must contribute **zero** operands for the wrapping
10479 /// `QuotedWord`; the inner `$x` / `$y` `variable_substitution` nodes are
10480 /// walked separately and count on their own. Operands are `f`, `x`, `y`,
10481 /// `s`, `$x`, `$y` = 6 (7 before #1354 dropped the proc-body
10482 /// `braced_word`). If the guard regressed (wrapper classified
10483 /// `Operand`), the wrapper string would add a 7th operand. This is the
10484 /// branch that had no test before.
10485 #[cfg(feature = "irules")]
10486 #[test]
10487 fn irules_interpolated_quoted_word_no_double_count() {
10488 let source = "proc f {x y} {\n set s \"$x is $y\"\n}\n";
10489 check_metrics::<IrulesParser>(source, "foo.irule", |metric| {
10490 // After #695 only the `{` opener counts; the `}` closer no
10491 // longer adds an operator (was 4 unique / 6 total).
10492 assert_eq!(metric.halstead.unique_operators(), 3);
10493 assert_eq!(metric.halstead.total_operators(), 4);
10494 assert_eq!(metric.halstead.unique_operands(), 6);
10495 assert_eq!(metric.halstead.total_operands(), 6);
10496 });
10497
10498 let path = PathBuf::from("foo.irule");
10499 let parser = IrulesParser::new(source.as_bytes().to_vec(), &path, None);
10500 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
10501 // The wrapping interpolated string must NOT appear as an operand;
10502 // its inner substitutions must. The wrapper, if wrongly counted,
10503 // would surface as the quoted literal `"$x is $y"` (with quotes,
10504 // like the inert `"hello world"` operand). Match that exact token —
10505 // a substring check would false-match the proc-body `braced_word`
10506 // operand, which legitimately contains the source text.
10507 assert!(
10508 !ops.operands.iter().any(|o| o.as_str() == "\"$x is $y\""),
10509 "interpolated wrapper must not be an operand; operands were {:?}",
10510 ops.operands
10511 );
10512 assert!(
10513 ops.operands.iter().any(|o| o.as_str() == "$x")
10514 && ops.operands.iter().any(|o| o.as_str() == "$y"),
10515 "inner $x / $y substitutions must each be operands; operands were {:?}",
10516 ops.operands
10517 );
10518 let unique_operands: HashSet<&str> = ops.operands.iter().map(String::as_str).collect();
10519 assert_eq!(unique_operands.len(), 6, "operands were {:?}", ops.operands);
10520 }
10521
10522 /// Exercises the operator families not covered by
10523 /// `irules_operators_and_operands`: bitwise (`& | ^ ~ << >>`), ternary
10524 /// (`? :`), the keyword string comparators (`starts_with`, `ends_with`,
10525 /// `contains`, `matches`, `eq`, `ne`), and the keyword logical operator
10526 /// (`and`). Pins every operator-family arm in `get_op_type` plus the
10527 /// lesson-4 dedupe invariant.
10528 #[cfg(feature = "irules")]
10529 #[test]
10530 fn irules_bitwise_ternary_string_ops() {
10531 let source = "proc f { a b } {
10532 set bits [expr { $a & $b | $a ^ ~$b }]
10533 set sh [expr { $a << 2 | $b >> 1 }]
10534 set t [expr { $a > 0 ? $a : $b }]
10535 if { $a starts_with \"x\" && $b ends_with \"y\" } { return 1 }
10536 if { $a contains \"z\" || $b matches \"q\" } { return 2 }
10537 if { $a eq \"m\" and $b ne \"n\" } { return 3 }
10538 return $b
10539}
10540";
10541 check_metrics::<IrulesParser>(source, "foo.irule", |metric| {
10542 // After #695 only opening delimiters count: the `}`/`]`
10543 // closers no longer add operators (was 26 unique / 57 total).
10544 assert_eq!(metric.halstead.unique_operators(), 24);
10545 assert_eq!(metric.halstead.total_operators(), 43);
10546 // Operands fell 23 / 42 → 19 / 38 with #1354: the proc body
10547 // and the three single-statement `if` bodies are
10548 // `BracedWord` script kinds and no longer count as operands.
10549 assert_eq!(metric.halstead.unique_operands(), 19);
10550 assert_eq!(metric.halstead.total_operands(), 38);
10551 });
10552
10553 let path = PathBuf::from("foo.irule");
10554 let parser = IrulesParser::new(source.as_bytes().to_vec(), &path, None);
10555 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
10556 let unique_operators: HashSet<&str> = ops.operators.iter().map(String::as_str).collect();
10557 let unique_operands: HashSet<&str> = ops.operands.iter().map(String::as_str).collect();
10558 assert_eq!(
10559 unique_operators.len(),
10560 24,
10561 "dedupe(ops.operators) must equal n1; operators were {:?}",
10562 ops.operators
10563 );
10564 assert_eq!(
10565 unique_operands.len(),
10566 19,
10567 "dedupe(ops.operands) must equal n2; operands were {:?}",
10568 ops.operands
10569 );
10570 }
10571
10572 /// A bare `$x` produces one `variable_substitution` operand. Its inner
10573 /// `id` leaf (the *named* `Id` node — not the anonymous `Id2` token Tcl
10574 /// has there) must NOT be counted separately, or every variable
10575 /// reference double-counts. `get_op_type` excludes `Id` whose parent is
10576 /// a `VariableSubstitution`. Operands: `f`, the proc arg `x`, `return`
10577 /// and `$x` — four, with no duplicate (`total_operands()` == 4; the
10578 /// proc-body `braced_word` was a fifth before #1354). If the guard
10579 /// regressed, the inner `id` "x" would add a fifth operand occurrence
10580 /// (it text-collides with the proc arg `x`, so `u_operands` would stay
10581 /// 4 but `total_operands()` would rise to 5 — hence the total, not just
10582 /// the unique count, is asserted).
10583 #[cfg(feature = "irules")]
10584 #[test]
10585 fn irules_array_reference_bills_the_reference_and_the_index() {
10586 // The iRules twin of
10587 // `tcl_array_reference_bills_the_reference_and_the_index`. Until
10588 // `ArrayIndex` left the operand arm this fixture billed `(k)` and
10589 // `($i)` beside the six operands below — the grammar-dispatch §5
10590 // wrapper-plus-leaf count, and a divergence from Tcl.
10591 assert_ops_operands::<IrulesParser>(
10592 "set arr(k) 1\nset z \"$arr($i)\"\n",
10593 "foo.irule",
10594 6,
10595 vec!["arr", "k", "1", "z", "$arr($i)", "$i"],
10596 );
10597 }
10598
10599 #[cfg(feature = "irules")]
10600 #[test]
10601 fn irules_bare_variable_operand() {
10602 let source = "proc f {x} {\n return $x\n}\n";
10603 check_metrics::<IrulesParser>(source, "foo.irule", |metric| {
10604 // After #695 only the `{` opener counts (folded `{}`); the
10605 // `}` closer no longer adds an operator (was 3 unique / 5 total).
10606 assert_eq!(metric.halstead.unique_operators(), 2);
10607 assert_eq!(metric.halstead.total_operators(), 3);
10608 assert_eq!(metric.halstead.unique_operands(), 4);
10609 assert_eq!(metric.halstead.total_operands(), 4);
10610 });
10611
10612 let path = PathBuf::from("foo.irule");
10613 let parser = IrulesParser::new(source.as_bytes().to_vec(), &path, None);
10614 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
10615 let bare_var = ops.operands.iter().filter(|o| o.as_str() == "$x").count();
10616 assert_eq!(
10617 bare_var, 1,
10618 "bare $x must be exactly one operand (inner id leaf not double-counted); operands were {:?}",
10619 ops.operands
10620 );
10621 }
10622
10623 #[cfg(feature = "irules")]
10624 #[test]
10625 fn irules_braced_word_delimiter_is_not_an_operator() {
10626 // Regression: issue #1314. The iRules twin of
10627 // `tcl_braced_word_delimiter_is_not_an_operator` — the two
10628 // getters are deliberate clones, so the guard lands in both and
10629 // is asserted in both.
10630 //
10631 // One fixture covers the guard and its control: `{braced word}`
10632 // and `{y}` are values whose openers must not count, while the
10633 // handler body, the `if` condition and the `if` body are
10634 // `BracedWord` / `Expr` and must keep theirs. A guard keyed on
10635 // the brace alone would take `{}` out of the operator set
10636 // entirely and fail here.
10637 //
10638 // expected: operators `when`, `set` × 2, `if`, `contains`,
10639 // `[]`, `{}` × 3 → n1 = 6, N1 = 9. Every operand is distinct →
10640 // n2 = N2 = 7: `HTTP_REQUEST`, `a`, `b`, `HTTP::uri`, `"x"` and
10641 // the two braced words `{braced word}` and `{y}`. Before the
10642 // guard the two value openers added two more `{}` occurrences →
10643 // N1 = 11; before #1354 the handler and `if` bodies and the
10644 // inner `braced` / `word` / `y` were operands too → n2 = 12.
10645 check_metrics::<IrulesParser>(
10646 "when HTTP_REQUEST {\n set a {braced word}\n if {[HTTP::uri] contains \"x\"} { set b {y} }\n}\n",
10647 "foo.irule",
10648 |metric| {
10649 assert_eq!(metric.halstead.unique_operators(), 6);
10650 assert_eq!(metric.halstead.total_operators(), 9);
10651 assert_eq!(metric.halstead.unique_operands(), 7);
10652 assert_eq!(metric.halstead.total_operands(), 7);
10653 },
10654 );
10655 }
10656
10657 /// Regression for #563: the two Halstead `Display` labels must use the
10658 /// underscore key that matches the JSON/CSV field name, so a user can grep
10659 /// the same token across `Display` and JSON. The space-separated forms
10660 /// (`estimated program length` / `purity ratio`) were the only outliers,
10661 /// mirroring the `dump` fix in #562.
10662 #[cfg(feature = "cpp")]
10663 #[test]
10664 fn display_halstead_labels_use_underscore_keys() {
10665 check_metrics::<CppParser>("int a = 42;", "foo.cpp", |metric| {
10666 let out = metric.halstead.to_string();
10667 assert!(
10668 out.contains("estimated_program_length: "),
10669 "Display must use the underscore key `estimated_program_length`:\n{out}"
10670 );
10671 assert!(
10672 out.contains("purity_ratio: "),
10673 "Display must use the underscore key `purity_ratio`:\n{out}"
10674 );
10675 assert!(
10676 !out.contains("estimated program length"),
10677 "Display must not emit the space-separated `estimated program length`:\n{out}"
10678 );
10679 assert!(
10680 !out.contains("purity ratio"),
10681 "Display must not emit the space-separated `purity ratio`:\n{out}"
10682 );
10683 });
10684 }
10685
10686 /// `@"…"` is one `string_literal` holding its `@` as a child, so the
10687 /// literal is the operand, keyed by its whole text, and the marker is
10688 /// not an operator on top — it was, which planted a phantom `@` in
10689 /// n1 for a file whose only `@` was in NSString literals and billed
10690 /// the same byte in both streams. Boxing (`@42`) keeps its `@`: there
10691 /// the token is a child of the `at_expression`, not of the literal.
10692 #[cfg(feature = "objc")]
10693 #[test]
10694 fn objc_nsstring_literal_is_one_operand() {
10695 // expected: [n1, N1, n2, N2], each operator count including the
10696 // wrapper's `@implementation` (#1552). Before the guard the first
10697 // two rows read [9, 9, 5, 5] and [9, 10, 6, 6] — one `@` operator
10698 // per literal; the boxing control is unchanged.
10699 let cases = [
10700 ("NSString *s = @\"str\";", [8, 8, 5, 5]),
10701 ("NSString *t = @\"x\" @\"y\";", [8, 8, 6, 6]),
10702 ("NSNumber *n = @42;", [9, 9, 5, 5]),
10703 ];
10704 for (body, counts) in cases {
10705 let source = format!("@implementation Foo\n- (void)m {{\n {body}\n}}\n@end\n");
10706 assert_halstead_counts::<ObjcParser>(&source, "foo.m", counts, body);
10707 }
10708 assert_ops_operands::<ObjcParser>(
10709 "@implementation Foo\n- (void)m {\n NSString *s = @\"str\";\n}\n@end\n",
10710 "foo.m",
10711 5,
10712 vec!["Foo", "m", "NSString", "s", "@\"str\""],
10713 );
10714 }
10715
10716 /// Comprehensive Objective-C Halstead fixture exercising a message
10717 /// send (`[self log:@"hi"]`), an ObjC string literal (`@"hi"`), an
10718 /// `if`, a short-circuit `&&`, arithmetic (`+`), comparisons, and
10719 /// assignment. Pins every field and enforces the lesson-4 invariants
10720 /// `unique_operators == n1` / `unique_operands == n2` via the
10721 /// independent `--ops` store.
10722 #[cfg(feature = "objc")]
10723 #[test]
10724 fn objc_operators_and_operands() {
10725 let source = "@implementation Foo
10726- (int)bar:(int)x {
10727 int y = x + 1;
10728 if (x > 0 && y < 10) {
10729 [self log:@\"hi\"];
10730 }
10731 return y;
10732}
10733@end
10734";
10735 check_metrics::<ObjcParser>(source, "foo.m", |metric| {
10736 // n1 = 15 unique operators:
10737 // `&&`, `()`, `+`, `-`, `:`, `;`, `<`, `=`, `>`,
10738 // `[]` (message send), `if`, `int`, `return`, `{}`,
10739 // `@implementation` (#1552).
10740 // The `@` of `@"hi"` is part of the literal's operand
10741 // key, not an operator (see `objc_nsstring_literal_is_one_operand`).
10742 // n2 = 10 unique operands:
10743 // `Foo`, `bar`, `log`, `self`, `x`, `y`, `0`, `1`, `10`,
10744 // `@"hi"` (the ObjC string literal).
10745 assert_eq!(metric.halstead.unique_operators(), 15);
10746 assert_eq!(metric.halstead.unique_operands(), 10);
10747 insta::assert_json_snapshot!(metric.halstead, @r#"
10748 {
10749 "unique_operators": 15,
10750 "total_operators": 23,
10751 "unique_operands": 10,
10752 "total_operands": 14,
10753 "length": 37,
10754 "estimated_program_length": 91.82263988300141,
10755 "purity_ratio": 2.481692969810849,
10756 "vocabulary": 25,
10757 "volume": 171.8226790216648,
10758 "difficulty": 10.5,
10759 "level": 0.09523809523809523,
10760 "effort": 1804.1381297274804,
10761 "time": 100.22989609597113,
10762 "bugs": 0.049399808887691035
10763 }
10764 "#);
10765 });
10766 // Lesson-4 invariant: dedupe(ops.operands) == n2 (10), via the
10767 // independent text-keyed `--ops` store.
10768 assert_ops_operands::<ObjcParser>(
10769 source,
10770 "foo.m",
10771 10,
10772 vec![
10773 "Foo", "bar", "log", "self", "x", "y", "0", "1", "10", "@\"hi\"",
10774 ],
10775 );
10776 }
10777
10778 /// #1316 fixture separating the vocabulary and occurrence axes
10779 /// (#1294): `'x'` appears twice, so a correct fix adds four `n2`
10780 /// entries and five `N2` hits. The `'ab'` multi-character constant
10781 /// is also the grammar-dispatch section 5 pin — that literal carries
10782 /// *two* `character` children and must still bill one operand.
10783 ///
10784 /// Both #1316 fixtures are plain C, which every C-family grammar
10785 /// parses to the same shape, so one source proves the same thing
10786 /// about each of the four clones.
10787 #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
10788 const C_FAMILY_CHAR_REPEATS: &str =
10789 "char a = 'x';\nchar b = 'x';\nchar c = 'y';\nchar d = '\\n';\nint e = 'ab';\n";
10790
10791 /// #1316 fixture holding all five spellings the grammars admit. Each
10792 /// opens on a distinct delimiter kind (`'`, `L'`, `u'`, `U'`, `u8'`),
10793 /// and operands key on source text, so the five are five vocabulary
10794 /// entries rather than one.
10795 #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
10796 const C_FAMILY_CHAR_PREFIXES: &str =
10797 "char a = 'x';\nchar b = L'x';\nchar c = u'x';\nchar d = U'x';\nchar e = u8'x';\n";
10798
10799 /// Asserts both #1316 fixtures for one C-family language, through
10800 /// the metrics store *and* the text-keyed `--ops` store (the
10801 /// lesson-4 invariant `n2 == len(dedupe(ops.operands))`).
10802 ///
10803 /// The `--ops` half pins *which text* each literal is billed under,
10804 /// which the counts cannot see: billing a literal's `character`
10805 /// payload instead of the whole literal keeps `n2` at 9 while the
10806 /// vocabulary silently becomes `x` rather than `'x'`. (Billing
10807 /// *both* is caught earlier, by the counts.) It is a second *walk*
10808 /// rather than a second classification — `ops_inner` reads the keys
10809 /// of the same `HalsteadMaps` — which is worth knowing before
10810 /// reading it as independent corroboration of the count.
10811 #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
10812 #[track_caller]
10813 fn assert_char_literal_operands<T: crate::MetricSuite>(file: &str, label: &str) {
10814 // `char` x4 and `int` are text-keyed primitive operators, so
10815 // n1 = 4 (`;`, `=`, `char`, `int`) and N1 = 5 + 5 + 4 + 1.
10816 // Operands: `a`..`e`, plus `'x'` (twice), `'y'`, `'\n'`, `'ab'`.
10817 let repeats = format!("{label}: repeated / escaped / multi-char literals");
10818 assert_halstead_counts::<T>(C_FAMILY_CHAR_REPEATS, file, [4, 15, 9, 10], &repeats);
10819 assert_ops_operands::<T>(
10820 C_FAMILY_CHAR_REPEATS,
10821 file,
10822 9,
10823 vec!["a", "b", "c", "d", "e", "'x'", "'y'", "'\\n'", "'ab'"],
10824 );
10825
10826 // Every declaration is `char` here, so the `int` primitive
10827 // operator of the other fixture is gone and n1 drops to 3. Ten
10828 // distinct operands, each seen once.
10829 let prefixes = format!("{label}: L / u / U / u8 prefixed literals");
10830 assert_halstead_counts::<T>(C_FAMILY_CHAR_PREFIXES, file, [3, 15, 10, 10], &prefixes);
10831 assert_ops_operands::<T>(
10832 C_FAMILY_CHAR_PREFIXES,
10833 file,
10834 10,
10835 vec![
10836 "a", "b", "c", "d", "e", "'x'", "L'x'", "u'x'", "U'x'", "u8'x'",
10837 ],
10838 );
10839 }
10840
10841 /// Regression for #1316: a C-family character literal is a Halstead
10842 /// operand.
10843 ///
10844 /// `char_literal` was in no arm of `CCode` / `CppCode` /
10845 /// `MozcppCode` / `ObjcCode`'s `get_op_type`, so a character literal
10846 /// contributed *nothing* — not an operator (correct) and not an
10847 /// operand (wrong) — while Rust, Java, Kotlin, C#, Go and Elixir all
10848 /// counted theirs. Both fixtures measured `n2` 5, `N2` 5 before the
10849 /// fix: the five declared identifiers and not one literal.
10850 ///
10851 /// Each language asserts separately over the same source rather than
10852 /// sharing one call, so reverting one clone's arm fails that row
10853 /// alone (grammar-dispatch section 11) — verified by perturbing each
10854 /// of the four arms in turn. A single shared assertion would be
10855 /// satisfied by whichever clone still had the arm.
10856 ///
10857 /// Mozcpp owns no file extension, so no integration snapshot ever
10858 /// reaches its clone; its row is the whole coverage that arm has.
10859 #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
10860 #[test]
10861 fn c_family_char_literals_are_operands() {
10862 assert_char_literal_operands::<CParser>("chars.c", "c");
10863 assert_char_literal_operands::<CppParser>("chars.cpp", "cpp");
10864 assert_char_literal_operands::<MozcppParser>("chars.cpp", "mozcpp");
10865 assert_char_literal_operands::<ObjcParser>("chars.m", "objc");
10866 }
10867
10868 /// ObjC boxes a character literal as `@'y'` — an `at_expression`
10869 /// wrapping the same `char_literal`, with the `@` counted as its own
10870 /// operator. The wrapper is in no operand arm, so the boxed form
10871 /// bills exactly the literal it wraps and stays distinct from a bare
10872 /// one (#1316).
10873 #[cfg(feature = "objc")]
10874 #[test]
10875 fn objc_boxed_char_literal_is_one_operand() {
10876 let source = "char a = 'x';\nid b = @'y';\n";
10877 // n1: `char`, `=`, `;`, `@`. N1: 1 + 2 + 2 + 1.
10878 // n2 / N2: `a`, `b`, `'x'`, `'y'` — `id` is a `typedefed_specifier`
10879 // and is classified by neither arm. Before #1316 this was n2 2,
10880 // N2 2.
10881 assert_halstead_counts::<ObjcParser>(source, "boxed.m", [4, 6, 4, 4], "objc @'y'");
10882 assert_ops_operands::<ObjcParser>(source, "boxed.m", 4, vec!["a", "b", "'x'", "'y'"]);
10883 }
10884
10885 /// Walks both #1316 fixtures under all four C-family `Getter`s and
10886 /// pins the two grammar facts the new operand arm rests on.
10887 ///
10888 /// * **Grammar-dispatch section 1.** In the positions these
10889 /// fixtures exercise, every node the grammar spells `char_literal`
10890 /// carries the one `kind_id` the arm lists. That is the weaker
10891 /// half of the alias evidence — an alias arises in a *different*
10892 /// syntactic position, which no fixture can enumerate. The strong
10893 /// half is that these generated enums do carry numeric-suffix
10894 /// aliases in quantity (`language_c.rs` alone has ninety) and none
10895 /// of the four spells a `CharLiteral2`, so its absence is a
10896 /// measurement rather than a silence. This loop is what notices if
10897 /// a grammar bump changes that under an existing fixture.
10898 /// * **Grammar-dispatch section 5.** No child of a `char_literal` is
10899 /// classified. That is what makes listing the wrapper safe rather
10900 /// than a wrapper/leaf double count: the opening delimiter, the
10901 /// closing `'`, and the `character` / `escape_sequence` payload
10902 /// must all stay `Unknown`, or every literal would bill two
10903 /// operands and a prefixed one three.
10904 ///
10905 /// Both loops are non-vacuous by assertion, since a fixture that
10906 /// stopped containing a character literal would otherwise make this
10907 /// test pass having checked nothing.
10908 #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
10909 #[test]
10910 fn c_family_char_literal_internals_stay_unclassified() {
10911 #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
10912 fn check<L: LanguageInfo + Getter>(char_literal: u16, label: &str) {
10913 let mut literals = 0_usize;
10914 let mut children = 0_usize;
10915 for source in [C_FAMILY_CHAR_REPEATS, C_FAMILY_CHAR_PREFIXES] {
10916 for_each_node_with_chain::<L>(source.as_bytes(), |node, chain| {
10917 if node.kind() == "char_literal" {
10918 assert_eq!(
10919 node.kind_id(),
10920 char_literal,
10921 "{label}: a `char_literal` carries kind_id {} rather than the \
10922 {char_literal} the operand arm lists — an alias the arm cannot see",
10923 node.kind_id()
10924 );
10925 literals += 1;
10926 }
10927 if chain
10928 .last()
10929 .is_none_or(|parent| parent.kind_id() != char_literal)
10930 {
10931 return;
10932 }
10933 children += 1;
10934 // `_with_code` is the spelling `compute_halstead`
10935 // calls. The default forwards to the byte-less form,
10936 // so today the two agree for every C-family
10937 // language — which is exactly why asking the wrong
10938 // one would read as correct right up until one of
10939 // these four grew an override (grammar-dispatch
10940 // section 7).
10941 assert!(
10942 matches!(
10943 L::get_op_type_with_code(
10944 node,
10945 source.as_bytes(),
10946 Ancestors::known(chain)
10947 ),
10948 TokenRole::Unknown
10949 ),
10950 "{label}: `{}` inside a character literal is classified, so the \
10951 literal now double-counts against its wrapper",
10952 node.kind()
10953 );
10954 });
10955 }
10956 // Ten literals across the two fixtures; each holds two
10957 // delimiters plus at least one payload leaf, and `'ab'` two.
10958 assert_eq!(literals, 10, "{label}: fixtures lost a character literal");
10959 assert_eq!(children, 31, "{label}: fixtures lost a literal's internals");
10960 }
10961
10962 check::<CCode>(C::CharLiteral as u16, "c");
10963 check::<CppCode>(Cpp::CharLiteral as u16, "cpp");
10964 check::<MozcppCode>(Mozcpp::CharLiteral as u16, "mozcpp");
10965 check::<ObjcCode>(Objc::CharLiteral as u16, "objc");
10966 }
10967
10968 /// #1361 fixture pairing the two spellings of a member access whose
10969 /// receiver is the enclosing object: `this->x` and `p->x`.
10970 ///
10971 /// The pairing is the point, not decoration. `field_expression` is
10972 /// `<receiver> -> <field>`, so the two lines are the same AST shape
10973 /// and must bill the same *kinds* of thing — a receiver operand, the
10974 /// `->` operator, a field operand. Before the fix `this` was in
10975 /// neither arm, so the two shapes scored differently and `m1` billed
10976 /// a `->` with only one operand around it.
10977 ///
10978 /// Plain C++ that the mozcpp fork parses identically, so one source
10979 /// proves the same thing about both clones.
10980 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
10981 const CPP_THIS_RECEIVER_PARITY: &str = "struct S {
10982 int x;
10983 int m1() { return this->x; }
10984 int m2(S* p) { return p->x; }
10985 };";
10986
10987 /// #1361 fixture holding one representative position per kind of
10988 /// container the pinned `tree-sitter-cpp` can put a `this` in: the
10989 /// `->` receiver, a dereference inside a parenthesised expression, a
10990 /// bare return value, both lambda-capture spellings (`[this]` and
10991 /// `[=, this]`), a `decltype` trailing return type, a constructor
10992 /// body following a member-initialiser list, and a call argument.
10993 ///
10994 /// Representative, not exhaustive — `delete this;`, `throw this;`,
10995 /// `[&, this]`, a default member initialiser and a `this->template
10996 /// f<int>()` all parse cleanly too. Every one of them is the same
10997 /// childless `{this:215}` leaf under an unclassified wrapper, which
10998 /// is precisely why the arm needs no guard and why enumerating them
10999 /// would add rows without adding a distinct path (grammar-dispatch
11000 /// section 11).
11001 ///
11002 /// C++23's explicit object parameter (`int g(this S&& self)`) is
11003 /// deliberately absent: the pinned grammar cannot parse it and emits
11004 /// `type_identifier "this"` plus an `ERROR`, which
11005 /// `for_each_node_with_chain` rejects outright. That spelling is
11006 /// already an operand through `TypeIdentifier`, so the construct is
11007 /// unaffected by this arm either way.
11008 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
11009 const CPP_THIS_POSITIONS: &str = "struct S {
11010 int x;
11011 void g(S*);
11012 int arrow() { return this->x; }
11013 int deref() { return (*this).x; }
11014 S* ret() { return this; }
11015 void cap() { auto l = [this]() { return x; }; }
11016 void cap2() { auto l = [=, this]() { return x; }; }
11017 auto trail() -> decltype(this->x) { return x; }
11018 S(int v) : x(v) { this->x = v; }
11019 void call() { g(this); }
11020 };";
11021
11022 /// Asserts `CPP_THIS_RECEIVER_PARITY` for one C++-family language,
11023 /// through the metrics store *and* the text-keyed `--ops` store
11024 /// (the lesson-4 invariant `n2 == len(dedupe(ops.operands))`).
11025 ///
11026 /// The `--ops` half pins *which text* the new operand is billed
11027 /// under, which the counts alone cannot see: billing the enclosing
11028 /// `field_expression` instead of the `this` leaf would hold `n2` at
11029 /// 6 while the vocabulary silently became `this->x`.
11030 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
11031 #[track_caller]
11032 fn assert_this_receiver_parity<T: crate::MetricSuite>(file: &str, label: &str) {
11033 // Operators, keyed by kind_id except the text-keyed primitives:
11034 // `{` x3 (class body, `m1`, `m2`), `int` x3, `;` x4 (the field,
11035 // the two returns, the struct terminator), `(` x2, `return` x2,
11036 // `->` x2, the one `*` of `S* p`, and `struct` (#1552). #695
11037 // dropped every closing delimiter. n1 = 8, N1 = 18.
11038 //
11039 // Operands, keyed by source text: `S` x2 (the name and the
11040 // parameter type), `x` x3, `m1`, `m2`, `p` x2, and — the fix —
11041 // `this` x1. n2 = 6, N2 = 10. Before #1361: n2 5, N2 9, with
11042 // every operator count identical, which is the whole claim.
11043 let counts = format!("{label}: this->x and p->x are the same shape");
11044 assert_halstead_counts::<T>(CPP_THIS_RECEIVER_PARITY, file, [8, 18, 6, 10], &counts);
11045 assert_ops_operands::<T>(
11046 CPP_THIS_RECEIVER_PARITY,
11047 file,
11048 6,
11049 vec!["S", "x", "m1", "m2", "p", "this"],
11050 );
11051 }
11052
11053 /// Every C++ overloaded-operator name the grammar spells as a token
11054 /// of its own, plus the `co_await` expression and the conversion
11055 /// operator's `operator` keyword (#1296).
11056 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
11057 const CPP_OPERATOR_NAMES: &str =
11058 "struct S { int operator[](int); void operator()(); bool operator co_await(); };
11059long operator\"\"_x(unsigned long long);
11060S::operator bool() const;
11061S s; int a = s[1]; auto t = co_await s;
11062";
11063
11064 /// Asserts `CPP_OPERATOR_NAMES` bills each operator name, keyword and
11065 /// `co_await` as an operator under parser `T`.
11066 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
11067 #[track_caller]
11068 fn assert_cpp_operator_names<T: crate::MetricSuite>(label: &str) {
11069 // Operators (n1 = 17, N1 = 40): `operator` 5 (four
11070 // `operator_name`s and the `operator_cast`); the names `[]` 1,
11071 // `()` 1, `""` 1; `co_await` 2 (the name and the expression);
11072 // the `(` of a parameter list 5; `;` 9; `int` 3; `long` 3;
11073 // `bool` 2; `=` 2; `{` 1; `void` 1; `unsigned` 1; `::` 1; the
11074 // subscript `[` 1; `struct` 1 (#1552). `auto` and `const` are in
11075 // no arm.
11076 //
11077 // Operands (n2 = 6, N2 = 9): `s` 3, `S` 2 (the qualifier in
11078 // `S::operator bool` is a namespace identifier, which #1096
11079 // leaves unbilled), `_x`, `a`, `1`, `t`.
11080 //
11081 // Before #1296: (12, 30, 6, 9) with `struct`, the five operator
11082 // kinds billed nowhere.
11083 assert_halstead_counts::<T>(CPP_OPERATOR_NAMES, "ops.cpp", [17, 40, 6, 9], label);
11084
11085 // `operator[]` / `operator()` are kinds of their own, so they sit
11086 // beside the subscript `[` and the parameter-list `(` as two more
11087 // operators, each rendered as the operator it names so that every
11088 // listed entry is distinct.
11089 let ops = ops_of::<T>(CPP_OPERATOR_NAMES, "ops.cpp");
11090 for (name, entries) in [
11091 ("operator", 1),
11092 ("co_await", 1),
11093 ("\"\"", 1),
11094 ("operator[]", 1),
11095 ("operator()", 1),
11096 ("[]", 1),
11097 ("()", 1),
11098 ] {
11099 assert_eq!(
11100 ops.operators.iter().filter(|o| *o == name).count(),
11101 entries,
11102 "{label}: `{name}` must be {entries} operator entry; operators were {:?}",
11103 ops.operators
11104 );
11105 assert!(
11106 !ops.operands.iter().any(|o| o == name),
11107 "{label}: `{name}` must not be an operand; operands were {:?}",
11108 ops.operands
11109 );
11110 }
11111 }
11112
11113 /// Regression for #1296: a C++ operator name is a Halstead operator,
11114 /// as `operator +`'s `+` already was. One row per clone, so reverting
11115 /// either arm fails its own row (grammar-dispatch section 11); Mozcpp
11116 /// owns no file extension, so this row is its only coverage.
11117 #[cfg(all(feature = "cpp", feature = "mozcpp"))]
11118 #[test]
11119 fn cpp_operator_names_are_operators() {
11120 assert_cpp_operator_names::<CppParser>("cpp");
11121 assert_cpp_operator_names::<MozcppParser>("mozcpp");
11122 }
11123
11124 /// Asserts a coroutine's `co_yield` and `co_return` keywords bill as
11125 /// operators under parser `T`, as `return` does (#1547).
11126 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
11127 #[track_caller]
11128 fn assert_cpp_coroutine_keywords<T: crate::MetricSuite>(label: &str) {
11129 let source = "T gen() { co_yield 1; co_yield 2; co_return 3; }";
11130 // Operators (n1 = 5, N1 = 8): `()` 1, `{}` 1, `co_yield` 2,
11131 // `co_return` 1, `;` 3. Operands (n2 = 5, N2 = 5): `T`, `gen`,
11132 // `1`, `2`, `3`. Before #1547: (3, 5, 5, 5), both keywords
11133 // billed nowhere.
11134 assert_halstead_counts::<T>(source, "foo.cpp", [5, 8, 5, 5], label);
11135 let ops = ops_of::<T>(source, "foo.cpp");
11136 for keyword in ["co_yield", "co_return"] {
11137 assert!(
11138 ops.operators.iter().any(|o| o == keyword),
11139 "{label}: `{keyword}` must be an operator; operators were {:?}",
11140 ops.operators
11141 );
11142 }
11143 }
11144
11145 /// Regression for #1546: a zero-width node is billed in neither
11146 /// Halstead half. Error recovery inserts one for a token the source
11147 /// lacks, usually flagged MISSING, and every getter that classifies
11148 /// the kind billed it — an identifier as the empty-string operand.
11149 /// One fixture per language, each a parse on which recovery inserts
11150 /// an operand-classified zero-width node; before the fix every row
11151 /// billed `""`. C#'s and Elixir's are not flagged MISSING, which is
11152 /// why the guard keys on width: an `is_missing()` guard leaves those
11153 /// two rows billing `""`. Each row names its zero-width operand's
11154 /// kind and flag, so the two unflagged rows keep discriminating. The
11155 /// rows pin no counts, since the recovery trees are the grammars' to
11156 /// change (grammar-dispatch §6); the operator half, an inserted `;` or
11157 /// `)`, is anchored by `cpp_missing_tokens_in_valid_code_are_not_billed`.
11158 #[cfg(any(
11159 feature = "bash",
11160 feature = "c",
11161 feature = "cpp",
11162 feature = "csharp",
11163 feature = "elixir",
11164 feature = "go",
11165 feature = "groovy",
11166 feature = "irules",
11167 feature = "java",
11168 feature = "javascript",
11169 feature = "kotlin",
11170 feature = "lua",
11171 feature = "mozcpp",
11172 feature = "mozjs",
11173 feature = "objc",
11174 feature = "perl",
11175 feature = "php",
11176 feature = "python",
11177 feature = "ruby",
11178 feature = "rust",
11179 feature = "tcl",
11180 feature = "typescript",
11181 ))]
11182 #[test]
11183 fn zero_width_nodes_bill_neither_halstead_half() {
11184 // `(language, file, source, kind, is_missing)`: the last two name
11185 // the zero-width operand each fixture's recovery inserts, so a
11186 // grammar bump that recovers some other way fails the row rather
11187 // than passing it on an unrelated zero-width node.
11188 let cases: &[(LANG, &str, &str, &str, bool)] = &[
11189 #[cfg(feature = "bash")]
11190 (LANG::Bash, "foo.sh", "echo a $( )\n", "word", true),
11191 #[cfg(feature = "c")]
11192 (
11193 LANG::C,
11194 "foo.c",
11195 "int x; void () { x = 1; }\n",
11196 "identifier",
11197 true,
11198 ),
11199 #[cfg(feature = "cpp")]
11200 (
11201 LANG::Cpp,
11202 "foo.cpp",
11203 "void f(S *s) {\n s->~D<F>();\n}\n",
11204 "identifier",
11205 true,
11206 ),
11207 #[cfg(feature = "csharp")]
11208 (
11209 LANG::Csharp,
11210 "foo.cs",
11211 "class A { int = 1; }\n",
11212 "identifier",
11213 false,
11214 ),
11215 #[cfg(feature = "elixir")]
11216 (LANG::Elixir, "foo.ex", "x = a.\n", "identifier", false),
11217 #[cfg(feature = "go")]
11218 (
11219 LANG::Go,
11220 "foo.go",
11221 "package m\nfunc f() { a.() }\n",
11222 "type_identifier",
11223 true,
11224 ),
11225 #[cfg(feature = "groovy")]
11226 (
11227 LANG::Groovy,
11228 "foo.groovy",
11229 "def x = a.\n",
11230 "identifier",
11231 true,
11232 ),
11233 #[cfg(feature = "irules")]
11234 (
11235 LANG::Irules,
11236 "foo.irule",
11237 "when HTTP_REQUEST { set x [expr {1 + }] }\n",
11238 "number",
11239 true,
11240 ),
11241 #[cfg(feature = "java")]
11242 (
11243 LANG::Java,
11244 "Foo.java",
11245 "class A { int = 1; }\n",
11246 "identifier",
11247 true,
11248 ),
11249 #[cfg(feature = "javascript")]
11250 (
11251 LANG::Javascript,
11252 "foo.js",
11253 "let {a: } = b;\n",
11254 "identifier",
11255 true,
11256 ),
11257 #[cfg(feature = "kotlin")]
11258 (
11259 LANG::Kotlin,
11260 "foo.kt",
11261 "fun f(: Int) {}\n",
11262 "identifier",
11263 true,
11264 ),
11265 #[cfg(feature = "lua")]
11266 (LANG::Lua, "foo.lua", "x = a.\n", "identifier", true),
11267 #[cfg(feature = "mozcpp")]
11268 (
11269 LANG::Mozcpp,
11270 "foo.cpp",
11271 "void f(S *s) {\n s->~D<F>();\n}\n",
11272 "identifier",
11273 true,
11274 ),
11275 #[cfg(feature = "mozjs")]
11276 (
11277 LANG::Mozjs,
11278 "foo.jsm",
11279 "let {a: } = b;\n",
11280 "identifier",
11281 true,
11282 ),
11283 #[cfg(feature = "objc")]
11284 (
11285 LANG::Objc,
11286 "foo.m",
11287 "@interface A\n- (void)f:;\n@end\n",
11288 "identifier",
11289 true,
11290 ),
11291 #[cfg(feature = "perl")]
11292 (LANG::Perl, "foo.pl", "sub { $x = ; }\n", "identifier", true),
11293 #[cfg(feature = "php")]
11294 (LANG::Php, "foo.php", "<?php $x = $a->;\n", "name", true),
11295 #[cfg(feature = "python")]
11296 (
11297 LANG::Python,
11298 "foo.py",
11299 "for in x:\n pass\n",
11300 "identifier",
11301 true,
11302 ),
11303 #[cfg(feature = "ruby")]
11304 (LANG::Ruby, "foo.rb", "x = a.\n", "identifier", true),
11305 #[cfg(feature = "rust")]
11306 (
11307 LANG::Rust,
11308 "foo.rs",
11309 "fn f() { let x = a.; }\n",
11310 "field_identifier",
11311 true,
11312 ),
11313 #[cfg(feature = "tcl")]
11314 (LANG::Tcl, "foo.tcl", "puts [expr {1 + }]\n", "number", true),
11315 #[cfg(feature = "typescript")]
11316 (
11317 LANG::Typescript,
11318 "foo.ts",
11319 "let {a: } = b;\n",
11320 "identifier",
11321 true,
11322 ),
11323 #[cfg(feature = "typescript")]
11324 (LANG::Tsx, "foo.tsx", "let {a: } = b;\n", "identifier", true),
11325 ];
11326 crate::test_support::assert_fixtures_present(cases);
11327
11328 let mut billed = Vec::new();
11329 for (lang, file, source, kind, missing) in cases {
11330 let ast = crate::test_support::parse_named(*lang, file, source);
11331 assert!(
11332 ast.root_node().preorder().any(|node| {
11333 node.start_byte() == node.end_byte()
11334 && node.kind() == *kind
11335 && node.is_missing() == *missing
11336 }),
11337 "{lang:?}: `{source}` no longer recovers with a zero-width `{kind}` \
11338 (is_missing: {missing}); find another fixture"
11339 );
11340 let ops = ast.ops().expect("ops walk must yield a top-level Ops");
11341 assert!(
11342 !ops.operands.is_empty(),
11343 "{lang:?}: `{source}` billed no operand at all"
11344 );
11345 billed.extend(ops.operands.iter().any(String::is_empty).then_some(*lang));
11346 }
11347 assert!(
11348 billed.is_empty(),
11349 "a zero-width node was billed as the empty operand in {billed:?}"
11350 );
11351 }
11352
11353 /// Asserts two valid C++ programs that tree-sitter-cpp 0.23.4 can only
11354 /// parse by inserting MISSING tokens bill only the tokens written
11355 /// (#1546), under parser `T`. Both come from the DeepSpeech corpus.
11356 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
11357 #[track_caller]
11358 fn assert_cpp_missing_tokens_unbilled<T: crate::MetricSuite>(label: &str) {
11359 // `openfst/dfs-visit.h:75`: an explicit destructor call through a
11360 // template name. Recovery reads `s->~D<F` as comparisons and
11361 // inserts a MISSING identifier inside the trailing `()`.
11362 // Operators (n1 = 9, N1 = 10): `void`, `()` 2, `*`, `{}`, `->`,
11363 // `~`, `<`, `>`, `;`. Operands (n2 = 5, N2 = 6): `f`, `S`, `s` 2,
11364 // `D`, `F`. Before #1546: (9, 10, 6, 7), the extra being `""`.
11365 let destructor = "void f(S *s) {\n s->~D<F>();\n}\n";
11366 assert_halstead_counts::<T>(destructor, "foo.cpp", [9, 10, 5, 6], label);
11367 let ops = ops_of::<T>(destructor, "foo.cpp");
11368 assert_eq!(ops.operands, ["D", "F", "S", "f", "s"], "{label}");
11369
11370 // `kenlm/lm/search_trie.cc:286`: a `typename` functional cast
11371 // used as a statement. Recovery inserts a MISSING `)` after
11372 // `quant_` and a MISSING `;` after `order`. Operators (n1 = 9,
11373 // N1 = 11): `void`, `()` 3, `{}`, `::`, `,`, `-`, `.`, the one
11374 // written `;`, and `typename` (#1557). Operands (n2 = 7, N2 = 7):
11375 // `f`, `MiddlePointer`, `quant_`, `order`, `2`, `Write`, `w`.
11376 // Before #1546 the MISSING `;` was a second `;`.
11377 let cast = "void f() {\n typename Quant::MiddlePointer(quant_, order - 2).Write(w);\n}\n";
11378 assert_halstead_counts::<T>(cast, "foo.cpp", [9, 11, 7, 7], label);
11379 }
11380
11381 /// Regression for #1546 on the two C++ clones. Mozcpp owns no file
11382 /// extension, so this row is its only coverage.
11383 #[cfg(all(feature = "cpp", feature = "mozcpp"))]
11384 #[test]
11385 fn cpp_missing_tokens_in_valid_code_are_not_billed() {
11386 assert_cpp_missing_tokens_unbilled::<CppParser>("cpp");
11387 assert_cpp_missing_tokens_unbilled::<MozcppParser>("mozcpp");
11388 }
11389
11390 /// Regression for #1547: `co_return` and `co_yield` are Halstead
11391 /// operators. One row per clone, so reverting either arm fails its
11392 /// own row; Mozcpp owns no file extension, so this row is its only
11393 /// coverage.
11394 #[cfg(all(feature = "cpp", feature = "mozcpp"))]
11395 #[test]
11396 fn cpp_coroutine_keywords_are_operators() {
11397 assert_cpp_coroutine_keywords::<CppParser>("cpp");
11398 assert_cpp_coroutine_keywords::<MozcppParser>("mozcpp");
11399 }
11400
11401 /// Regression for #1361: a C++ `this` is a Halstead operand.
11402 ///
11403 /// `Cpp::This` / `Mozcpp::This` were in neither arm of
11404 /// `CppCode::get_op_type` / `MozcppCode::get_op_type`, so a `this`
11405 /// contributed *nothing* — not an operator and not an operand —
11406 /// while eleven of the fourteen languages here bill their
11407 /// self-reference as an operand. Same shape as #1316's character
11408 /// literals, and the inverse of the #1351-#1355 over-counts.
11409 ///
11410 /// Each language asserts separately over the same source rather than
11411 /// sharing one call, so reverting one clone's arm fails that row
11412 /// alone (grammar-dispatch section 11). Mozcpp owns no file
11413 /// extension, so no integration snapshot reaches its clone; this row
11414 /// and `cpp_and_mozcpp_agree_on_this` in `tests/parity/` are the
11415 /// whole coverage that arm has.
11416 #[cfg(all(feature = "cpp", feature = "mozcpp"))]
11417 #[test]
11418 fn cpp_this_is_an_operand() {
11419 assert_this_receiver_parity::<CppParser>("this.cpp", "cpp");
11420 assert_this_receiver_parity::<MozcppParser>("this.cpp", "mozcpp");
11421 }
11422
11423 /// The `This` arm is unconditional: every `this` in
11424 /// `CPP_THIS_POSITIONS` classifies as an operand whatever encloses
11425 /// it (#1361).
11426 ///
11427 /// The counts in `cpp_this_is_an_operand` are measured on one
11428 /// position only. An arm reached through a parent-scoped guard —
11429 /// the shape the neighbouring `RawStringLiteral` `LPAREN` arm uses,
11430 /// and the shape a reviewer might reasonably add here — would pass
11431 /// that test and still drop `this` in a lambda capture or a
11432 /// `decltype`. This walk is what makes the arm's unconditional
11433 /// reach a measurement rather than an assumption.
11434 ///
11435 /// Named for the *property* rather than for the fixture: the fixture
11436 /// samples one position per container kind and is deliberately not
11437 /// exhaustive (see `CPP_THIS_POSITIONS`), so a name promising "every
11438 /// position" would claim more than it checks.
11439 #[cfg(all(feature = "cpp", feature = "mozcpp"))]
11440 #[test]
11441 fn cpp_this_is_an_operand_regardless_of_position() {
11442 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
11443 fn check<L: LanguageInfo + Getter>(label: &str) {
11444 let mut seen = 0_usize;
11445 for_each_node_with_chain::<L>(CPP_THIS_POSITIONS.as_bytes(), |node, chain| {
11446 if node.kind() != "this" {
11447 return;
11448 }
11449 seen += 1;
11450 // `_with_code` is the spelling `compute_halstead` calls.
11451 // The default forwards to the byte-less form, so the two
11452 // agree for both languages today — which is exactly why
11453 // asking the wrong one would read as correct right up
11454 // until one of them grew an override
11455 // (grammar-dispatch section 7).
11456 assert!(
11457 matches!(
11458 L::get_op_type_with_code(
11459 node,
11460 CPP_THIS_POSITIONS.as_bytes(),
11461 Ancestors::known(chain)
11462 ),
11463 TokenRole::Operand
11464 ),
11465 "{label}: a `this` under `{}` is not an operand",
11466 chain.last().map_or("<root>", Node::kind)
11467 );
11468 });
11469 // One per position: `->` receiver, `(*this)`, bare return,
11470 // `[this]`, `[=, this]`, `decltype(this->x)`, the
11471 // constructor body, and the call argument.
11472 assert_eq!(seen, 8, "{label}: fixture lost a `this` position");
11473 }
11474
11475 check::<CppCode>("cpp");
11476 check::<MozcppCode>("mozcpp");
11477 }
11478
11479 /// Pins the two grammar facts the #1361 operand arm rests on, across
11480 /// both fixtures and both languages.
11481 ///
11482 /// * **Grammar-dispatch section 1.** Every node the grammar spells
11483 /// `this` carries the one `kind_id` the arm lists. That is the
11484 /// weaker half of the alias evidence — an alias arises in a
11485 /// *different* syntactic position, which no fixture can enumerate.
11486 /// The strong half is that these generated enums carry
11487 /// numeric-suffix aliases in quantity (`FunctionDefinition2..4`,
11488 /// `QualifiedIdentifier2..4`, `LPAREN2`, `Try2`, `GT2` in this one
11489 /// file) and neither spells a `This2` — `This = 215` is the sole
11490 /// variant rendering `"this"` in `language_cpp.rs` and
11491 /// `language_mozcpp.rs`. This loop is what notices if a grammar
11492 /// bump changes that under an existing fixture.
11493 /// * **Grammar-dispatch section 5.** A `this` is a childless leaf and
11494 /// no node containing it is classified. That is both halves of the
11495 /// double-count question: nothing below it can be billed a second
11496 /// time, and no `field_expression` / `pointer_expression` /
11497 /// `lambda_capture_specifier` / `argument_list` wrapper bills the
11498 /// same source text from above.
11499 #[cfg(all(feature = "cpp", feature = "mozcpp"))]
11500 #[test]
11501 fn cpp_this_is_a_childless_unaliased_leaf() {
11502 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
11503 fn check<L: LanguageInfo + Getter>(this: u16, label: &str) {
11504 let mut seen = 0_usize;
11505 for source in [CPP_THIS_RECEIVER_PARITY, CPP_THIS_POSITIONS] {
11506 for_each_node_with_chain::<L>(source.as_bytes(), |node, chain| {
11507 if node.kind() != "this" {
11508 return;
11509 }
11510 seen += 1;
11511 assert_eq!(
11512 node.kind_id(),
11513 this,
11514 "{label}: a `this` carries kind_id {} rather than the {this} the \
11515 operand arm lists — an alias the arm cannot see",
11516 node.kind_id()
11517 );
11518 assert_eq!(
11519 node.child_count(),
11520 0,
11521 "{label}: `this` grew children, which the operand arm would now \
11522 double-count against"
11523 );
11524 assert!(!chain.is_empty(), "{label}: a `this` cannot be the root");
11525 // Every ancestor, not just the parent. The claim is
11526 // that *no node containing* a `this` is classified,
11527 // and `(*this)` alone puts two wrappers between the
11528 // leaf and the nearest classified node — checking
11529 // only `chain.last()` would assert something
11530 // narrower than the comment on the arm promises.
11531 // `chain` is the *node's* ancestry, so the ancestor
11532 // at `i` has `chain[..i]` for its own.
11533 for (i, ancestor) in chain.iter().enumerate() {
11534 assert!(
11535 matches!(
11536 L::get_op_type_with_code(
11537 ancestor,
11538 source.as_bytes(),
11539 Ancestors::known(&chain[..i])
11540 ),
11541 TokenRole::Unknown
11542 ),
11543 "{label}: `{}` contains a `this` and is itself classified, so the \
11544 reference now counts twice",
11545 ancestor.kind()
11546 );
11547 }
11548 });
11549 }
11550 // One `this` in the parity fixture, eight in the positions
11551 // fixture. Asserted so a fixture that stopped containing one
11552 // cannot make this test pass having checked nothing.
11553 assert_eq!(seen, 9, "{label}: fixtures lost a `this`");
11554 }
11555
11556 check::<CppCode>(Cpp::This as u16, "cpp");
11557 check::<MozcppCode>(Mozcpp::This as u16, "mozcpp");
11558 }
11559
11560 /// Builds a `HalsteadMaps` from explicit occurrence counts.
11561 ///
11562 /// The per-language tests above reach these maps only through a
11563 /// parse, which cannot produce a *chosen* overlap between a child
11564 /// and its parent — the cases `merge` exists to get right.
11565 fn halstead_maps_of<'a>(
11566 operators: &[(u16, u64)],
11567 primitive_operators: &[(&'a [u8], u64)],
11568 operands: &[(&'a [u8], u64)],
11569 ) -> HalsteadMaps<'a> {
11570 HalsteadMaps {
11571 operators: operators.iter().copied().collect(),
11572 primitive_operators: primitive_operators.iter().copied().collect(),
11573 operands: operands.iter().copied().collect(),
11574 }
11575 }
11576
11577 /// `HalsteadMaps::operators` must stay on the crate's integer hasher.
11578 ///
11579 /// Swapping a hasher moves no metric value, so every other test in
11580 /// this file passes just as well with #1108 reverted. Both halves
11581 /// here are needed: the typed binding stops compiling if the field
11582 /// goes back to a default-hasher `HashMap`, and the `type_name`
11583 /// comparison still fails at runtime if `IntKeyHashMap` itself is
11584 /// ever redefined to wrap `RandomState`.
11585 ///
11586 /// The two text-keyed maps are pinned to SipHash in the same test,
11587 /// because moving *them* would be a regression rather than an
11588 /// optimisation. `crate::int_hash`'s module doc is the single place
11589 /// that argues why analysed source text does not qualify.
11590 #[test]
11591 fn halstead_operator_map_uses_the_int_key_hasher() {
11592 use std::any::{type_name, type_name_of_val};
11593 use std::hash::BuildHasherDefault;
11594
11595 use crate::int_hash::IntKeyHasher;
11596
11597 let maps = HalsteadMaps::new();
11598
11599 let operators: &IntKeyHashMap<u16, u64> = &maps.operators;
11600 assert_eq!(
11601 type_name_of_val(operators.hasher()),
11602 type_name::<BuildHasherDefault<IntKeyHasher>>(),
11603 "the kind_id-keyed operator map must use the int_hash hasher"
11604 );
11605
11606 let siphash = type_name::<std::collections::hash_map::RandomState>();
11607 assert_eq!(
11608 type_name_of_val(maps.operands.hasher()),
11609 siphash,
11610 "operand keys come from the analysed source, so the keyed hash \
11611 is what stops a crafted file from flooding this map"
11612 );
11613 assert_eq!(
11614 type_name_of_val(maps.primitive_operators.hasher()),
11615 siphash,
11616 "primitive-operator keys come from the analysed source, so the \
11617 keyed hash is what stops a crafted file from flooding this map"
11618 );
11619 }
11620
11621 /// `merge` sums overlapping keys and adopts disjoint ones, in all
11622 /// three maps, and `finalize` reads the union back as n1/N1/n2/N2.
11623 ///
11624 /// Every count differs from every other and none is zero, so a
11625 /// dropped key, an overwrite where an addition belongs, or a map
11626 /// crossed with its neighbour all change the totals.
11627 #[test]
11628 fn halstead_maps_merge_sums_overlaps_and_adopts_disjoint_keys() {
11629 let mut parent = halstead_maps_of(
11630 &[(1, 2), (2, 3)],
11631 &[(b"int", 1)],
11632 &[(b"alpha", 4), (b"beta", 7)],
11633 );
11634 let child = halstead_maps_of(
11635 &[(2, 5), (7, 11)],
11636 &[(b"double", 13)],
11637 &[(b"alpha", 17), (b"gamma", 19)],
11638 );
11639
11640 parent.merge(&child);
11641
11642 // expected: operators {1: 2, 2: 3+5, 7: 11}; primitives
11643 // {int: 1, double: 13}; operands {alpha: 4+17, beta: 7,
11644 // gamma: 19}.
11645 assert_eq!(
11646 parent,
11647 halstead_maps_of(
11648 &[(1, 2), (2, 8), (7, 11)],
11649 &[(b"int", 1), (b"double", 13)],
11650 &[(b"alpha", 21), (b"beta", 7), (b"gamma", 19)],
11651 )
11652 );
11653
11654 let mut stats = Stats::default();
11655 parent.finalize(&mut stats);
11656 // expected: n1 = 3 kind ids + 2 primitives; N1 = (2+8+11) +
11657 // (1+13); n2 = 3 texts; N2 = 21+7+19.
11658 assert_eq!(stats.unique_operators(), 5);
11659 assert_eq!(stats.total_operators(), 35);
11660 assert_eq!(stats.unique_operands(), 3);
11661 assert_eq!(stats.total_operands(), 47);
11662 }
11663
11664 /// Merging an empty child leaves the parent untouched.
11665 ///
11666 /// A space with no operators or operands is the common case for a
11667 /// leaf getter or an empty function body, and `finalize` runs on
11668 /// the parent afterwards either way.
11669 #[test]
11670 fn halstead_maps_merge_of_empty_child_is_a_no_op() {
11671 let mut parent = halstead_maps_of(&[(3, 5)], &[(b"char", 2)], &[(b"delta", 9)]);
11672 let before = parent.clone();
11673
11674 parent.merge(&HalsteadMaps::new());
11675
11676 assert_eq!(parent, before);
11677
11678 let mut stats = Stats::default();
11679 parent.finalize(&mut stats);
11680 // expected: n1 = 1 kind id + 1 primitive; N1 = 5 + 2; n2 = 1;
11681 // N2 = 9.
11682 assert_eq!(stats.unique_operators(), 2);
11683 assert_eq!(stats.total_operators(), 7);
11684 assert_eq!(stats.unique_operands(), 1);
11685 assert_eq!(stats.total_operands(), 9);
11686 }
11687
11688 /// Folding a chain of nested spaces bottom-up must reach the union
11689 /// of every level, re-merging already-merged maps on the way up.
11690 ///
11691 /// This is what `spaces.rs` and `ops.rs` actually do: each space is
11692 /// merged into its parent as the walk pops it, so by the time the
11693 /// root sees a grandchild's counts they have already passed through
11694 /// one `merge`. The literal expectation below is what discriminates
11695 /// — the `nested == flat` cross-check on its own does not, because
11696 /// any entry-wise fold over the same levels agrees with itself
11697 /// however it is associated, including a broken one.
11698 #[test]
11699 fn halstead_maps_merge_folds_a_nested_chain() {
11700 let levels = [
11701 halstead_maps_of(&[(1, 1)], &[(b"int", 1)], &[(b"a", 1)]),
11702 halstead_maps_of(&[(1, 2), (2, 3)], &[], &[(b"a", 2), (b"b", 4)]),
11703 halstead_maps_of(&[(2, 5)], &[(b"long", 6)], &[(b"b", 7)]),
11704 halstead_maps_of(&[(3, 8)], &[(b"int", 9)], &[(b"c", 10)]),
11705 ];
11706
11707 // Bottom-up: the deepest level folds into its parent, that
11708 // result into *its* parent, and so on up to the root.
11709 let mut nested = levels[levels.len() - 1].clone();
11710 for level in levels.iter().rev().skip(1) {
11711 let mut outer = level.clone();
11712 outer.merge(&nested);
11713 nested = outer;
11714 }
11715
11716 // Flat: every level merged directly into the root.
11717 let mut flat = levels[0].clone();
11718 for level in &levels[1..] {
11719 flat.merge(level);
11720 }
11721
11722 // expected: every key summed across the four levels — operators
11723 // {1: 1+2, 2: 3+5, 3: 8}, primitives {int: 1+9, long: 6},
11724 // operands {a: 1+2, b: 4+7, c: 10}.
11725 assert_eq!(
11726 nested,
11727 halstead_maps_of(
11728 &[(1, 3), (2, 8), (3, 8)],
11729 &[(b"int", 10), (b"long", 6)],
11730 &[(b"a", 3), (b"b", 11), (b"c", 10)],
11731 )
11732 );
11733 assert_eq!(nested, flat);
11734
11735 let mut stats = Stats::default();
11736 nested.finalize(&mut stats);
11737 // expected: n1 = 3 kind ids + 2 primitives; N1 = (3+8+8) +
11738 // (10+6); n2 = 3 texts; N2 = 3+11+10.
11739 assert_eq!(stats.unique_operators(), 5);
11740 assert_eq!(stats.total_operators(), 35);
11741 assert_eq!(stats.unique_operands(), 3);
11742 assert_eq!(stats.total_operands(), 24);
11743 }
11744
11745 /// A `kind_id` at the top of the `u16` range must behave like any
11746 /// other key.
11747 ///
11748 /// The largest grammar in the workspace (`mozcpp`) tops out around
11749 /// 640 symbols, so nothing near `u16::MAX` occurs today — but the
11750 /// map is keyed by the raw id, and a dense-array representation
11751 /// (the shape #1108 considered and rejected) is exactly what such a
11752 /// key would break. Pinning it keeps that trade-off honest if the
11753 /// representation is ever revisited.
11754 #[test]
11755 fn halstead_maps_handle_the_full_kind_id_range() {
11756 let mut parent = halstead_maps_of(&[(0, 3), (u16::MAX, 5)], &[], &[]);
11757 parent.merge(&halstead_maps_of(&[(u16::MAX, 7)], &[], &[]));
11758
11759 let mut stats = Stats::default();
11760 parent.finalize(&mut stats);
11761 // expected: two distinct kind ids, occurrences 3 and 5+7.
11762 assert_eq!(stats.unique_operators(), 2);
11763 assert_eq!(stats.total_operators(), 15);
11764 }
11765}