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.
79 ///
80 /// Text-keyed, so it keeps SipHash — see the module doc on
81 /// [`crate::int_hash`] for why analysed source text does not qualify
82 /// for the fast hasher.
83 pub(crate) primitive_operators: HashMap<&'a [u8], u64>,
84 /// Text-keyed, and on SipHash for the same reason as
85 /// `primitive_operators`.
86 pub(crate) operands: HashMap<&'a [u8], u64>,
87}
88
89impl<'a> HalsteadMaps<'a> {
90 pub(crate) fn new() -> Self {
91 Self::default()
92 }
93
94 pub(crate) fn merge(&mut self, other: &HalsteadMaps<'a>) {
95 for (k, v) in &other.operators {
96 *self.operators.entry(*k).or_insert(0) += v;
97 }
98 for (k, v) in &other.primitive_operators {
99 *self.primitive_operators.entry(*k).or_insert(0) += v;
100 }
101 for (k, v) in &other.operands {
102 *self.operands.entry(*k).or_insert(0) += v;
103 }
104 }
105
106 pub(crate) fn finalize(&self, stats: &mut Stats) {
107 stats.u_operators = (self.operators.len() + self.primitive_operators.len()) as u64;
108 stats.operators =
109 self.operators.values().sum::<u64>() + self.primitive_operators.values().sum::<u64>();
110 stats.u_operands = self.operands.len() as u64;
111 stats.operands = self.operands.values().sum::<u64>();
112 }
113}
114
115impl fmt::Display for Stats {
116 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
117 write!(
118 f,
119 "unique_operators: {}, \
120 total_operators: {}, \
121 unique_operands: {}, \
122 total_operands: {}, \
123 length: {}, \
124 estimated_program_length: {}, \
125 purity_ratio: {}, \
126 size: {}, \
127 volume: {}, \
128 difficulty: {}, \
129 level: {}, \
130 effort: {}, \
131 time: {}, \
132 bugs: {}",
133 self.unique_operators(),
134 self.total_operators(),
135 self.unique_operands(),
136 self.total_operands(),
137 self.length(),
138 self.estimated_program_length(),
139 self.purity_ratio(),
140 self.vocabulary(),
141 self.volume(),
142 self.difficulty(),
143 self.level(),
144 self.effort(),
145 self.time(),
146 self.bugs(),
147 )
148 }
149}
150
151impl Stats {
152 // Intentionally a no-op. Halstead distinct-counts (`u_operators` /
153 // `u_operands`) cannot be summed across sibling spaces without
154 // double-counting operators/operands they share. Cross-space
155 // aggregation is instead done by unioning the occurrence maps
156 // (`HalsteadMaps::merge`) and re-running `finalize` on the parent
157 // (see `spaces/compute.rs`). Summing the finalized fields here —
158 // mirroring the sibling metrics' `merge` — would silently inflate
159 // every parent space's n1/n2/N1/N2.
160 pub(crate) fn merge(&mut self, _other: &Stats) {}
161
162 /// Returns `η1`, the number of distinct operators
163 #[inline]
164 #[must_use]
165 pub fn unique_operators(&self) -> u64 {
166 self.u_operators
167 }
168
169 /// Returns `N1`, the number of total operators
170 #[inline]
171 #[must_use]
172 pub fn total_operators(&self) -> u64 {
173 self.operators
174 }
175
176 /// Returns `η2`, the number of distinct operands
177 #[inline]
178 #[must_use]
179 pub fn unique_operands(&self) -> u64 {
180 self.u_operands
181 }
182
183 /// Returns `N2`, the number of total operands
184 #[inline]
185 #[must_use]
186 pub fn total_operands(&self) -> u64 {
187 self.operands
188 }
189
190 /// Returns the program length
191 ///
192 /// Computed as `N = N1 + N2`, the sum of [`Self::total_operators`] and
193 /// [`Self::total_operands`].
194 #[inline]
195 #[must_use]
196 pub fn length(&self) -> u64 {
197 self.total_operands() + self.total_operators()
198 }
199
200 /// Returns the calculated estimated program length
201 ///
202 /// Computed as `N^ = n1 * log2(n1) + n2 * log2(n2)`, where `n1` is
203 /// [`Self::unique_operators`] and `n2` is [`Self::unique_operands`]. Each term is
204 /// treated as `0` when its unique count is `0`.
205 #[inline]
206 #[must_use]
207 pub fn estimated_program_length(&self) -> f64 {
208 let uo = self.unique_operators() as f64;
209 let ud = self.unique_operands() as f64;
210 let uo_term = if uo == 0.0 { 0.0 } else { uo * uo.log2() };
211 let ud_term = if ud == 0.0 { 0.0 } else { ud * ud.log2() };
212 uo_term + ud_term
213 }
214
215 /// Returns the purity ratio
216 ///
217 /// Computed as `PR = N^ / N`, the ratio of
218 /// [`Self::estimated_program_length`] to [`Self::length`].
219 #[inline]
220 #[must_use]
221 pub fn purity_ratio(&self) -> f64 {
222 let len = self.length() as f64;
223 if len == 0.0 {
224 0.0
225 } else {
226 self.estimated_program_length() / len
227 }
228 }
229
230 /// Returns the program vocabulary
231 ///
232 /// Computed as `n = n1 + n2`, the sum of [`Self::unique_operators`] and
233 /// [`Self::unique_operands`].
234 #[inline]
235 #[must_use]
236 pub fn vocabulary(&self) -> u64 {
237 self.unique_operands() + self.unique_operators()
238 }
239
240 /// Returns the program volume.
241 ///
242 /// Computed as `V = N * log2(n)`, where `N` is [`Self::length`] and `n`
243 /// is [`Self::vocabulary`]. Returns `0` when the vocabulary is `<= 1`,
244 /// since `log2` would be non-positive.
245 ///
246 /// Unit of measurement: bits
247 #[inline]
248 #[must_use]
249 pub fn volume(&self) -> f64 {
250 // Assumes a uniform binary encoding for the vocabulary is used.
251 let vocab = self.vocabulary() as f64;
252 if vocab <= 1.0 {
253 0.0
254 } else {
255 self.length() as f64 * vocab.log2()
256 }
257 }
258
259 /// Returns the estimated difficulty required to program
260 ///
261 /// Computed as `D = (n1 / 2) * (N2 / n2)`, where `n1` is
262 /// [`Self::unique_operators`], `N2` is [`Self::total_operands`], and `n2` is
263 /// [`Self::unique_operands`].
264 #[inline]
265 #[must_use]
266 pub fn difficulty(&self) -> f64 {
267 let ud = self.unique_operands() as f64;
268 if ud == 0.0 {
269 0.0
270 } else {
271 self.unique_operators() as f64 / 2. * self.total_operands() as f64 / ud
272 }
273 }
274
275 /// Returns the estimated level of difficulty required to program
276 ///
277 /// Computed as `L = 1 / D`, the reciprocal of [`Self::difficulty`].
278 #[inline]
279 #[must_use]
280 pub fn level(&self) -> f64 {
281 let d = self.difficulty();
282 if d == 0.0 { 0.0 } else { 1. / d }
283 }
284
285 /// Returns the estimated effort required to program
286 ///
287 /// Computed as `E = D * V`, the product of [`Self::difficulty`] and
288 /// [`Self::volume`].
289 #[inline]
290 #[must_use]
291 pub fn effort(&self) -> f64 {
292 self.difficulty() * self.volume()
293 }
294
295 /// Returns the estimated time required to program.
296 ///
297 /// Computed as `T = E / 18`, where `E` is [`Self::effort`] and `18` is
298 /// the Stroud number (see the divisor rationale below).
299 ///
300 /// Unit of measurement: seconds
301 #[inline]
302 #[must_use]
303 pub fn time(&self) -> f64 {
304 // The floating point `18.` aims to describe the processing rate of the
305 // human brain. It is called Stoud number, S, and its
306 // unit of measurement is moments/seconds.
307 // A moment is the time required by the human brain to carry out the
308 // most elementary decision.
309 // 5 <= S <= 20. Halstead uses 18.
310 // The value of S has been empirically developed from psychological
311 // reasoning, and its recommended value for
312 // programming applications is 18.
313 //
314 // Source: https://www.geeksforgeeks.org/software-engineering-halsteads-software-metrics/
315 self.effort() / 18.
316 }
317
318 /// Returns the estimated number of delivered bugs.
319 ///
320 /// This metric represents the average amount of work a programmer can do
321 /// without introducing an error.
322 ///
323 /// Computed as `B = E^(2/3) / 3000`, where `E` is [`Self::effort`]. This
324 /// is the effort-based variant of Halstead's delivered-bugs estimate
325 /// rather than the more commonly cited volume-based form `B = V / 3000`;
326 /// it matches the formula used by upstream `rust-code-analysis`.
327 #[inline]
328 #[must_use]
329 pub fn bugs(&self) -> f64 {
330 // The floating point `3000.` represents the number of elementary
331 // mental discriminations.
332 // A mental discrimination, in psychology, is the ability to perceive
333 // and respond to differences among stimuli.
334 //
335 // The value above is obtained starting from a constant that
336 // is different for every language and assumes that natural language is
337 // the language of the brain.
338 // For programming languages, the English language constant
339 // has been considered.
340 //
341 // After every 3000 mental discriminations a result is produced.
342 // This result, whether correct or incorrect, is more than likely
343 // either used as an input for the next operation or is output to the
344 // environment.
345 // If incorrect the error should become apparent.
346 // Thus, an opportunity for error occurs every 3000
347 // mental discriminations.
348 //
349 // Source: https://docs.lib.purdue.edu/cgi/viewcontent.cgi?article=1145&context=cstech
350 self.effort().powf(2. / 3.) / 3000.
351 }
352}
353
354#[doc(hidden)]
355/// Per-language extraction of Halstead operator/operand maps.
356pub(crate) trait Halstead
357where
358 Self: Checker + Getter,
359{
360 /// Walk `node` and update `stats` with this metric for the language
361 /// implementing the trait.
362 ///
363 /// `ancestors` is the chain the walker descended through; it is
364 /// handed to [`Getter::get_op_type`], six of whose impls classify a
365 /// token by what encloses it (#1096).
366 fn compute<'a>(
367 node: &Node<'a>,
368 code: &'a [u8],
369 ancestors: Ancestors<'a, '_>,
370 halstead_maps: &mut HalsteadMaps<'a>,
371 );
372}
373
374#[inline]
375fn get_id<'a>(node: &Node<'a>, code: &'a [u8]) -> &'a [u8] {
376 &code[node.start_byte()..node.end_byte()]
377}
378
379#[inline]
380fn compute_halstead<'a, T: Getter + Checker>(
381 node: &Node<'a>,
382 code: &'a [u8],
383 ancestors: Ancestors<'a, '_>,
384 halstead_maps: &mut HalsteadMaps<'a>,
385) {
386 match T::get_op_type_with_code(node, code, ancestors) {
387 TokenRole::Operator => {
388 if T::is_primitive(node) {
389 // Store primitive-type operators by text so distinct
390 // primitives (e.g. `int` vs `double`) that share a
391 // single kind_id are counted separately in n1/N1.
392 *halstead_maps
393 .primitive_operators
394 .entry(get_id(node, code))
395 .or_insert(0) += 1;
396 } else {
397 *halstead_maps.operators.entry(node.kind_id()).or_insert(0) += 1;
398 }
399 }
400 TokenRole::Operand => {
401 *halstead_maps
402 .operands
403 .entry(T::get_operand_id(node, code, ancestors))
404 .or_insert(0) += 1;
405 }
406 _ => {}
407 }
408}
409
410// Every language's `Halstead::compute` is the same forward to
411// `compute_halstead`, which classifies each node through the language's
412// own `Getter` / `Checker`. Nothing per-language lives here — it lives
413// in `big-code-analysis-ast/src/getter/<lang>.rs` — so writing the
414// impls out was 23 copies of one signature. (This is the only metric
415// whose per-language impls are all identical; every other trait has
416// real per-language bodies.)
417macro_rules! impl_halstead_forwarding {
418 ($($code:ty),+ $(,)?) => {
419 $(
420 impl Halstead for $code {
421 fn compute<'a>(
422 node: &Node<'a>,
423 code: &'a [u8],
424 ancestors: Ancestors<'a, '_>,
425 halstead_maps: &mut HalsteadMaps<'a>,
426 ) {
427 compute_halstead::<Self>(node, code, ancestors, halstead_maps);
428 }
429 }
430 )+
431 };
432}
433
434impl_halstead_forwarding!(
435 PythonCode,
436 MozjsCode,
437 JavascriptCode,
438 TypescriptCode,
439 TsxCode,
440 RustCode,
441 CppCode,
442 CCode,
443 ObjcCode,
444 MozcppCode,
445 JavaCode,
446 GroovyCode,
447 CsharpCode,
448 GoCode,
449 PerlCode,
450 KotlinCode,
451 LuaCode,
452 PhpCode,
453 RubyCode,
454 ElixirCode,
455 BashCode,
456 TclCode,
457 IrulesCode,
458);
459
460// Real defaults — no operators / operands to count. Audited in #188.
461implement_metric_trait!(Halstead, PreprocCode, CcommentCode);
462
463#[cfg(test)]
464#[allow(
465 clippy::float_cmp,
466 clippy::cast_precision_loss,
467 clippy::cast_possible_truncation,
468 clippy::cast_sign_loss,
469 clippy::similar_names,
470 clippy::doc_markdown,
471 clippy::needless_raw_string_hashes,
472 clippy::too_many_lines
473)]
474mod tests {
475 use std::collections::HashSet;
476 use std::path::PathBuf;
477
478 use crate::test_support::{ast_has_kind_id, check_metrics_only_shim, for_each_node_with_chain};
479
480 use super::*;
481
482 check_metrics_only_shim!(check_metrics, Halstead);
483
484 // `HalsteadType` is the pre-#1376 name for `TokenRole`, kept as a
485 // deprecated alias because removing it before `3.0` would be a
486 // SemVer break (STABILITY.md). Nothing in this repository uses the
487 // old spelling any more, so only this guard would notice it being
488 // dropped — and dropping it silently is exactly what the promise
489 // rules out.
490 //
491 // The `matches!` arms are the assertion: a value typed through the
492 // alias can only be matched against `TokenRole`'s variants if the
493 // alias still names that enum, so this fails to *compile* if the
494 // alias is removed or re-pointed, rather than failing at runtime.
495 // `TokenRole` derives nothing, so there is no `assert_eq!` to reach
496 // for here.
497 #[test]
498 #[allow(deprecated)]
499 fn halstead_type_alias_still_names_token_role() {
500 let via_alias: HalsteadType = HalsteadType::Operand;
501 assert!(matches!(via_alias, TokenRole::Operand));
502 assert!(!matches!(via_alias, TokenRole::Operator));
503 }
504
505 /// Runs the `--ops` walk over `source` and returns the root space's
506 /// merged vocabulary, with every nested space still reachable
507 /// through [`crate::ops::Ops::spaces`].
508 #[cfg(any(
509 feature = "c",
510 feature = "cpp",
511 feature = "csharp",
512 feature = "groovy",
513 feature = "irules",
514 feature = "java",
515 feature = "kotlin",
516 feature = "mozcpp",
517 feature = "objc",
518 feature = "perl",
519 feature = "ruby",
520 feature = "tcl",
521 ))]
522 fn ops_of<T: crate::MetricSuite>(source: &str, file: &str) -> crate::ops::Ops {
523 let path = PathBuf::from(file);
524 let parser = T::new(source.as_bytes().to_vec(), &path, None);
525 crate::ops::ops_inner(&parser, None).expect("ops walk succeeds")
526 }
527
528 // Pins the lesson-4 invariant `n2 == len(dedupe(ops.operands))` by
529 // running `operands_and_operators` (the text-keyed `--ops` store)
530 // on the same source and comparing its deduplicated operand count
531 // to the expected `n2`. The metrics store and the ops store are
532 // independent (lesson 4); this catches a classification change that
533 // moves one without the other.
534 // `#[track_caller]` so a failure reports the *caller's* line rather
535 // than this helper's. Callers that wrap it in a per-language helper
536 // (`assert_char_literal_operands`, #1316) are tracked too, so the
537 // reported location names the language row instead of a shared line
538 // no assertion message distinguishes.
539 #[cfg(any(
540 feature = "c",
541 feature = "cpp",
542 feature = "groovy",
543 feature = "irules",
544 feature = "kotlin",
545 feature = "mozcpp",
546 feature = "objc",
547 feature = "perl",
548 feature = "ruby",
549 feature = "tcl",
550 ))]
551 #[track_caller]
552 fn assert_ops_operands<T: crate::MetricSuite>(
553 source: &str,
554 file: &str,
555 expected_n2: usize,
556 mut expected_operands: Vec<&str>,
557 ) {
558 let ops = ops_of::<T>(source, file);
559
560 let unique: HashSet<&str> = ops.operands.iter().map(String::as_str).collect();
561 assert_eq!(
562 unique.len(),
563 expected_n2,
564 "dedupe(ops.operands) must equal n2; operands were {:?}",
565 ops.operands
566 );
567
568 let mut got: Vec<&str> = unique.into_iter().collect();
569 got.sort_unstable();
570 expected_operands.sort_unstable();
571 assert_eq!(got, expected_operands, "operand vocabulary for {file}");
572 }
573
574 /// Asserts each keyword in `keywords` reaches the operand vocabulary
575 /// and reaches the operator vocabulary nowhere.
576 ///
577 /// Both halves are load-bearing for #1380. A keyword listed in two
578 /// arms is billed twice — once into `n1`/`N1` and once into
579 /// `n2`/`N2` — and an assertion that only looked for its arrival
580 /// among the operands would pass on that. The operator side is what
581 /// pins the removal.
582 #[cfg(any(
583 feature = "csharp",
584 feature = "groovy",
585 feature = "java",
586 feature = "kotlin"
587 ))]
588 #[track_caller]
589 fn assert_keywords_are_operands_only<T: crate::MetricSuite>(
590 source: &str,
591 file: &str,
592 keywords: &[&str],
593 ) {
594 let ops = ops_of::<T>(source, file);
595
596 for keyword in keywords {
597 assert!(
598 ops.operands.iter().any(|o| o == keyword),
599 "{file}: `{keyword}` must be an operand; operands were {:?}",
600 ops.operands
601 );
602 assert!(
603 !ops.operators.iter().any(|o| o == keyword),
604 "{file}: `{keyword}` must not be an operator; operators were {:?}",
605 ops.operators
606 );
607 }
608 }
609
610 /// How one space's vocabularies classify a single keyword.
611 #[derive(Debug, PartialEq, Eq)]
612 enum Role {
613 OperatorOnly,
614 OperandOnly,
615 Both,
616 }
617
618 /// Records `keyword`'s role in `ops` and in every space below it.
619 ///
620 /// A space that bills the keyword in neither vocabulary contributes
621 /// no row, so the result is the set of places the keyword actually
622 /// reaches — which is what distinguishes a parent gate from its own
623 /// inverse, and what the merged root vocabulary cannot show.
624 #[cfg(feature = "csharp")]
625 fn collect_keyword_roles(ops: &crate::ops::Ops, keyword: &str, out: &mut Vec<Role>) {
626 let operator = ops.operators.iter().any(|o| o == keyword);
627 let operand = ops.operands.iter().any(|o| o == keyword);
628 match (operator, operand) {
629 (true, true) => out.push(Role::Both),
630 (true, false) => out.push(Role::OperatorOnly),
631 (false, true) => out.push(Role::OperandOnly),
632 (false, false) => {}
633 }
634 for space in &ops.spaces {
635 collect_keyword_roles(space, keyword, out);
636 }
637 }
638
639 /// Pins the three grammar facts every #1380 operand arm rests on,
640 /// over a fixture that samples one self- or super-reference per
641 /// syntactic position the language allows.
642 ///
643 /// Each `keywords` row is `(spelling, kind_id, occurrences)`.
644 ///
645 /// * **Grammar-dispatch section 1.** Every node the grammar spells
646 /// `this` / `super` / `base` / `this@` / `super@` carries the one
647 /// `kind_id` its arm lists. An alias arises in a *different*
648 /// syntactic position, which no fixture can enumerate, so this is
649 /// the weaker half of the alias evidence — the strong half is that
650 /// these generated enums carry numeric-suffix aliases in quantity
651 /// and none spells a `This2`. What this loop catches is a grammar
652 /// bump introducing one under an existing fixture.
653 /// * **Grammar-dispatch section 5.** Each is a childless leaf and no
654 /// node *containing* it is classified, so the reference cannot be
655 /// billed twice. Every ancestor is checked, not just the parent:
656 /// Kotlin puts `this_expression` *and* `navigation_expression`
657 /// between the leaf and the nearest classified node, and the claim
658 /// the arms make is about containment, not parentage.
659 /// * **Grammar-dispatch section 11.** The arm is unconditional. A
660 /// parent-scoped guard added later — the shape the two deliberate
661 /// carve-outs use, and so the shape a reviewer might reasonably
662 /// add — would satisfy the single-position count tests and still
663 /// drop `this` in a method reference or a constructor delegation.
664 ///
665 /// The fixtures deliberately exclude the two gated positions (C#'s
666 /// indexer declarator, Java's wildcard bound); those are operators,
667 /// and their own tests pin them.
668 #[cfg(any(feature = "csharp", feature = "java", feature = "kotlin"))]
669 #[track_caller]
670 fn assert_self_reference_leaves<L: LanguageInfo + Getter>(
671 source: &str,
672 keywords: &[(&str, u16, usize)],
673 label: &str,
674 ) {
675 let code = source.as_bytes();
676 let mut seen = vec![0_usize; keywords.len()];
677
678 for_each_node_with_chain::<L>(code, |node, chain| {
679 let Some(index) = keywords.iter().position(|(kw, _, _)| *kw == node.kind()) else {
680 return;
681 };
682 let (keyword, kind_id, _) = keywords[index];
683 seen[index] += 1;
684
685 assert_eq!(
686 node.kind_id(),
687 kind_id,
688 "{label}: a `{keyword}` carries kind_id {} rather than the {kind_id} its arm \
689 lists — an alias the arm cannot see",
690 node.kind_id()
691 );
692 assert_eq!(
693 node.child_count(),
694 0,
695 "{label}: `{keyword}` grew children, which the operand arm would now \
696 double-count against"
697 );
698 for (depth, ancestor) in chain.iter().enumerate() {
699 assert!(
700 matches!(
701 L::get_op_type_with_code(ancestor, code, Ancestors::known(&chain[..depth])),
702 TokenRole::Unknown
703 ),
704 "{label}: `{}` contains a `{keyword}` and is itself classified, so the \
705 reference now counts twice",
706 ancestor.kind()
707 );
708 }
709 // `_with_code` is the spelling `compute_halstead` calls. The
710 // default forwards to the byte-less form, so asking the wrong
711 // one reads as correct right up until a language grows an
712 // override (grammar-dispatch section 7).
713 assert!(
714 matches!(
715 L::get_op_type_with_code(node, code, Ancestors::known(chain)),
716 TokenRole::Operand
717 ),
718 "{label}: a `{keyword}` under `{}` is not an operand",
719 chain.last().map_or("<root>", Node::kind)
720 );
721 });
722
723 for ((keyword, _, expected), got) in keywords.iter().zip(&seen) {
724 assert_eq!(
725 got, expected,
726 "{label}: fixture holds {got} `{keyword}` rather than {expected}"
727 );
728 }
729 }
730
731 /// Asserts the root space's `[n1, N1, n2, N2]`, naming `label` when
732 /// it does not hold.
733 ///
734 /// The delimiter-invariance tests (#1256 Elixir, #1312 Ruby and
735 /// Perl) each loop over spellings of one literal and need the
736 /// spelling in the failure message; `check_metrics` expands to a
737 /// plain `fn` that cannot capture a loop variable, so they reach
738 /// for the closure-taking helper it wraps. Three copies of that
739 /// dance is two too many.
740 #[cfg(any(
741 feature = "bash",
742 feature = "c",
743 feature = "cpp",
744 feature = "csharp",
745 feature = "elixir",
746 feature = "groovy",
747 feature = "irules",
748 feature = "java",
749 feature = "javascript",
750 feature = "kotlin",
751 feature = "mozcpp",
752 feature = "mozjs",
753 feature = "objc",
754 feature = "perl",
755 feature = "php",
756 feature = "ruby",
757 feature = "tcl",
758 feature = "typescript",
759 ))]
760 fn assert_halstead_counts<T: crate::MetricSuite>(
761 source: &str,
762 file: &str,
763 expected: [u64; 4],
764 label: &str,
765 ) {
766 crate::test_support::check_func_space_only::<T, _>(
767 source,
768 file,
769 &[crate::Metric::Halstead],
770 |space| {
771 let halstead = &space.metrics.halstead;
772 assert_eq!(
773 [
774 halstead.unique_operators(),
775 halstead.total_operators(),
776 halstead.unique_operands(),
777 halstead.total_operands(),
778 ],
779 expected,
780 "{label}"
781 );
782 },
783 );
784 }
785
786 #[cfg(feature = "python")]
787 #[test]
788 fn python_operators_and_operands() {
789 check_metrics::<PythonParser>(
790 "def foo():
791 def bar():
792 def toto():
793 a = 1 + 1
794 b = 2 + a
795 c = 3 + 3",
796 "foo.py",
797 |metric| {
798 // unique operators: def, =, +
799 // operators: def, def, def, =, =, =, +, +, +
800 // unique operands: foo, bar, toto, a, b, c, 1, 2, 3
801 // operands: foo, bar, toto, a, b, c, 1, 1, 2, a, 3, 3
802 insta::assert_json_snapshot!(
803 metric.halstead,
804 @r#"
805 {
806 "unique_operators": 3,
807 "total_operators": 9,
808 "unique_operands": 9,
809 "total_operands": 12,
810 "length": 21,
811 "estimated_program_length": 33.284212515144276,
812 "purity_ratio": 1.584962500721156,
813 "vocabulary": 12,
814 "volume": 75.28421251514428,
815 "difficulty": 2.0,
816 "level": 0.5,
817 "effort": 150.56842503028855,
818 "time": 8.364912501682698,
819 "bugs": 0.0094341190071077
820 }
821 "#
822 );
823 },
824 );
825 }
826
827 /// Pointer-arithmetic operators: `*` (dereference), `&` (address-of),
828 /// `->` (member-of-pointer), `+` (pointer + offset). Each is counted
829 /// once in `n1`; multiple uses bump `N1`. The headline integer values
830 /// (`u_operators`, `u_operands`) anchor the snapshot per the
831 /// snapshot-anchor policy.
832 #[cfg(feature = "c")]
833 #[test]
834 fn c_pointer_arithmetic_operators() {
835 check_metrics::<CParser>(
836 "int g(int* p, int* q) {
837 return *(p + 1) + *q;
838 }",
839 "foo.c",
840 |metric| {
841 // Unique operators: int, *, (), {, }, +, ;, return (= 8)
842 // `*` covers both pointer-type and dereference; the grammar
843 // does NOT split them. `,` does not appear (only one
844 // parameter on each side of the body).
845 // Unique operands: g, p, q, 1 (= 4)
846 assert_eq!(metric.halstead.unique_operators(), 8);
847 assert_eq!(metric.halstead.unique_operands(), 4);
848 insta::assert_json_snapshot!(metric.halstead);
849 },
850 );
851 }
852
853 /// Bitwise (`&`, `|`, `^`, `~`, `<<`, `>>`) and logical (`&&`, `||`,
854 /// `!`) operators are distinct kind_ids and count as separate unique
855 /// operators in Halstead. `&` (bitwise-and) and `&&` (logical-and)
856 /// must NOT collapse, even though both render as ampersands.
857 #[cfg(feature = "c")]
858 #[test]
859 fn c_bitwise_and_logical_operators() {
860 check_metrics::<CParser>(
861 "int f(int a, int b) {
862 int x = (a & b) | (a ^ b);
863 int y = ~a;
864 int z = (a << 1) >> 2;
865 return (a && b) || !x;
866 }",
867 "foo.c",
868 |metric| {
869 // Expect: 6 bitwise op kinds (& | ^ ~ << >>), 3 logical (&& || !).
870 // Plus int, (), {, }, =, ;, return, , — 8 syntactic / arithmetic
871 // operator kinds. Six bitwise + three logical + eight = 17 unique
872 // operators is the upper bound; actuals depend on grammar collapse,
873 // so we assert a lower-bound and anchor via snapshot below.
874 let s = &metric.halstead;
875 assert!(
876 s.unique_operators() >= 14,
877 "expected >= 14 unique operators (bitwise + logical + syntax), got {}",
878 s.unique_operators(),
879 );
880 assert_eq!(s.unique_operands(), 8); // f, a, b, x, y, z, 1, 2
881 insta::assert_json_snapshot!(metric.halstead);
882 },
883 );
884 }
885
886 /// Increment / decrement (`++`, `--`) and `sizeof` / cast operators
887 /// each contribute distinct unique operators. C-style casts in the
888 /// tree-sitter grammar surface as `cast_expression` with the type
889 /// token classified as a primitive_type operator.
890 #[cfg(feature = "c")]
891 #[test]
892 fn c_increment_decrement_and_sizeof() {
893 check_metrics::<CParser>(
894 "void f(int* p) {
895 int n = sizeof(int);
896 ++p;
897 --n;
898 long w = (long) n;
899 }",
900 "foo.c",
901 |metric| {
902 // Unique operators include: void, int, long, *, =, sizeof, ++, --, (), {, }, ;
903 // Unique operands: f, p, n, w
904 let s = &metric.halstead;
905 assert!(
906 s.unique_operators() >= 10,
907 "expected >= 10 unique operators including ++ / -- / sizeof / cast, got {}",
908 s.unique_operators(),
909 );
910 assert_eq!(s.unique_operands(), 4);
911 insta::assert_json_snapshot!(metric.halstead);
912 },
913 );
914 }
915
916 #[cfg(feature = "cpp")]
917 #[test]
918 fn cpp_operators_and_operands() {
919 // Define operators and operands for C/C++ grammar according to this specification:
920 // https://www.verifysoft.com/en_halstead_metrics.html
921 // The only difference with the specification above is that
922 // primitive types are treated as operators, since the definition of a
923 // primitive type can be seen as the creation of a slot of a certain size.
924 // i.e. The `int a;` definition creates a n-bytes slot.
925 check_metrics::<CppParser>(
926 "main()
927 {
928 int a, b, c, avg;
929 scanf(\"%d %d %d\", &a, &b, &c);
930 avg = (a + b + c) / 3;
931 printf(\"avg = %d\", avg);
932 }",
933 "foo.c",
934 |metric| {
935 // unique operators: (), {}, int, &, =, +, /, ,, ;
936 // unique operands: main, a, b, c, avg, scanf, "%d %d %d", 3, printf, "avg = %d"
937 insta::assert_json_snapshot!(
938 metric.halstead,
939 @r#"
940 {
941 "unique_operators": 9,
942 "total_operators": 24,
943 "unique_operands": 10,
944 "total_operands": 18,
945 "length": 42,
946 "estimated_program_length": 61.74860596185444,
947 "purity_ratio": 1.470204903853677,
948 "vocabulary": 19,
949 "volume": 178.41295556463058,
950 "difficulty": 8.1,
951 "level": 0.1234567901234568,
952 "effort": 1445.1449400735075,
953 "time": 80.28583000408375,
954 "bugs": 0.04260752914034329
955 }
956 "#
957 );
958 },
959 );
960 }
961
962 /// A `sized_type_specifier` carries its `unsigned`/`signed`/`long`/
963 /// `short` modifiers as bare keyword tokens (distinct kind_ids), not
964 /// as `primitive_type` children. Prior to issue #466 those tokens
965 /// fell through to the `Unknown` arm and were dropped from `n1`/`N1`,
966 /// so `unsigned int` collapsed to just `int` and `signed long`
967 /// contributed nothing. They must each count as a distinct operator,
968 /// while `long long`'s two `long` tokens fold to one `n1` entry but
969 /// two `N1` hits. Regression test for issue #466.
970 #[cfg(feature = "cpp")]
971 #[test]
972 fn cpp_sized_type_specifier_operators() {
973 let source = "unsigned int u = 3; signed long b = 4; long long c = 5;";
974 check_metrics::<CppParser>(source, "foo.cpp", |metric| {
975 // Distinct operators (n1): unsigned, signed, long, int, =, ; = 6
976 // Total operators (N1):
977 // unsigned(1) + int(1) + =(3) + ;(3) + signed(1) + long(3) = 12
978 // (`long` appears once in `signed long` and twice in `long long`)
979 // Distinct/total operands: u, b, c, 3, 4, 5 = 6 / 6
980 assert_eq!(metric.halstead.unique_operators(), 6);
981 assert_eq!(metric.halstead.total_operators(), 12);
982 assert_eq!(metric.halstead.unique_operands(), 6);
983 assert_eq!(metric.halstead.total_operands(), 6);
984 });
985
986 // Pin the lesson-4 `n1 == dedupe(ops.operators)` invariant: the
987 // kind_id-keyed metrics store and the text-keyed `--ops` store are
988 // independent, so a modifier classified in one but not the other
989 // would diverge here.
990 let path = PathBuf::from("foo.cpp");
991 let parser = CppParser::new(source.as_bytes().to_vec(), &path, None);
992 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
993 let unique_operators: HashSet<&str> = ops.operators.iter().map(String::as_str).collect();
994 assert_eq!(
995 unique_operators.len(),
996 6,
997 "dedupe(ops.operators) must equal n1; operators were {:?}",
998 ops.operators
999 );
1000 for modifier in ["unsigned", "signed", "long"] {
1001 assert!(
1002 unique_operators.contains(modifier),
1003 "sized_type_specifier modifier {modifier:?} missing from ops.operators: {:?}",
1004 ops.operators
1005 );
1006 }
1007 }
1008
1009 /// C++20 spaceship operator `<=>` (`Cpp::LTEQGT`) is a comparison
1010 /// operator and must be counted in Halstead, like its sibling
1011 /// comparison operators `<`, `>`, `<=`, `>=`, `==`, `!=`. Prior to
1012 /// this fix it fell through to the `Unknown` arm and was silently
1013 /// dropped from `n1` / `N1`, under-reporting volume / effort on any
1014 /// C++20+ codebase that defines `operator<=>`. Regression test for
1015 /// issue #197.
1016 #[cfg(feature = "cpp")]
1017 #[test]
1018 fn cpp_spaceship_operator_is_halstead_operator() {
1019 check_metrics::<CppParser>(
1020 "int f(int a, int b) {
1021 return (a <=> b) != 0;
1022 }",
1023 "foo.cpp",
1024 |metric| {
1025 // Unique operators (grammar collapses matched delimiters
1026 // to a single kind_id): int, (), {}, <=>, !=, return, ;, ,
1027 // `<=>` is the regression target — without the fix it
1028 // would be Unknown and `u_operators` would be 7.
1029 // Unique operands: f, a, b, 0
1030 let s = &metric.halstead;
1031 assert_eq!(s.unique_operators(), 8);
1032 assert_eq!(s.unique_operands(), 4);
1033 insta::assert_json_snapshot!(
1034 s,
1035 @r#"
1036 {
1037 "unique_operators": 8,
1038 "total_operators": 11,
1039 "unique_operands": 4,
1040 "total_operands": 6,
1041 "length": 17,
1042 "estimated_program_length": 32.0,
1043 "purity_ratio": 1.8823529411764706,
1044 "vocabulary": 12,
1045 "volume": 60.94436251225965,
1046 "difficulty": 6.0,
1047 "level": 0.16666666666666666,
1048 "effort": 365.6661750735579,
1049 "time": 20.31478750408655,
1050 "bugs": 0.01704519358507665
1051 }
1052 "#
1053 );
1054 },
1055 );
1056 }
1057
1058 /// C++ compound subtract-assign `-=` (`Cpp::DASHEQ`) must be counted
1059 /// in Halstead like every other compound assignment (`+=`, `*=`,
1060 /// `/=`, etc.). Prior to the fix it fell through to the `Unknown`
1061 /// arm and was silently dropped from `n1` / `N1` — under-reporting
1062 /// volume / effort wherever C++ code subtracts in place. Regression
1063 /// test for issue #198.
1064 #[cfg(feature = "cpp")]
1065 #[test]
1066 fn cpp_dash_eq_is_halstead_operator() {
1067 check_metrics::<CppParser>("void f(int a, int b) { a -= b; }", "foo.cpp", |metric| {
1068 // Unique operators: void, (), {}, int, ,, -=, ;
1069 // `-=` is the regression target — without the fix it
1070 // would be Unknown and `u_operators` would be 6.
1071 // Unique operands: f, a, b
1072 let s = &metric.halstead;
1073 assert_eq!(s.unique_operators(), 7);
1074 assert_eq!(s.unique_operands(), 3);
1075 });
1076 }
1077
1078 /// C++ pointer-to-member access `.*` (`Cpp::DOTSTAR`) must be
1079 /// counted in Halstead. Prior to the fix it fell through to the
1080 /// `Unknown` arm and was silently dropped from `n1` / `N1`.
1081 /// Regression test for issue #198.
1082 ///
1083 /// The snippet uses an `operator.*` declaration because that is
1084 /// where the C++ tree-sitter grammar reliably emits a single
1085 /// `DOTSTAR` leaf; in expression position (`a.*b`) some grammar
1086 /// versions split the token into `DOT` + `STAR` and the regression
1087 /// would be masked.
1088 #[cfg(feature = "cpp")]
1089 #[test]
1090 fn cpp_dot_star_is_halstead_operator() {
1091 check_metrics::<CppParser>("struct S { void operator.*(int); };", "foo.cpp", |metric| {
1092 // Unique operators with fix: {}, ;, (), int, void, .*
1093 // `.*` is the regression target — without the fix it
1094 // falls through to `Unknown` and `u_operators` is 5.
1095 // Unique operands: S
1096 let s = &metric.halstead;
1097 assert_eq!(s.unique_operators(), 6);
1098 assert_eq!(s.unique_operands(), 1);
1099 });
1100 }
1101
1102 /// C++ pointer-to-member access through pointer `->*`
1103 /// (`Cpp::DASHGTSTAR`) must be counted in Halstead. Prior to the
1104 /// fix it fell through to the `Unknown` arm and was silently
1105 /// dropped from `n1` / `N1`. Regression test for issue #198.
1106 ///
1107 /// The snippet uses an `operator->*` declaration because that is
1108 /// where the C++ tree-sitter grammar reliably emits a single
1109 /// `DASHGTSTAR` leaf; in expression position (`a->*b`) the grammar
1110 /// splits the token into `DASHGT` + `STAR` and the regression would
1111 /// be masked.
1112 #[cfg(feature = "cpp")]
1113 #[test]
1114 fn cpp_dash_gt_star_is_halstead_operator() {
1115 check_metrics::<CppParser>(
1116 "struct S { void operator->*(int); };",
1117 "foo.cpp",
1118 |metric| {
1119 // Unique operators with fix: {}, ;, (), int, void, ->*
1120 // `->*` is the regression target — without the fix it
1121 // falls through to `Unknown` and `u_operators` is 5.
1122 // Unique operands: S
1123 let s = &metric.halstead;
1124 assert_eq!(s.unique_operators(), 6);
1125 assert_eq!(s.unique_operands(), 1);
1126 },
1127 );
1128 }
1129
1130 #[cfg(feature = "cpp")]
1131 #[test]
1132 fn cpp_raw_string_delimiter_is_not_an_operator() {
1133 // Regression: issue #1314, the C++ sibling of Elixir #1256 and
1134 // Ruby/Perl #1312. A `raw_string_literal` carries its `R"(`
1135 // opener as a bare `LPAREN` child — the kind id a call uses —
1136 // so `auto a = R"(raw)";` reported a `()` operator with no call
1137 // in the source.
1138 //
1139 // The fixture holds both sides at once: two raw strings and one
1140 // real call. A guard widened past the literal would drop
1141 // `f(a)`'s parenthesis and fail here rather than silently
1142 // passing.
1143 //
1144 // The second literal uses the custom-delimiter form to pin that
1145 // shape too — it adds a `raw_string_delimiter` child but keeps
1146 // the same `(` — and its distinct text makes n2 differ from N2.
1147 //
1148 // expected: operators `;` × 3, `=` × 3, `int`, `()` × 1 →
1149 // n1 = 4, N1 = 8. Operands the two literals, `a` × 2, `b`, `c`,
1150 // `f` → n2 = 6, N2 = 7. Before the guard the two openers added
1151 // two more `()` → N1 = 10.
1152 check_metrics::<CppParser>(
1153 "auto a = R\"(raw)\";\nauto b = R\"tag(raw)tag\";\nint c = f(a);\n",
1154 "foo.cpp",
1155 |metric| {
1156 assert_eq!(metric.halstead.unique_operators(), 4);
1157 assert_eq!(metric.halstead.total_operators(), 8);
1158 assert_eq!(metric.halstead.unique_operands(), 6);
1159 assert_eq!(metric.halstead.total_operands(), 7);
1160 },
1161 );
1162 }
1163
1164 #[cfg(feature = "rust")]
1165 #[test]
1166 fn rust_operators_and_operands() {
1167 check_metrics::<RustParser>(
1168 "fn main() {
1169 let a = 5; let b = 5; let c = 5;
1170 let avg = (a + b + c) / 3;
1171 println!(\"{}\", avg);
1172 }",
1173 "foo.rs",
1174 |metric| {
1175 // unique operators: fn, (), {}, let, =, +, /, ;, !, ,
1176 // unique operands: main, a, b, c, avg, 5, 3, println, "{}"
1177 insta::assert_json_snapshot!(
1178 metric.halstead,
1179 @r#"
1180 {
1181 "unique_operators": 10,
1182 "total_operators": 23,
1183 "unique_operands": 9,
1184 "total_operands": 15,
1185 "length": 38,
1186 "estimated_program_length": 61.74860596185444,
1187 "purity_ratio": 1.624963314785643,
1188 "vocabulary": 19,
1189 "volume": 161.42124551085624,
1190 "difficulty": 8.333333333333334,
1191 "level": 0.12,
1192 "effort": 1345.177045923802,
1193 "time": 74.7320581068779,
1194 "bugs": 0.040619232256751396
1195 }
1196 "#
1197 );
1198 },
1199 );
1200 }
1201
1202 #[cfg(feature = "rust")]
1203 #[test]
1204 fn rust_aliased_primitive_type_classification() {
1205 // Regression for issue #95 (lesson #2): the Rust grammar emits 17
1206 // distinct `kind_id`s for `primitive_type` (one base plus 16
1207 // numeric-suffixed alias variants). `RustCode::is_primitive` in
1208 // `big-code-analysis-ast/src/checker.rs` must list every variant;
1209 // if a future regression omits one, primitive type names emitted
1210 // in that aliased position silently drop into the kind_id-keyed
1211 // operators bucket instead of the text-keyed primitive_operators
1212 // map, miscounting Halstead n1.
1213 //
1214 // The snippet exercises every primitive scalar type across many
1215 // syntactic positions (function parameter types, return types,
1216 // let-binding annotations, `as` casts, const items, type aliases,
1217 // struct fields, function pointer types, tuple types, array types,
1218 // reference types, generic type arguments). Empirically, ordinary
1219 // Rust source emits the base `Rust::PrimitiveType` variant from
1220 // all of these positions; the 16 suffixed alias variants are
1221 // produced by specific grammar productions not reachable from
1222 // user-written code. Mutation-verified: dropping
1223 // `Rust::PrimitiveType` from `is_primitive` fails this test
1224 // (u_operators 30→15). Dropping any single suffixed variant
1225 // currently leaves the test passing; if a future grammar bump
1226 // makes any suffixed variant reachable from idiomatic source,
1227 // extend the snippet so the test fires for that variant too.
1228 check_metrics::<RustParser>(
1229 "const C: u8 = 0;
1230 type T = i64;
1231 struct S { x: u32, y: u64 }
1232 fn g(p: fn(u8) -> u16) -> bool { let _ = p(0); true }
1233 fn f(a: u8, b: u16, c: u32, d: u64) -> u128 {
1234 let _x: i8 = 0;
1235 let _y: i16 = 0;
1236 let _z: i32 = 0;
1237 let _w: i64 = 0;
1238 let _v: i128 = 0;
1239 let _p: f32 = 1.0;
1240 let _q: f64 = 2.0;
1241 let _r: bool = true;
1242 let _s: char = 'x';
1243 let _t: usize = 0;
1244 let _u: isize = 0;
1245 let _arr: [u32; 4] = [0; 4];
1246 let _ref: &u8 = &0;
1247 let _tup: (u32, u64) = (0, 0);
1248 let _opt: Option<u32> = None;
1249 a as u128 + b as u128 + c as u128 + d
1250 }",
1251 "foo.rs",
1252 |metric| {
1253 // Headline: u_operators is the load-bearing assertion —
1254 // the 16 distinct primitive type names dedupe by text in
1255 // the primitive_operators map. Total operators (N1) and
1256 // operand counts pin the rest of the Halstead state.
1257 // Grew from 30 → 33 with the issue #394 fix: `const`,
1258 // `type`, and `struct` keywords are now classified as
1259 // operators (one occurrence each).
1260 assert_eq!(metric.halstead.unique_operators(), 33);
1261 assert_eq!(metric.halstead.total_operators(), 121);
1262 // u_operands / operands grew (was 31/50 before #390): the
1263 // fix now classifies TypeIdentifier (`T`, `S`, `Option`)
1264 // and FieldIdentifier (struct fields `x`, `y`) as operands
1265 // alongside the existing primitive type names.
1266 assert_eq!(metric.halstead.unique_operands(), 36);
1267 assert_eq!(metric.halstead.total_operands(), 55);
1268 },
1269 );
1270 }
1271
1272 #[cfg(feature = "rust")]
1273 #[test]
1274 fn rust_field_identifier_is_operand() {
1275 // Regression for issue #390: prior to the fix, `FieldIdentifier`
1276 // (e.g. the `x` / `y` in `p.x`, `p.y`) fell through to
1277 // `TokenRole::Unknown`, so the field names were not counted
1278 // as operands. Both C++ and Go already classify FieldIdentifier
1279 // as an operand. After the fix:
1280 // unique operators: fn, (), {}, let, =, +, ;, .
1281 // unique operands : main, p, Point, x, y, sum, 0, 1
1282 // Field names `x` and `y` each appear twice (`p.x + p.y` and
1283 // the struct literal `Point { x: 0, y: 1 }`).
1284 check_metrics::<RustParser>(
1285 "fn main() {
1286 let p = Point { x: 0, y: 1 };
1287 let sum = p.x + p.y;
1288 }",
1289 "foo.rs",
1290 |metric| {
1291 // Headline: pre-fix, FieldIdentifier (`x`, `y`) and
1292 // TypeIdentifier (`Point`) fell through to Unknown, so
1293 // u_operands was 5 (main, p, sum, 0, 1). After the
1294 // fix, +Point, +x, +y → 8 distinct names.
1295 assert_eq!(metric.halstead.unique_operands(), 8);
1296 assert_eq!(metric.halstead.total_operands(), 12);
1297 insta::assert_json_snapshot!(
1298 metric.halstead,
1299 @r#"
1300 {
1301 "unique_operators": 9,
1302 "total_operators": 14,
1303 "unique_operands": 8,
1304 "total_operands": 12,
1305 "length": 26,
1306 "estimated_program_length": 52.529325012980806,
1307 "purity_ratio": 2.0203586543454155,
1308 "vocabulary": 17,
1309 "volume": 106.27403387250882,
1310 "difficulty": 6.75,
1311 "level": 0.14814814814814814,
1312 "effort": 717.3497286394346,
1313 "time": 39.85276270219081,
1314 "bugs": 0.026711567292222575
1315 }
1316 "#
1317 );
1318 },
1319 );
1320 }
1321
1322 #[cfg(feature = "rust")]
1323 #[test]
1324 fn rust_type_identifier_is_operand() {
1325 // Regression for issue #390: `TypeIdentifier` (e.g. `Vec`,
1326 // `HashMap`, `String` when used as a path name) was dropped to
1327 // `TokenRole::Unknown` for Rust. C++ and Go classify them as
1328 // operands. After the fix, u_operands = 8:
1329 // main, v, m, Vec, HashMap, new, K, V
1330 // (`i32` is a primitive type, classified as an operator.)
1331 //
1332 // Also covers issue #394: `::` is now an operator. The snippet
1333 // has two `::` tokens (`Vec::new`, `HashMap::new`), so n1 grew
1334 // from 10 → 11 and N1 from 17 → 19.
1335 check_metrics::<RustParser>(
1336 "fn main() {
1337 let v: Vec<i32> = Vec::new();
1338 let m: HashMap<K, V> = HashMap::new();
1339 }",
1340 "foo.rs",
1341 |metric| {
1342 // Headline: u_operands includes `Vec`, `HashMap`, `K`,
1343 // `V` (and `i32` as a primitive operator). Without the
1344 // fix, Vec/HashMap/K/V silently dropped to Unknown.
1345 assert_eq!(metric.halstead.unique_operands(), 8);
1346 assert_eq!(metric.halstead.total_operands(), 11);
1347 // `::` appears twice (Vec::new, HashMap::new); without
1348 // the #394 fix u_operators was 10 and operators 17.
1349 assert_eq!(metric.halstead.unique_operators(), 11);
1350 assert_eq!(metric.halstead.total_operators(), 19);
1351 insta::assert_json_snapshot!(
1352 metric.halstead,
1353 @r#"
1354 {
1355 "unique_operators": 11,
1356 "total_operators": 19,
1357 "unique_operands": 8,
1358 "total_operands": 11,
1359 "length": 30,
1360 "estimated_program_length": 62.05374780501027,
1361 "purity_ratio": 2.068458260167009,
1362 "vocabulary": 19,
1363 "volume": 127.43782540330756,
1364 "difficulty": 7.5625,
1365 "level": 0.1322314049586777,
1366 "effort": 963.7485546125134,
1367 "time": 53.54158636736186,
1368 "bugs": 0.03252279825177962
1369 }
1370 "#
1371 );
1372 },
1373 );
1374 }
1375
1376 #[cfg(feature = "rust")]
1377 #[test]
1378 fn rust_path_separator_is_operator() {
1379 // Regression for issue #394: `::` (`COLONCOLON`) was missing
1380 // from the Rust `get_op_type` operator arm even though C++,
1381 // Java, C#, and Kotlin all classify it as an operator. Path-
1382 // heavy code (`std::collections::HashMap`, `Vec::new`,
1383 // `T::method`) had every `::` silently dropped into
1384 // TokenRole::Unknown.
1385 //
1386 // Snippet has three `::` tokens (`std::collections::HashMap`,
1387 // counted as two `::` separators, plus `HashMap::new`).
1388 check_metrics::<RustParser>(
1389 "fn main() {
1390 let m = std::collections::HashMap::new();
1391 }",
1392 "foo.rs",
1393 |metric| {
1394 // `::` appears 3 times across the two path expressions
1395 // (`std::collections::HashMap` contributes two; the
1396 // `HashMap::new` contributes one). Pre-fix all three
1397 // dropped to Unknown: u_operators would be 6 (no `::`
1398 // distinct) and total_operators() would be 7 (minus 3 `::`
1399 // occurrences). With the fix u_operators=7 and
1400 // operators=10.
1401 //
1402 // unique operators (post-fix): fn, LPAREN, LBRACE,
1403 // let, =, ::, ;. unique operands: main, m, std,
1404 // collections, HashMap, new.
1405 assert_eq!(metric.halstead.unique_operators(), 7);
1406 assert_eq!(metric.halstead.total_operators(), 10);
1407 assert_eq!(metric.halstead.unique_operands(), 6);
1408 assert_eq!(metric.halstead.total_operands(), 6);
1409 },
1410 );
1411 }
1412
1413 #[cfg(feature = "rust")]
1414 #[test]
1415 fn rust_declaration_keywords_are_operators() {
1416 // Regression for issue #394: the Rust impl already accepted 17
1417 // keywords as operators (As, Async, Await, …, Fn) but omitted
1418 // 14 declaration / visibility keywords. The fix adds `Const`,
1419 // `Static`, `Enum`, `Struct`, `Trait`, `Impl`, `Use`, `Mod`,
1420 // `Pub`, `Type`, `Union`, `Where`, `Extern`, `Dyn`.
1421 //
1422 // Snippet exercises `use`, `pub`, `struct`, and `impl` (one of
1423 // each); together they account for 4 new operator occurrences
1424 // and 4 new unique operators.
1425 check_metrics::<RustParser>(
1426 "use std::fmt;
1427 pub struct S;
1428 impl S { fn n() -> u8 { 0 } }",
1429 "foo.rs",
1430 |metric| {
1431 // expected: unique operators (11) = use, ::, ;, pub,
1432 // struct, impl, LBRACE, fn, LPAREN, DASHGT, u8. Without
1433 // the #394 fix, `use`, `pub`, `struct`, and `impl`
1434 // would each drop to Unknown and u_operators would be
1435 // 7. unique operands (5): std, fmt, S, n, 0.
1436 assert_eq!(metric.halstead.unique_operators(), 11);
1437 assert_eq!(metric.halstead.total_operators(), 13);
1438 assert_eq!(metric.halstead.unique_operands(), 5);
1439 assert_eq!(metric.halstead.total_operands(), 6);
1440 },
1441 );
1442 }
1443
1444 #[cfg(feature = "javascript")]
1445 #[test]
1446 fn javascript_operators_and_operands() {
1447 check_metrics::<JavascriptParser>(
1448 "function main() {
1449 var a, b, c, avg;
1450 a = 5; b = 5; c = 5;
1451 avg = (a + b + c) / 3;
1452 console.log(\"{}\", avg);
1453 }",
1454 "foo.js",
1455 |metric| {
1456 // unique operators: function, (), {}, var, =, +, /, ,, ., ;
1457 // unique operands: main, a, b, c, avg, 3, 5, console, log, "{}"
1458 // `console.log` is the `.` operator applied to the two
1459 // identifier leaves; the composite `member_expression`
1460 // text is deliberately not a third operand (#1263), so
1461 // n2/N2 are 10/20 rather than the pre-#1263 11/21.
1462 insta::assert_json_snapshot!(
1463 metric.halstead,
1464 @r#"
1465 {
1466 "unique_operators": 10,
1467 "total_operators": 24,
1468 "unique_operands": 10,
1469 "total_operands": 20,
1470 "length": 44,
1471 "estimated_program_length": 66.43856189774725,
1472 "purity_ratio": 1.5099673158578921,
1473 "vocabulary": 20,
1474 "volume": 190.16483617504394,
1475 "difficulty": 10.0,
1476 "level": 0.1,
1477 "effort": 1901.6483617504396,
1478 "time": 105.64713120835775,
1479 "bugs": 0.05116412536051621
1480 }
1481 "#
1482 );
1483 },
1484 );
1485 }
1486
1487 #[cfg(feature = "mozjs")]
1488 #[test]
1489 fn mozjs_operators_and_operands() {
1490 check_metrics::<MozjsParser>(
1491 "function main() {
1492 var a, b, c, avg;
1493 a = 5; b = 5; c = 5;
1494 avg = (a + b + c) / 3;
1495 console.log(\"{}\", avg);
1496 }",
1497 "foo.js",
1498 |metric| {
1499 // unique operators: function, (), {}, var, =, +, /, ,, ., ;
1500 // unique operands: main, a, b, c, avg, 3, 5, console, log, "{}"
1501 // `console.log` is the `.` operator applied to the two
1502 // identifier leaves; the composite `member_expression`
1503 // text is deliberately not a third operand (#1263), so
1504 // n2/N2 are 10/20 rather than the pre-#1263 11/21.
1505 insta::assert_json_snapshot!(
1506 metric.halstead,
1507 @r#"
1508 {
1509 "unique_operators": 10,
1510 "total_operators": 24,
1511 "unique_operands": 10,
1512 "total_operands": 20,
1513 "length": 44,
1514 "estimated_program_length": 66.43856189774725,
1515 "purity_ratio": 1.5099673158578921,
1516 "vocabulary": 20,
1517 "volume": 190.16483617504394,
1518 "difficulty": 10.0,
1519 "level": 0.1,
1520 "effort": 1901.6483617504396,
1521 "time": 105.64713120835775,
1522 "bugs": 0.05116412536051621
1523 }
1524 "#
1525 );
1526 },
1527 );
1528 }
1529
1530 #[cfg(feature = "typescript")]
1531 #[test]
1532 fn typescript_operators_and_operands() {
1533 check_metrics::<TypescriptParser>(
1534 "function main() {
1535 var a, b, c, avg;
1536 a = 5; b = 5; c = 5;
1537 avg = (a + b + c) / 3;
1538 console.log(\"{}\", avg);
1539 }",
1540 "foo.ts",
1541 |metric| {
1542 // unique operators: function, (), {}, var, =, +, /, ,, ., ;
1543 // unique operands: main, a, b, c, avg, 3, 5, console, log, "{}"
1544 // `console.log` is the `.` operator applied to the two
1545 // identifier leaves; the composite `member_expression`
1546 // text is deliberately not a third operand (#1263), so
1547 // n2/N2 are 10/20 rather than the pre-#1263 11/21.
1548 insta::assert_json_snapshot!(
1549 metric.halstead,
1550 @r#"
1551 {
1552 "unique_operators": 10,
1553 "total_operators": 24,
1554 "unique_operands": 10,
1555 "total_operands": 20,
1556 "length": 44,
1557 "estimated_program_length": 66.43856189774725,
1558 "purity_ratio": 1.5099673158578921,
1559 "vocabulary": 20,
1560 "volume": 190.16483617504394,
1561 "difficulty": 10.0,
1562 "level": 0.1,
1563 "effort": 1901.6483617504396,
1564 "time": 105.64713120835775,
1565 "bugs": 0.05116412536051621
1566 }
1567 "#
1568 );
1569 },
1570 );
1571 }
1572
1573 #[cfg(feature = "typescript")]
1574 #[test]
1575 fn tsx_operators_and_operands() {
1576 check_metrics::<TsxParser>(
1577 "function main() {
1578 var a, b, c, avg;
1579 a = 5; b = 5; c = 5;
1580 avg = (a + b + c) / 3;
1581 console.log(\"{}\", avg);
1582 }",
1583 "foo.ts",
1584 |metric| {
1585 // unique operators: function, (), {}, var, =, +, /, ,, ., ;
1586 // unique operands: main, a, b, c, avg, 3, 5, console, log, "{}"
1587 // `console.log` is the `.` operator applied to the two
1588 // identifier leaves; the composite `member_expression`
1589 // text is deliberately not a third operand (#1263), so
1590 // n2/N2 are 10/20 rather than the pre-#1263 11/21.
1591 insta::assert_json_snapshot!(
1592 metric.halstead,
1593 @r#"
1594 {
1595 "unique_operators": 10,
1596 "total_operators": 24,
1597 "unique_operands": 10,
1598 "total_operands": 20,
1599 "length": 44,
1600 "estimated_program_length": 66.43856189774725,
1601 "purity_ratio": 1.5099673158578921,
1602 "vocabulary": 20,
1603 "volume": 190.16483617504394,
1604 "difficulty": 10.0,
1605 "level": 0.1,
1606 "effort": 1901.6483617504396,
1607 "time": 105.64713120835775,
1608 "bugs": 0.05116412536051621
1609 }
1610 "#
1611 );
1612 },
1613 );
1614 }
1615
1616 #[cfg(feature = "javascript")]
1617 #[test]
1618 fn javascript_template_string_plain_is_operand() {
1619 // Regression: issue #192. A backtick-delimited `` `hello` ``
1620 // without `${...}` is semantically identical to `"hello"` /
1621 // `'hello'` and must contribute exactly one operand — before
1622 // the fix `TemplateString` fell through to `TokenRole::Unknown`
1623 // and contributed zero. expected: operands are `f` (function
1624 // name) and the wrapping `` `hello` `` template literal →
1625 // u_operands = 2, N2 = 2 (matches the equivalent
1626 // `function f() { return "hello"; }` baseline).
1627 check_metrics::<JavascriptParser>("function f() { return `hello`; }", "foo.js", |metric| {
1628 assert_eq!(metric.halstead.unique_operands(), 2);
1629 assert_eq!(metric.halstead.total_operands(), 2);
1630 });
1631 }
1632
1633 /// Regression for #695. The `get` / `set` property-accessor keywords
1634 /// are operators, matching the C# getter's `Get | Set | Init | Add |
1635 /// Remove` accessor arm. Before #695 the JS family classified them as
1636 /// operands, so the same accessor keyword landed in opposite Halstead
1637 /// groups across languages. This pins them in the operator store and
1638 /// out of the operand store.
1639 #[cfg(feature = "javascript")]
1640 #[test]
1641 fn js_get_set_accessors_are_operators() {
1642 let source = "class C { get x() { return 1; } set x(v) { this._x = v; } }";
1643 let path = PathBuf::from("foo.js");
1644 let parser = JavascriptParser::new(source.as_bytes().to_vec(), &path, None);
1645 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
1646 assert!(
1647 ops.operators.iter().any(|o| o.as_str() == "get")
1648 && ops.operators.iter().any(|o| o.as_str() == "set"),
1649 "`get`/`set` accessors must be operators; operators were {:?}",
1650 ops.operators
1651 );
1652 assert!(
1653 !ops.operands.iter().any(|o| o.as_str() == "get")
1654 && !ops.operands.iter().any(|o| o.as_str() == "set"),
1655 "`get`/`set` accessors must not be operands; operands were {:?}",
1656 ops.operands
1657 );
1658 }
1659
1660 #[cfg(feature = "javascript")]
1661 #[test]
1662 fn javascript_template_string_interpolation_no_double_count() {
1663 // Regression: issue #192. An interpolated template literal
1664 // `` `Hi ${name}!` `` used to fall through to `Unknown`,
1665 // dropping the wrapper from the count entirely; the inner
1666 // `name` was still walked and counted via the
1667 // `TemplateSubstitution` child. Mirrors #183 (C#), #191
1668 // (Kotlin), #199 (Perl): the wrapper is skipped when a
1669 // `TemplateSubstitution` child is present so the inner
1670 // expression is not double-counted.
1671 //
1672 // expected: for `function f(name) { return ` + "`Hi ${name}!`"
1673 // + `; }`, operands are `f` and `name` (twice — `name` as the
1674 // parameter, then again inside the interpolation), so
1675 // u_operands = 2 and N2 = 3. Without the wrapper-skip guard
1676 // the wrapping literal would also be counted, lifting
1677 // u_operands to 3 and N2 to 4.
1678 check_metrics::<JavascriptParser>(
1679 "function f(name) { return `Hi ${name}!`; }",
1680 "foo.js",
1681 |metric| {
1682 assert_eq!(metric.halstead.unique_operands(), 2);
1683 assert_eq!(metric.halstead.total_operands(), 3);
1684 },
1685 );
1686 }
1687
1688 #[cfg(feature = "mozjs")]
1689 #[test]
1690 fn mozjs_template_string_plain_is_operand() {
1691 // Regression: issue #192. Mirrors
1692 // `javascript_template_string_plain_is_operand` for the
1693 // Firefox-mode dialect — the four JS-family `get_op_type`
1694 // impls share the same template-literal handling.
1695 check_metrics::<MozjsParser>("function f() { return `hello`; }", "foo.js", |metric| {
1696 assert_eq!(metric.halstead.unique_operands(), 2);
1697 assert_eq!(metric.halstead.total_operands(), 2);
1698 });
1699 }
1700
1701 #[cfg(feature = "mozjs")]
1702 #[test]
1703 fn mozjs_template_string_interpolation_no_double_count() {
1704 // Regression: issue #192. Mirrors
1705 // `javascript_template_string_interpolation_no_double_count`
1706 // for the Firefox-mode dialect.
1707 check_metrics::<MozjsParser>(
1708 "function f(name) { return `Hi ${name}!`; }",
1709 "foo.js",
1710 |metric| {
1711 assert_eq!(metric.halstead.unique_operands(), 2);
1712 assert_eq!(metric.halstead.total_operands(), 3);
1713 },
1714 );
1715 }
1716
1717 #[cfg(feature = "typescript")]
1718 #[test]
1719 fn typescript_template_string_plain_is_operand() {
1720 // Regression: issue #192. Mirrors
1721 // `javascript_template_string_plain_is_operand` for
1722 // TypeScript — the four JS-family `get_op_type` impls share
1723 // the same template-literal handling.
1724 //
1725 // The `: string` annotation contributes no operand — its
1726 // keyword counts once, as the text-keyed operator (#1261) — so
1727 // the operands are `f` and `` `hello` `` (2 each). The headline
1728 // of this test — that the plain template literal contributes
1729 // one operand — is unaffected.
1730 check_metrics::<TypescriptParser>(
1731 "function f(): string { return `hello`; }",
1732 "foo.ts",
1733 |metric| {
1734 assert_eq!(metric.halstead.unique_operands(), 2);
1735 assert_eq!(metric.halstead.total_operands(), 2);
1736 },
1737 );
1738 }
1739
1740 #[cfg(feature = "typescript")]
1741 #[test]
1742 fn typescript_template_string_interpolation_no_double_count() {
1743 // Regression: issue #192. Mirrors
1744 // `javascript_template_string_interpolation_no_double_count`
1745 // for TypeScript.
1746 //
1747 // The `: string` annotations contribute no operands (#1261).
1748 // Unique operands: `f`, `name` (2). Total operands: `f`, `name`
1749 // (param), `name` (in the interpolation) (3). The interpolation
1750 // guard from #192 still holds — the wrapping `` `Hi ${name}!` ``
1751 // is `Unknown`, not double-counted.
1752 check_metrics::<TypescriptParser>(
1753 "function f(name: string): string { return `Hi ${name}!`; }",
1754 "foo.ts",
1755 |metric| {
1756 assert_eq!(metric.halstead.unique_operands(), 2);
1757 assert_eq!(metric.halstead.total_operands(), 3);
1758 },
1759 );
1760 }
1761
1762 #[cfg(feature = "typescript")]
1763 #[test]
1764 fn tsx_template_string_plain_is_operand() {
1765 // Regression: issue #192. Mirrors
1766 // `javascript_template_string_plain_is_operand` for the
1767 // TSX (TypeScript + JSX) variant.
1768 //
1769 // TSX's type-keyword `string` (`String3`) contributes no
1770 // operand, mirroring TS::String2 (#1261): operands are `f` and
1771 // `` `hello` `` (2 each).
1772 check_metrics::<TsxParser>(
1773 "function f(): string { return `hello`; }",
1774 "foo.tsx",
1775 |metric| {
1776 assert_eq!(metric.halstead.unique_operands(), 2);
1777 assert_eq!(metric.halstead.total_operands(), 2);
1778 },
1779 );
1780 }
1781
1782 #[cfg(feature = "typescript")]
1783 #[test]
1784 fn tsx_template_string_interpolation_no_double_count() {
1785 // Regression: issue #192. Mirrors
1786 // `javascript_template_string_interpolation_no_double_count`
1787 // for the TSX (TypeScript + JSX) variant.
1788 //
1789 // The `: string` annotations contribute no `String3` operands
1790 // (#1261); see `typescript_template_string_…` for the count
1791 // derivation.
1792 check_metrics::<TsxParser>(
1793 "function f(name: string): string { return `Hi ${name}!`; }",
1794 "foo.tsx",
1795 |metric| {
1796 assert_eq!(metric.halstead.unique_operands(), 2);
1797 assert_eq!(metric.halstead.total_operands(), 3);
1798 },
1799 );
1800 }
1801
1802 /// The JS-family regex fixture, asserted against all four grammars.
1803 ///
1804 /// `impl_js_family_get_op_type!` is instantiated four times against
1805 /// four distinct `kind_id` enums (`SLASH` 87/81/90/87, `Regex`
1806 /// 224/250/264/225), so each expansion is a separate compiled arm
1807 /// and a drift in one grammar is invisible if only one is checked
1808 /// (grammar-dispatch section 11).
1809 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
1810 fn assert_js_family_counts(source: &str, expected: [u64; 4]) {
1811 assert_halstead_counts::<JavascriptParser>(source, "foo.js", expected, "javascript");
1812 assert_halstead_counts::<MozjsParser>(source, "foo.jsm", expected, "mozjs");
1813 assert_halstead_counts::<TypescriptParser>(source, "foo.ts", expected, "typescript");
1814 assert_halstead_counts::<TsxParser>(source, "foo.tsx", expected, "tsx");
1815 }
1816
1817 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
1818 #[test]
1819 fn js_family_regex_delimiters_are_not_operators() {
1820 // Regression: issue #1314, the JS-family sibling of Elixir
1821 // #1256 and Ruby/Perl #1312. A `regex` literal spells both of
1822 // its delimiters `SLASH` — the kind id real division uses — so
1823 // `const a = /abc/g;` reported a `/` operator with no division
1824 // in the source, and n1/N1 counted the literal's punctuation as
1825 // arithmetic.
1826 //
1827 // The same fixture pins the second, independent half: `Regex`
1828 // was in neither arm, so the literal contributed no operand
1829 // either and reached the vocabulary from *neither* side.
1830 //
1831 // expected: operators `const`, `=`, `;`, `let` → n1 = 4;
1832 // `const` `=` `;` on line 1, `let` `=` `;` on line 2, `=` `;`
1833 // on line 3 → N1 = 8. Operands `a`, `/abc/g`, `b` → n2 = 3,
1834 // with `a` used three times and `b` twice → N2 = 6.
1835 //
1836 // Before the fix: n1 = 5 and N1 = 10 (the two fabricated `/`),
1837 // n2 = 2 and N2 = 5 (no operand for the literal).
1838 //
1839 // The four values are deliberately distinct so no transposition
1840 // of the unique-vs-total axes inside `assert_halstead_counts`
1841 // can pass (#1312).
1842 assert_js_family_counts("const a = /abc/g;\nlet b = a;\nb = a;\n", [4, 8, 3, 6]);
1843 }
1844
1845 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
1846 #[test]
1847 fn js_family_division_survives_the_regex_guard() {
1848 // Control for #1314: the guard is scoped to a `Regex` parent,
1849 // so real division must still count. This fixture holds both
1850 // sides at once — two divisions and one regex literal — so a
1851 // guard widened to every `SLASH` fails here rather than
1852 // silently passing the test above.
1853 //
1854 // expected: operators `const`, `=`, `;`, `/` → n1 = 4; two
1855 // `const`, two `=`, two `;` and two `/` → N1 = 8. Operands
1856 // `q`, `a`, `b`, `c`, `r`, `/x/` → n2 = N2 = 6.
1857 assert_js_family_counts("const q = a / b / c;\nconst r = /x/;\n", [4, 8, 6, 6]);
1858 }
1859
1860 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
1861 #[test]
1862 fn js_regex_delimiter_guard_is_parent_scoped_is_unobservable() {
1863 // Companion to the two above, and a statement of what they do
1864 // *not* cover. Ruby's guard has
1865 // `ruby_regex_guard_is_parent_scoped_not_ancestor_scoped`
1866 // because a division inside `#{…}` sits under a `Regex`
1867 // ancestor without being its child. No JS fixture can do that:
1868 // a regex literal admits no nested expression at all, its
1869 // `regex_pattern` and `regex_flags` children being leaves. So
1870 // the ancestor-scoped mutant of this guard — the one #1256's
1871 // post-mortem says survives every ordinary fixture — is
1872 // unobservable here. Measured, not assumed.
1873 //
1874 // Rather than write a fixture that would pass under both
1875 // spellings and read as coverage, pin the grammar property the
1876 // claim rests on: within a fixture that puts a division, a
1877 // template substitution and a regex in one file, every `SLASH`
1878 // reachable *below* a `Regex` is its immediate child. Should a
1879 // bump start nesting expressions inside a regex, this turns red
1880 // and the distinction becomes both observable and worth a real
1881 // test.
1882 //
1883 // Checked against all four grammars, not just JavaScript: the
1884 // guard is instantiated four times against four distinct enums,
1885 // and the property this test exists to watch could hold in one
1886 // and lapse in another.
1887 let source = b"const a = /abc/g;\nconst q = x / y;\nconst t = `p ${x / y} ${/zz/} q`;\n";
1888 assert_regex_slashes_are_immediate_children::<crate::langs::JavascriptCode>(
1889 source,
1890 Javascript::SLASH as u16,
1891 Javascript::Regex as u16,
1892 "javascript",
1893 );
1894 assert_regex_slashes_are_immediate_children::<crate::langs::MozjsCode>(
1895 source,
1896 Mozjs::SLASH as u16,
1897 Mozjs::Regex as u16,
1898 "mozjs",
1899 );
1900 assert_regex_slashes_are_immediate_children::<crate::langs::TypescriptCode>(
1901 source,
1902 Typescript::SLASH as u16,
1903 Typescript::Regex as u16,
1904 "typescript",
1905 );
1906 assert_regex_slashes_are_immediate_children::<crate::langs::TsxCode>(
1907 source,
1908 Tsx::SLASH as u16,
1909 Tsx::Regex as u16,
1910 "tsx",
1911 );
1912 }
1913
1914 /// Asserts every `slash` token below a `regex` node in `source` is
1915 /// that node's *immediate* child, for one grammar.
1916 ///
1917 /// Backs `js_regex_delimiter_guard_is_parent_scoped_is_unobservable`
1918 /// — see there for why the property is worth pinning.
1919 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
1920 fn assert_regex_slashes_are_immediate_children<L: crate::traits::LanguageInfo>(
1921 source: &[u8],
1922 slash: u16,
1923 regex: u16,
1924 label: &str,
1925 ) {
1926 let mut slashes_below_a_regex = 0;
1927 let visited = for_each_node_with_chain::<L>(source, |node: &Node<'_>, chain| {
1928 if node.kind_id() != slash {
1929 return;
1930 }
1931 let Some(depth) = chain.iter().position(|a| a.kind_id() == regex) else {
1932 return;
1933 };
1934 slashes_below_a_regex += 1;
1935 assert_eq!(
1936 depth,
1937 chain.len() - 1,
1938 "{label}: a slash at row {} has a regex ancestor that is not its parent, so \
1939 the parent-vs-ancestor mutant is now observable and needs a real test",
1940 node.start_row()
1941 );
1942 });
1943 assert!(visited > 20, "{label}: fixture is too small to prove much");
1944 // Without this the assertion above is vacuous whenever the
1945 // fixture stops containing a regex at all — the failure mode a
1946 // filter that matches nothing always has.
1947 assert_eq!(
1948 slashes_below_a_regex, 4,
1949 "{label}: expected the two regex literals' four delimiters; the fixture no \
1950 longer exercises what this test claims"
1951 );
1952 }
1953
1954 // Issue #281: optional chaining (`?.`) was double-counted as a
1955 // Halstead operator in TypeScript and TSX because the grammar
1956 // exposes both an `optional_chain` named wrapper AND a child
1957 // `?.` token, and both were classified as `Operator`. The fix
1958 // counts only the bare `?.` token (`QMARKDOT`) in TS/TSX so each
1959 // textual `?.` contributes exactly once, matching JS / MozJS
1960 // (whose grammars expose only `OptionalChain` — the `?.` token
1961 // itself).
1962 //
1963 // The four assertions below all compare against the same totals:
1964 // for `function f(a) { return a?.b?.c; }` the operator stream is
1965 // `function`, `(`, `{`, `return`, `?.`, `?.`, `;` (7 total, 6
1966 // unique — `LPAREN`/`LBRACE` count once, closing tokens are not
1967 // in the operator set). Before the fix, TS/TSX reported 9/7
1968 // instead of 7/6.
1969 #[cfg(feature = "javascript")]
1970 #[test]
1971 fn javascript_optional_chain_not_double_counted_in_halstead_281() {
1972 check_metrics::<JavascriptParser>("function f(a) { return a?.b?.c; }", "foo.js", |m| {
1973 assert_eq!(m.halstead.unique_operators(), 6);
1974 assert_eq!(m.halstead.total_operators(), 7);
1975 });
1976 }
1977
1978 #[cfg(feature = "mozjs")]
1979 #[test]
1980 fn mozjs_optional_chain_not_double_counted_in_halstead_281() {
1981 check_metrics::<MozjsParser>("function f(a) { return a?.b?.c; }", "foo.js", |m| {
1982 assert_eq!(m.halstead.unique_operators(), 6);
1983 assert_eq!(m.halstead.total_operators(), 7);
1984 });
1985 }
1986
1987 #[cfg(feature = "typescript")]
1988 #[test]
1989 fn typescript_optional_chain_not_double_counted_in_halstead_281() {
1990 // The TS grammar wraps member-expression `?.` in an
1991 // `optional_chain` named node containing the bare `?.`
1992 // token; classifying both as `Operator` double-counted the
1993 // chain. We now count only the bare token, so TS matches JS.
1994 check_metrics::<TypescriptParser>("function f(a) { return a?.b?.c; }", "foo.ts", |m| {
1995 assert_eq!(m.halstead.unique_operators(), 6);
1996 assert_eq!(m.halstead.total_operators(), 7);
1997 });
1998 }
1999
2000 #[cfg(feature = "typescript")]
2001 #[test]
2002 fn tsx_optional_chain_not_double_counted_in_halstead_281() {
2003 check_metrics::<TsxParser>("function f(a) { return a?.b?.c; }", "foo.tsx", |m| {
2004 assert_eq!(m.halstead.unique_operators(), 6);
2005 assert_eq!(m.halstead.total_operators(), 7);
2006 });
2007 }
2008
2009 // Issue #299: parity guard for the JS-family `get_op_type` macro
2010 // on the optional-chain operator token (#281's prior regression
2011 // surface). All four languages must classify the bare `?.` token
2012 // identically — `OptionalChain` in JS/MozJS, `QMARKDOT` in
2013 // TS/TSX — and emit the same totals for
2014 // `function f(a) { return a?.b?.c; }`:
2015 //
2016 // * Operators: `function`, `(`, `{`, `return`, `?.`, `?.`, `;`
2017 // (7 total, 6 unique).
2018 // * Operands: `f`, `a` (parameter), `a`, `b`, `c` — the identifier
2019 // and property leaves only (5 total, 4 unique). Until #1263 the
2020 // two wrapping member expressions (`a?.b`, `a?.b?.c`) were
2021 // classified as `MemberExpression*` operands on top of the leaves
2022 // they contain, making this 7 total / 6 unique.
2023 //
2024 // Verified by test-via-revert: dropping `OptionalChain` from
2025 // JS/MozJS, or `QMARKDOT` from TS/TSX, trips the test
2026 // (u_operators 6→5). This input does NOT exercise every operand
2027 // alias in the per-language `operand_extras` (`Identifier2`, the
2028 // JS/MozJS/TSX string-literal `String2`); drift in
2029 // those is out of scope for this regression guard and would need a
2030 // separate fixture. The `PredefinedType` operator path (`: void`
2031 // double-count) is now covered by `ts_void_return_type_single_operator_453`
2032 // below.
2033 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2034 #[test]
2035 fn js_family_get_op_type_parity_optional_chain_member_299() {
2036 // Non-capturing closure (coerced to the `fn` pointer that
2037 // `check_metrics` accepts) avoids the
2038 // `clippy::needless_pass_by_value` warning that a free `fn`
2039 // taking `CodeMetrics` by value would trigger.
2040 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2041 const SRC: &str = "function f(a) { return a?.b?.c; }";
2042 let check = |m: crate::CodeMetrics| {
2043 assert_eq!(m.halstead.unique_operators(), 6);
2044 assert_eq!(m.halstead.total_operators(), 7);
2045 assert_eq!(m.halstead.unique_operands(), 4);
2046 assert_eq!(m.halstead.total_operands(), 5);
2047 };
2048
2049 check_metrics::<JavascriptParser>(SRC, "foo.js", check);
2050 check_metrics::<MozjsParser>(SRC, "foo.js", check);
2051 check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2052 check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2053 }
2054
2055 // Issue #1263: a member access contributes its leaves and the `.`
2056 // operator, never the `member_expression` composite as well. The
2057 // classification does not stop the walk, so `a` and `b` were always
2058 // counted; listing the wrapper billed a third operand keyed on the
2059 // whole `a.b` text, which no other language here does.
2060 //
2061 // expected, for `var r = a.b;`:
2062 //
2063 // * Operators: `var`, `=`, `.`, `;` — 4 total, 4 unique.
2064 // * Operands: `r`, `a`, `b` — 3 total, 3 unique. Before the fix
2065 // the `member_expression` wrapper added `a.b`, making both 4.
2066 //
2067 // All four JS-family languages are asserted because
2068 // `impl_js_family_get_op_type!` emits one shared operand arm: the
2069 // lockstep is the point of the macro, and a per-language extras
2070 // list is exactly where a future edit could break it.
2071 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2072 #[test]
2073 fn js_family_member_access_counts_leaves_not_the_composite_1263() {
2074 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2075 const SRC: &str = "var r = a.b;";
2076 let check = |m: crate::CodeMetrics| {
2077 assert_eq!(m.halstead.unique_operators(), 4);
2078 assert_eq!(m.halstead.total_operators(), 4);
2079 assert_eq!(m.halstead.unique_operands(), 3);
2080 assert_eq!(m.halstead.total_operands(), 3);
2081 };
2082
2083 check_metrics::<JavascriptParser>(SRC, "foo.js", check);
2084 check_metrics::<MozjsParser>(SRC, "foo.js", check);
2085 check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2086 check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2087 }
2088
2089 // Issue #1263, the grammar-dispatch section 6 half: dropping
2090 // `MemberExpression*` from the operand arm would have regressed
2091 // private-field access to *zero* operands for the field, because
2092 // `PrivatePropertyIdentifier` — the `#x` leaf — was in no operand
2093 // list and the composite `this.#x` had been its only count. Adding
2094 // the leaf also fixes the declaration site `#x = 1`, which no
2095 // wrapper covered and which therefore counted nothing at all.
2096 //
2097 // expected, for `class C { #x = 1; m() { return this.#x; } }`:
2098 //
2099 // * Operators: `{`×2 (class body, method body), `=`, `;`×2, `(`,
2100 // `return`, `.` — 8 total, 6 unique. (`class` is not in the
2101 // JS-family operator arm, so it contributes nothing; that is
2102 // pre-existing and unrelated.)
2103 // * Operands: `C`, `#x`, `1`, `m`, `this`, `#x` — 6 total, 5
2104 // unique under JS/MozJS. Under TS/TSX the class *name* `C`
2105 // parses as `type_identifier`, which those getters do not
2106 // classify, so both counts drop by one to 5/4 — a pre-existing
2107 // divergence this fixture records rather than fixes.
2108 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2109 #[test]
2110 fn js_family_private_field_leaf_is_the_operand_1263() {
2111 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2112 const SRC: &str = "class C { #x = 1; m() { return this.#x; } }";
2113 let check_js = |m: crate::CodeMetrics| {
2114 assert_eq!(m.halstead.unique_operators(), 6);
2115 assert_eq!(m.halstead.total_operators(), 8);
2116 assert_eq!(m.halstead.unique_operands(), 5);
2117 assert_eq!(m.halstead.total_operands(), 6);
2118 };
2119 let check_ts = |m: crate::CodeMetrics| {
2120 assert_eq!(m.halstead.unique_operators(), 6);
2121 assert_eq!(m.halstead.total_operators(), 8);
2122 assert_eq!(m.halstead.unique_operands(), 4);
2123 assert_eq!(m.halstead.total_operands(), 5);
2124 };
2125
2126 check_metrics::<JavascriptParser>(SRC, "foo.js", check_js);
2127 check_metrics::<MozjsParser>(SRC, "foo.js", check_js);
2128 check_metrics::<TypescriptParser>(SRC, "foo.ts", check_ts);
2129 check_metrics::<TsxParser>(SRC, "foo.tsx", check_ts);
2130 }
2131
2132 // Issue #1263, the other section 6 half: `meta_property` is the one
2133 // composite the leaves-not-composites drop has to keep. `import.meta`
2134 // / `new.target` have no classified leaf — `meta` and `target` are
2135 // anonymous tokens in no arm — so with `MemberExpression*` gone the
2136 // meta-object contributed no operand at all while `this.env.x` still
2137 // yielded three.
2138 //
2139 // expected operands, for `var t = import.meta.url; function f() {
2140 // return new.target; }`: `t`, `import.meta`, `url`, `f`,
2141 // `new.target` — 5 total, 5 unique. Operators are deliberately not
2142 // asserted: the `import` / `new` keyword tokens inside the
2143 // meta-property keep their pre-existing operator classification,
2144 // which this fixture neither pins nor contests.
2145 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2146 #[test]
2147 fn js_family_meta_property_is_one_operand_1263() {
2148 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2149 const SRC: &str = "var t = import.meta.url; function f() { return new.target; }";
2150 let check = |m: crate::CodeMetrics| {
2151 assert_eq!(m.halstead.unique_operands(), 5);
2152 assert_eq!(m.halstead.total_operands(), 5);
2153 };
2154
2155 check_metrics::<JavascriptParser>(SRC, "foo.js", check);
2156 check_metrics::<MozjsParser>(SRC, "foo.js", check);
2157 check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2158 check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2159 }
2160
2161 // Issue #1263: TS/TSX `nested_identifier` (`namespace N.M`) is the
2162 // same container/leaf double-count as `member_expression`.
2163 //
2164 // expected, for `namespace N.M { }`:
2165 //
2166 // * Operators: `.`, `{` — 2 total, 2 unique. (`namespace` is not in
2167 // the JS-family operator arm.)
2168 // * Operands: `N`, `M` — 2 total, 2 unique. Before the fix the
2169 // `nested_identifier` added `N.M`, making both 3.
2170 #[cfg(feature = "typescript")]
2171 #[test]
2172 fn ts_nested_identifier_counts_leaves_not_the_composite_1263() {
2173 const SRC: &str = "namespace N.M { }";
2174 let check = |m: crate::CodeMetrics| {
2175 assert_eq!(m.halstead.unique_operators(), 2);
2176 assert_eq!(m.halstead.total_operators(), 2);
2177 assert_eq!(m.halstead.unique_operands(), 2);
2178 assert_eq!(m.halstead.total_operands(), 2);
2179 };
2180
2181 check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2182 check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2183 }
2184
2185 // Issue #1261 (inverting the #313 pin): the `"string"` type-keyword
2186 // aliases the TS / TSX grammars expose must contribute NO operand.
2187 // #313 put them in `operand_extras` for parity with the then-wider
2188 // `Checker::is_string`, but the `predefined_type` wrapper already
2189 // counts as the text-keyed `"string"` operator, so one `: string`
2190 // token tallied as operator AND operand while `: number` counted
2191 // once. #1261 drops the aliases from both `operand_extras` and
2192 // `is_string`, so the keyword counts once, as the operator.
2193 //
2194 // For the input `let x: string = "y";`:
2195 //
2196 // * TypeScript emits `Typescript::String2` for the `string` type
2197 // keyword (kind_id 135, in the type-keyword block of the enum).
2198 // * TSX emits `Tsx::String3` for the same role (kind_id 141).
2199 //
2200 // Verified by test-via-revert: restoring `String2` to TS's
2201 // `operand_extras` (or `String3` to TSX's) trips this test on
2202 // `u_operands` / `operands` for the affected language.
2203 #[cfg(feature = "typescript")]
2204 #[test]
2205 fn ts_family_type_keyword_counts_once_1261() {
2206 const SRC: &str = "let x: string = \"y\";";
2207 // Operators (n1 = 5, N1 = 5):
2208 // `let`, `:`, `=`, `;`, plus `string` (PredefinedType wrapper,
2209 // routed through `is_primitive` so it's keyed by its lexeme
2210 // `"string"` in `primitive_operators`).
2211 // Operands (n2 = 2, N2 = 2):
2212 // `x` and the `"y"` literal. Under #313 the type-keyword
2213 // child of `predefined_type` added a third, phantom
2214 // `"string"` operand (n2 = 3 / N2 = 3).
2215 let check = |m: crate::CodeMetrics| {
2216 assert_eq!(m.halstead.unique_operators(), 5);
2217 assert_eq!(m.halstead.total_operators(), 5);
2218 assert_eq!(m.halstead.unique_operands(), 2);
2219 assert_eq!(m.halstead.total_operands(), 2);
2220 };
2221
2222 check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2223 check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2224 }
2225
2226 // Issue #1261 regression, the issue's reproducer plus a literal
2227 // whose contents spell the keyword: `: string` and `: number` must
2228 // contribute symmetrically — one text-keyed operator each, zero
2229 // operands — while a string *literal* `"string"` stays an operand
2230 // (distinct from the keyword: TS kind `String`, TSX kind `String2`,
2231 // both quoted in the operand key).
2232 #[cfg(feature = "typescript")]
2233 #[test]
2234 fn ts_family_string_annotation_symmetric_with_number_1261() {
2235 const SRC: &str = "let x: string = \"a\";\nlet y: number = 1;\nlet s = \"string\";";
2236 // Operators (n1 = 6, N1 = 13):
2237 // `let` ×3, `:` ×2, `=` ×3, `;` ×3, `string` ×1, `number` ×1.
2238 // Pre-fix N1 was identical — the wrapper operator was always
2239 // counted; the defect was the extra operand below.
2240 // Operands (n2 = 6, N2 = 6):
2241 // `x`, `"a"`, `y`, `1`, `s`, `"string"` — one each. Pre-fix
2242 // the `: string` keyword added a bare `string` operand
2243 // (n2 = 7 / N2 = 7) that `: number` had no analogue of.
2244 let check = |m: crate::CodeMetrics| {
2245 assert_eq!(m.halstead.unique_operators(), 6);
2246 assert_eq!(m.halstead.total_operators(), 13);
2247 assert_eq!(m.halstead.unique_operands(), 6);
2248 assert_eq!(m.halstead.total_operands(), 6);
2249 };
2250
2251 check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2252 check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2253 }
2254
2255 /// Drift marker for #1261 (lesson 34 / grammar-dispatch §2): the
2256 /// anonymous `string` type-keyword token appears **only** as a
2257 /// `predefined_type` child.
2258 ///
2259 /// That is the whole argument for classifying the keyword without a
2260 /// parent guard — the wrapper is guaranteed to be there to carry the
2261 /// operator. The two tests above measure the consequence and would
2262 /// still pass if the grammar started emitting the keyword somewhere
2263 /// else, as long as their own two fixtures kept their counts; this one
2264 /// measures the premise, over every position #1261's dump probes
2265 /// covered: annotation, parameter and return type, union member,
2266 /// generic argument, and template-literal type. A string *literal*
2267 /// spelling `"string"` is a different kind and must not be confused
2268 /// for the keyword, so one is in the fixture too.
2269 #[cfg(feature = "typescript")]
2270 #[test]
2271 fn ts_family_type_keyword_only_appears_under_predefined_type_1261() {
2272 // Exercises each position the keyword can take. Valid in both
2273 // grammars: no angle-bracket cast, which TSX would read as JSX.
2274 const SRC: &str = "const a: string = \"string\";\n\
2275 function f(x: string): string {\n\
2276 return x;\n\
2277 }\n\
2278 type U = string | number;\n\
2279 type A = Array<string>;\n\
2280 type M = Map<string, number>;\n\
2281 type T = `id-${string}`;\n";
2282 // Seven type positions: `a`, `x`, `f`'s return, the union member,
2283 // `Array`'s argument, `Map`'s first argument, and the template
2284 // placeholder. The `"string"` initialiser is a literal, not the
2285 // keyword, and must not be among them.
2286 const EXPECTED_OCCURRENCES: usize = 7;
2287
2288 fn keyword_occurrences<P: ParserTrait>(
2289 path: &str,
2290 keyword: u16,
2291 predefined_type: u16,
2292 ) -> usize {
2293 let parser = P::new(SRC.as_bytes().to_vec(), &PathBuf::from(path), None);
2294 parser
2295 .root()
2296 .preorder()
2297 .filter(|node| node.kind_id() == keyword)
2298 .inspect(|node| {
2299 assert_eq!(
2300 node.parent().map(|parent| parent.kind_id()),
2301 Some(predefined_type),
2302 "the `string` type keyword surfaced outside \
2303 `predefined_type` in {path}; the operator is carried \
2304 by the wrapper, so `get_op_type` needs a parent guard \
2305 before that arm can be trusted (#1261)",
2306 );
2307 })
2308 .count()
2309 }
2310
2311 // Kind ids re-read from the generated enums, not carried over: TS
2312 // `String2` = 135, TSX `String3` = 141 (TSX's `String2` = 261 is the
2313 // string-literal production and stays an operand).
2314 assert_eq!(
2315 keyword_occurrences::<TypescriptParser>(
2316 "foo.ts",
2317 Typescript::String2 as u16,
2318 Typescript::PredefinedType as u16,
2319 ),
2320 EXPECTED_OCCURRENCES,
2321 "TypeScript no longer emits the `string` type keyword in every \
2322 position #1261 probed",
2323 );
2324 assert_eq!(
2325 keyword_occurrences::<TsxParser>(
2326 "foo.tsx",
2327 Tsx::String3 as u16,
2328 Tsx::PredefinedType as u16,
2329 ),
2330 EXPECTED_OCCURRENCES,
2331 "TSX no longer emits the `string` type keyword in every position \
2332 #1261 probed",
2333 );
2334 }
2335
2336 // Issue #453: a `void` return type must contribute exactly one
2337 // Halstead operator. The TS / TSX grammars parse `: void` as a
2338 // `predefined_type` wrapper around an inner `void` token. `is_primitive`
2339 // routes the wrapper into the text-keyed `primitive_operators` map as
2340 // `"void"`, while the inner `Void` token is independently a standalone
2341 // expression operator (`void 0`). Pre-fix both classified as operators
2342 // and one source `void` counted as TWO distinct Halstead operators.
2343 // The fix suppresses the wrapper when its child is a `Void` token, so
2344 // only the inner token carries the operator — matching expression
2345 // `void 0` and keeping the kind_id-keyed count consistent.
2346 //
2347 // For `function f(): void { return; }`:
2348 //
2349 // * Operators (n1 = 7, N1 = 7): `function`, `()`, `{}`, `:`, `return`,
2350 // `;`, and a single `void`. (The untyped form is n1 = 5; the `: void`
2351 // annotation adds the `:` operator and one `void`, NOT two — the
2352 // issue's "n1 = 6" target overlooked the annotation colon.)
2353 //
2354 // Verified by test-via-revert: removing the `predefined_void` guard
2355 // restores the pre-fix `u_operators` 7 -> 8 with a duplicate `"void"`
2356 // (one kind_id-keyed, one in `primitive_operators`). Both `metrics()`
2357 // and the `ops`-list dedup invariant (`ts_void_return_and_expression_*`
2358 // in `ops.rs`) are pinned per lesson 4.
2359 #[cfg(feature = "typescript")]
2360 #[test]
2361 fn ts_void_return_type_single_operator_453() {
2362 const SRC: &str = "function f(): void { return; }";
2363 let check = |m: crate::CodeMetrics| {
2364 assert_eq!(m.halstead.unique_operators(), 7);
2365 assert_eq!(m.halstead.total_operators(), 7);
2366 };
2367
2368 check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2369 check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2370 }
2371
2372 // Issue #453 over-suppression guard: expression `void 0` (a
2373 // `unary_expression`, NOT a `predefined_type` wrapper) must still
2374 // count `void` as exactly one operator. The fix keys only on a
2375 // `predefined_type` whose child is a `Void` token, so the bare
2376 // expression operator is untouched.
2377 //
2378 // For `const x = void 0;`:
2379 //
2380 // * Operators (n1 = 4, N1 = 4): `const`, `=`, `void`, `;`.
2381 // * Operands (n2 = 2, N2 = 2): `x`, `0`.
2382 #[cfg(feature = "typescript")]
2383 #[test]
2384 fn ts_void_expression_still_single_operator_453() {
2385 const SRC: &str = "const x = void 0;";
2386 let check = |m: crate::CodeMetrics| {
2387 assert_eq!(m.halstead.unique_operators(), 4);
2388 assert_eq!(m.halstead.total_operators(), 4);
2389 assert_eq!(m.halstead.unique_operands(), 2);
2390 assert_eq!(m.halstead.total_operands(), 2);
2391 };
2392
2393 check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2394 check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2395 }
2396
2397 #[cfg(feature = "python")]
2398 #[test]
2399 fn python_wrong_operators() {
2400 check_metrics::<PythonParser>("()[]{}", "foo.py", |metric| {
2401 insta::assert_json_snapshot!(
2402 metric.halstead,
2403 @r#"
2404 {
2405 "unique_operators": 0,
2406 "total_operators": 0,
2407 "unique_operands": 0,
2408 "total_operands": 0,
2409 "length": 0,
2410 "estimated_program_length": 0.0,
2411 "purity_ratio": 0.0,
2412 "vocabulary": 0,
2413 "volume": 0.0,
2414 "difficulty": 0.0,
2415 "level": 0.0,
2416 "effort": 0.0,
2417 "time": 0.0,
2418 "bugs": 0.0
2419 }
2420 "#
2421 );
2422 });
2423 }
2424
2425 #[cfg(feature = "python")]
2426 #[test]
2427 fn python_check_metrics() {
2428 check_metrics::<PythonParser>(
2429 "def f():
2430 pass",
2431 "foo.py",
2432 |metric| {
2433 insta::assert_json_snapshot!(
2434 metric.halstead,
2435 @r#"
2436 {
2437 "unique_operators": 2,
2438 "total_operators": 2,
2439 "unique_operands": 1,
2440 "total_operands": 1,
2441 "length": 3,
2442 "estimated_program_length": 2.0,
2443 "purity_ratio": 0.6666666666666666,
2444 "vocabulary": 3,
2445 "volume": 4.754887502163468,
2446 "difficulty": 1.0,
2447 "level": 1.0,
2448 "effort": 4.754887502163468,
2449 "time": 0.26416041678685936,
2450 "bugs": 0.0009425525573729414
2451 }
2452 "#
2453 );
2454 },
2455 );
2456 }
2457
2458 #[cfg(feature = "java")]
2459 #[test]
2460 fn java_operators_and_operands() {
2461 check_metrics::<JavaParser>(
2462 "public class Main {
2463 public static void main(string args[]) {
2464 int a, b, c, avg;
2465 a = 5; b = 5; c = 5;
2466 avg = (a + b + c) / 3;
2467 MessageFormat.format(\"{0}\", avg);
2468 }
2469 }",
2470 "foo.java",
2471 |metric| {
2472 // Operators (n1=11): {} void () [] , . ; int = + /
2473 // Operands (n2=12): Main main args a b c avg 5 3 MessageFormat format "{0}"
2474 insta::assert_json_snapshot!(
2475 metric.halstead,
2476 @r#"
2477 {
2478 "unique_operators": 11,
2479 "total_operators": 26,
2480 "unique_operands": 12,
2481 "total_operands": 22,
2482 "length": 48,
2483 "estimated_program_length": 81.07329781366414,
2484 "purity_ratio": 1.6890270377846697,
2485 "vocabulary": 23,
2486 "volume": 217.13097389073664,
2487 "difficulty": 10.083333333333334,
2488 "level": 0.09917355371900825,
2489 "effort": 2189.4039867315946,
2490 "time": 121.63355481842193,
2491 "bugs": 0.05620341201461669
2492 }
2493 "#
2494 );
2495 },
2496 );
2497 }
2498
2499 #[cfg(feature = "java")]
2500 #[test]
2501 fn java_primitive_types_and_booleans() {
2502 check_metrics::<JavaParser>(
2503 "public class Prims {
2504 byte a = 1;
2505 short b = 2;
2506 int c = 3;
2507 long d = 4;
2508 char e = 'x';
2509 float f = 1.0f;
2510 double g = 2.0;
2511 boolean h = true;
2512 boolean i = false;
2513 }",
2514 "foo.java",
2515 |metric| {
2516 // Verifies all 8 Java primitive-type keywords (byte, short, int, long,
2517 // char, float, double, boolean) are counted as distinct operators, and
2518 // that true/false are counted as operands.
2519 insta::assert_json_snapshot!(
2520 metric.halstead,
2521 @r#"
2522 {
2523 "unique_operators": 11,
2524 "total_operators": 28,
2525 "unique_operands": 19,
2526 "total_operands": 19,
2527 "length": 47,
2528 "estimated_program_length": 118.76437056043838,
2529 "purity_ratio": 2.526901501285923,
2530 "vocabulary": 30,
2531 "volume": 230.62385799360038,
2532 "difficulty": 5.5,
2533 "level": 0.18181818181818182,
2534 "effort": 1268.4312189648022,
2535 "time": 70.46840105360012,
2536 "bugs": 0.03905920146699976
2537 }
2538 "#
2539 );
2540 },
2541 );
2542 }
2543
2544 // #1380: `this` and `super` were swept into the operator arm under a
2545 // `// Operator: … keywords` heading — classification by lexical
2546 // class rather than by Halstead role. A member access is
2547 // `<receiver> <op> <field>`, so billing the receiver as an operator
2548 // scored `this.x` as a binary operator with one operand while `p.x`
2549 // is one operator and two operands. Both are operands now, matching
2550 // the eleven other languages that classify a self-reference.
2551 #[cfg(feature = "java")]
2552 #[test]
2553 fn java_self_and_super_references_are_operands() {
2554 let source = "class T {\n int f() { return this.x + super.y; }\n}";
2555 // Operators (n1=7, N1=9): {} x2, int, (), return, . x2, +, ;
2556 // Operands (n2=6, N2=6): T, f, this, x, super, y
2557 // Before the fix this read (9, 11, 4, 4) — `this` and `super`
2558 // billed into the operator side of both counts.
2559 assert_halstead_counts::<JavaParser>(
2560 source,
2561 "foo.java",
2562 [7, 9, 6, 6],
2563 "java self/super references",
2564 );
2565 assert_keywords_are_operands_only::<JavaParser>(source, "foo.java", &["this", "super"]);
2566 }
2567
2568 // The `super` of a wildcard type bound denotes no value, so it keeps
2569 // the operator classification `? extends T`'s `extends` already has
2570 // — the two keywords are siblings under the same `wildcard` node and
2571 // nothing but the parent separates them from a real self-reference
2572 // (#1380). The explicit receiver parameter in the same fixture is
2573 // the independent path through the operand arm (grammar-dispatch
2574 // section 11): only it can put `this` in the operand vocabulary
2575 // here, and only the wildcard can put `super` in the operator one.
2576 #[cfg(feature = "java")]
2577 #[test]
2578 fn java_wildcard_super_bound_stays_an_operator() {
2579 let source = "import java.util.List;\n\
2580 class T {\n \
2581 void m(T T.this, List<? super String> a, List<? extends String> b) { }\n\
2582 }";
2583 let ops = ops_of::<JavaParser>(source, "foo.java");
2584
2585 assert!(
2586 ops.operators.iter().any(|o| o == "super"),
2587 "`? super String` must keep `super` an operator; operators were {:?}",
2588 ops.operators
2589 );
2590 assert!(
2591 !ops.operands.iter().any(|o| o == "super"),
2592 "a wildcard bound must not bill `super` as an operand; operands were {:?}",
2593 ops.operands
2594 );
2595 assert!(
2596 ops.operators.iter().any(|o| o == "extends"),
2597 "the fixture must still contain the `? extends String` \
2598 bound this arm mirrors; operators were {:?}",
2599 ops.operators
2600 );
2601 assert_keywords_are_operands_only::<JavaParser>(source, "foo.java", &["this"]);
2602 }
2603
2604 /// One `this` and one `super` per container the pinned
2605 /// `tree-sitter-java` can put them in: a delegating constructor
2606 /// call, an explicit superclass constructor call, a field access, a
2607 /// method-invocation receiver, a method reference, a call argument,
2608 /// and the two qualified forms an inner class allows.
2609 #[cfg(feature = "java")]
2610 const JAVA_SELF_POSITIONS: &str = "class Pos extends P {
2611 int x;
2612 Pos() { this(1); }
2613 Pos(int v) { super(v); this.x = v; }
2614 int a() { return this.x; }
2615 int b() { return super.h(); }
2616 java.util.function.IntSupplier c() { return this::a; }
2617 void d() { g(this); }
2618 void g(Object o) { }
2619 class In {
2620 int e() { return Pos.this.x; }
2621 int f() { return Pos.super.h(); }
2622 }
2623 }";
2624
2625 #[cfg(feature = "java")]
2626 #[test]
2627 fn java_self_and_super_leaves_are_unaliased_and_unconditional() {
2628 assert_self_reference_leaves::<JavaCode>(
2629 JAVA_SELF_POSITIONS,
2630 &[
2631 ("this", Java::This as u16, 6),
2632 ("super", Java::Super as u16, 3),
2633 ],
2634 "java",
2635 );
2636 }
2637
2638 // Groovy's `Super` arm, pinned from all three sides. The pinned
2639 // grammar emits the `super` token only as a `wildcard` bound, where it
2640 // is an operator exactly as in Java above; every super-*reference* is
2641 // a plain `identifier`, and so an operand.
2642 //
2643 // Since #1419 the arm carries Java's `Wildcard` parent gate, so the
2644 // bound half below exercises the gate's operator branch — inverting
2645 // the gate fails it. The gate's operand branch is unreachable under
2646 // the pin (only error recovery on invalid Groovy, `List<? super>`,
2647 // detaches a `super` token from its `wildcard`), so it is
2648 // deliberately untested rather than pinned against an invalid
2649 // fixture. The node census is the drift marker for it: a grammar bump
2650 // that routes a reference to `Groovy::Super` fails here by name.
2651 #[cfg(feature = "groovy")]
2652 #[test]
2653 fn groovy_wildcard_super_bound_stays_an_operator() {
2654 let bounds =
2655 "class T {\n void m(List<? super String> a, List<? extends String> b) { }\n}";
2656 let ops = ops_of::<GroovyParser>(bounds, "foo.groovy");
2657 assert!(
2658 ops.operators.iter().any(|o| o == "super"),
2659 "`? super String` must keep `super` an operator; operators were {:?}",
2660 ops.operators
2661 );
2662 assert!(
2663 !ops.operands.iter().any(|o| o == "super"),
2664 "a wildcard bound must not bill `super` as an operand; operands were {:?}",
2665 ops.operands
2666 );
2667 assert!(
2668 ops.operators.iter().any(|o| o == "extends"),
2669 "the fixture must still contain the `? extends String` bound this arm \
2670 mirrors; operators were {:?}",
2671 ops.operators
2672 );
2673
2674 let mixed = "class T extends B {\n void m(List<? super String> a) { super.h() }\n}";
2675 let mut tokens = 0;
2676 for_each_node_with_chain::<GroovyCode>(mixed.as_bytes(), |node, chain| {
2677 if node.kind_id() == Groovy::Super as u16 {
2678 tokens += 1;
2679 assert_eq!(
2680 chain.last().map(Node::kind_id),
2681 Some(Groovy::Wildcard as u16),
2682 "groovy: a `super` token outside a wildcard bound now reaches \
2683 the gate's operand branch, which the pinned grammar made \
2684 unreachable — re-check the arm against the new grammar (#1419)"
2685 );
2686 }
2687 });
2688 assert_eq!(
2689 tokens, 1,
2690 "the fixture's one wildcard bound must still spell `super`, and its \
2691 `super.h()` reference must not"
2692 );
2693
2694 // The reference direction, on valid input the gate must leave
2695 // alone: every super-reference form the grammar accepts stays an
2696 // operand, so splitting `Super` out of the operator alternation
2697 // cannot have moved one. `this` rides along for the same reason
2698 // Java's test carries it.
2699 let refs = "class A extends B {\n A() { super(1) }\n \
2700 def g() { return super.h() }\n \
2701 def i() { return A.super.h() }\n \
2702 def j() { return super::h }\n \
2703 def k() { return super?.h() }\n \
2704 def l() { return this.x }\n}";
2705 assert_keywords_are_operands_only::<GroovyParser>(refs, "foo.groovy", &["super", "this"]);
2706 }
2707
2708 #[cfg(feature = "groovy")]
2709 #[test]
2710 fn groovy_operators_and_operands() {
2711 check_metrics::<GroovyParser>(
2712 "class Main {
2713 static void main(String[] args) {
2714 int a, b, c, avg;
2715 a = 5; b = 5; c = 5;
2716 avg = (a + b + c) / 3;
2717 println(avg);
2718 }
2719 }",
2720 "foo.groovy",
2721 |metric| {
2722 // Groovy mirror of `java_operators_and_operands`. The juxt
2723 // call `println avg` exercises `juxt_function_call` in
2724 // place of Java's `MessageFormat.format(...)`. amaanq's
2725 // grammar inherits Java's tokenisation, so n1/N1/n2/N2
2726 // shapes match Java up to those substitutions.
2727 // The dekobon grammar parses primitive type names
2728 // (`void`, `int`, `String`) as `type_identifier`
2729 // rather than as distinct keyword tokens, so they
2730 // count as operands here — the prior amaanq grammar
2731 // treated them as operators. Net shift: −2 unique
2732 // operators (`void`, `int`), +2 unique operands
2733 // (`void`, `int` were the only two type_identifiers
2734 // not already counted as operands, since `String`
2735 // was already an identifier in the prior grammar's
2736 // counting).
2737 assert_eq!(metric.halstead.unique_operators(), 8);
2738 assert_eq!(metric.halstead.unique_operands(), 13);
2739 insta::assert_json_snapshot!(
2740 metric.halstead,
2741 @r#"
2742 {
2743 "unique_operators": 8,
2744 "total_operators": 22,
2745 "unique_operands": 13,
2746 "total_operands": 23,
2747 "length": 45,
2748 "estimated_program_length": 72.10571633583419,
2749 "purity_ratio": 1.6023492519074265,
2750 "vocabulary": 21,
2751 "volume": 197.65428402504423,
2752 "difficulty": 7.076923076923077,
2753 "level": 0.14130434782608697,
2754 "effort": 1398.7841638695438,
2755 "time": 77.71023132608576,
2756 "bugs": 0.04169134280255714
2757 }
2758 "#
2759 );
2760 },
2761 );
2762 }
2763
2764 #[cfg(feature = "groovy")]
2765 #[test]
2766 fn groovy_primitive_types_and_booleans() {
2767 check_metrics::<GroovyParser>(
2768 "class Prims {
2769 byte a = 1
2770 short b = 2
2771 int c = 3
2772 long d = 4
2773 char e = 'x'
2774 float f = 1.0f
2775 double g = 2.0
2776 boolean h = true
2777 boolean i = false
2778 }",
2779 "foo.groovy",
2780 |metric| {
2781 // The dekobon grammar consolidates the 8 primitive
2782 // type names (`byte`, `short`, `int`, `long`, `char`,
2783 // `float`, `double`, `boolean`) under `type_identifier`
2784 // — so they count as operands, not as distinct
2785 // operators. Likewise numeric literals collapse to one
2786 // `NumberLiteral` shape (no Hex/Octal/Binary/Decimal
2787 // split), and `'x'` parses as `StringLiteral` (Groovy
2788 // single-quoted strings) rather than as
2789 // `CharacterLiteral`. Operators remaining in this
2790 // fixture: `=` and `class`-body braces (only `{` is in
2791 // the operator set). True/false collapse under one
2792 // `BooleanLiteral`.
2793 assert_eq!(metric.halstead.unique_operators(), 2);
2794 assert_eq!(metric.halstead.unique_operands(), 27);
2795 insta::assert_json_snapshot!(
2796 metric.halstead,
2797 @r#"
2798 {
2799 "unique_operators": 2,
2800 "total_operators": 10,
2801 "unique_operands": 27,
2802 "total_operands": 28,
2803 "length": 38,
2804 "estimated_program_length": 130.38196255841365,
2805 "purity_ratio": 3.4311042778529908,
2806 "vocabulary": 29,
2807 "volume": 184.60327781484773,
2808 "difficulty": 1.037037037037037,
2809 "level": 0.9642857142857143,
2810 "effort": 191.44043625243467,
2811 "time": 10.635579791801925,
2812 "bugs": 0.01107221547116606
2813 }
2814 "#
2815 );
2816 },
2817 );
2818 }
2819
2820 // Issue #1263 swept Groovy alongside the JS family and C#: its
2821 // operand arm listed `QualifiedName` (a `package` / `import` path)
2822 // and `QualifiedType` on top of the identifier leaves the walker
2823 // already reached.
2824 //
2825 // Only the `QualifiedName` half was observable. The runtime emits
2826 // `qualified_type` as the *alias* `QualifiedType2` (kind_id 228),
2827 // which the arm never named — a lesson-2 miss that, by accident,
2828 // made that half already leaves-only and is why #1263's issue body
2829 // recorded Groovy as compliant. Both kinds are gone rather than
2830 // completed.
2831 //
2832 // expected, for `package com.example`: operators `.` (1/1);
2833 // operands `com`, `example` (2/2). Pre-fix the `qualified_name`
2834 // added `com.example`, making the operand counts 3/3.
2835 #[cfg(feature = "groovy")]
2836 #[test]
2837 fn groovy_qualified_name_counts_leaves_not_the_composite_1263() {
2838 check_metrics::<GroovyParser>("package com.example", "foo.groovy", |metric| {
2839 assert_eq!(metric.halstead.unique_operators(), 1);
2840 assert_eq!(metric.halstead.total_operators(), 1);
2841 assert_eq!(metric.halstead.unique_operands(), 2);
2842 assert_eq!(metric.halstead.total_operands(), 2);
2843 });
2844 }
2845
2846 // The type half of the same arm (#1352). #1263 dropped
2847 // `QualifiedType` alongside `QualifiedName`, but only the latter
2848 // was pinned: adding `QualifiedType2` back to the operand arm
2849 // failed none of the 3,523 tests then in the lib targets, so the
2850 // leaves-only reading of a qualified *type* was correct by accident
2851 // rather than by contract. The tempting "fix" for the alias miss
2852 // #1263 recorded is to complete the list with 228, which is exactly
2853 // the double count #1263 removed — this test is what stops that.
2854 //
2855 // The kind assertions are the grammar-dispatch section 1 / 2 drift
2856 // marker: if a grammar bump renumbers the alias, the operand
2857 // assertions below would keep passing while measuring a construct
2858 // this arm no longer describes.
2859 //
2860 // expected, for `java.util.List x = null`: operators `.` × 2 and
2861 // `=` → n1 = 2, N1 = 3; operands `java`, `util`, `List`, `x`,
2862 // `null` → n2 = 5, N2 = 5. Listing the wrapper would add the whole
2863 // span `java.util.List`, making the operand counts 6/6.
2864 #[cfg(feature = "groovy")]
2865 #[test]
2866 fn groovy_qualified_type_counts_leaves_not_the_composite_1352() {
2867 const SOURCE: &str = "java.util.List x = null";
2868
2869 let parser = GroovyParser::new(
2870 SOURCE.as_bytes().to_vec(),
2871 &PathBuf::from("foo.groovy"),
2872 None,
2873 );
2874 assert!(
2875 ast_has_kind_id(&parser, Groovy::QualifiedType2 as u16),
2876 "the dekobon grammar no longer emits `qualified_type` as the \
2877 alias `QualifiedType2`; re-derive the Groovy operand arm \
2878 before trusting the counts below",
2879 );
2880 assert!(
2881 !ast_has_kind_id(&parser, Groovy::QualifiedType as u16),
2882 "the unsuffixed `QualifiedType` is now reachable; it is a \
2883 second wrapper this arm must keep excluded",
2884 );
2885
2886 check_metrics::<GroovyParser>(SOURCE, "foo.groovy", |metric| {
2887 assert_eq!(metric.halstead.unique_operators(), 2);
2888 assert_eq!(metric.halstead.total_operators(), 3);
2889 assert_eq!(metric.halstead.unique_operands(), 5);
2890 assert_eq!(metric.halstead.total_operands(), 5);
2891 });
2892 }
2893
2894 #[cfg(feature = "groovy")]
2895 #[test]
2896 fn groovy_closure_operators_and_operands() {
2897 check_metrics::<GroovyParser>("def double = { x -> x * 2 }", "foo.groovy", |metric| {
2898 // Closure with arrow-style parameter list.
2899 // Distinct operators: def, =, {}, ->, * = 5.
2900 // Distinct operands: double, x, 2 = 3.
2901 assert_eq!(metric.halstead.unique_operators(), 5);
2902 assert_eq!(metric.halstead.unique_operands(), 3);
2903 });
2904 }
2905
2906 /// Regression for issue #247: every Groovy-specific operator the
2907 /// prior amaanq grammar dropped to ERROR or mis-shaped as a Java
2908 /// node now parses as a distinct lexer token in the dekobon
2909 /// grammar, so Halstead counts each one. The fixture below
2910 /// exercises Elvis `?:`, safe-nav `?.`, safe-chain `??.`,
2911 /// spread-dot `*.`, method-pointer `.&`, direct-field `.@`,
2912 /// identity `===` / `!==`, spaceship `<=>`, regex `=~` / `==~`,
2913 /// exclusive ranges `..<` / `<..` / `<..<`, `as` coercion, and
2914 /// `?[` safe index — every distinct operator kind must appear in
2915 /// `u_operators` (the count grows by exactly the number of new
2916 /// distinct operator tokens introduced).
2917 #[cfg(feature = "groovy")]
2918 #[test]
2919 fn groovy_dekobon_operator_coverage_247() {
2920 check_metrics::<GroovyParser>(
2921 "def f(a, b, list, s) {
2922 def x = a ?: b
2923 def y = a?.field
2924 def z = a??.field
2925 def items = list*.size()
2926 def ptr = a.&size
2927 def fld = a.@field
2928 def id1 = a === b
2929 def id2 = a !== b
2930 def ship = a <=> b
2931 def find = s =~ /pat/
2932 def match = s ==~ /^pat\\$/
2933 def r1 = 0..<10
2934 def r2 = 0<..10
2935 def r3 = 0<..<10
2936 def cast = a as String
2937 def safe = list?[0]
2938 return x
2939 }",
2940 "foo.groovy",
2941 |metric| {
2942 // Exact pin: with the dekobon Groovy grammar this
2943 // fixture exercises 16 Groovy-specific tokens (`?:`,
2944 // `?.`, `??.`, `*.`, `.&`, `.@`, `===`, `!==`, `<=>`,
2945 // `=~`, `==~`, `..<`, `<..`, `<..<`, `as`, `?[`) plus
2946 // 6 ambient Java-shaped operators the fixture also
2947 // uses (`def`, `=`, `,`, `{}`, `()`, `return`), for a
2948 // total of 22 distinct operator kinds. A regression
2949 // that drops any one of the 16 #247 operators would
2950 // push the count below 22 and fail this assertion. The
2951 // complementary AST walk below pins each #247
2952 // operator's identity individually so a grammar change
2953 // that adds an unrelated operator (lifting
2954 // `u_operators` to 23) still flags the loss of a #247
2955 // operator at the per-token level.
2956 //
2957 // Was 23 until #1314. The extra entry was a `/` — the
2958 // fixture's two slashy literals (`/pat/`, `/^pat\$/`)
2959 // each spelled their closing delimiter with the
2960 // division kind, and the arm now guards them. The
2961 // enumeration above was wrong in two ways at that
2962 // count: it listed an ambient `[`, which this fixture
2963 // never emits (`list?[0]` is the single `?[` token),
2964 // and omitted the fabricated `/` that made up the
2965 // difference. Both are corrected here.
2966 assert_eq!(
2967 metric.halstead.unique_operators(),
2968 22,
2969 "u_operators changed; check whether a #247 operator was dropped or an unrelated operator added (and update the comment / token list above accordingly)",
2970 );
2971 },
2972 );
2973 }
2974
2975 #[cfg(feature = "groovy")]
2976 #[test]
2977 fn groovy_gstring_no_double_count() {
2978 // Issue #454: before the fix Groovy had no interpolation guard
2979 // at all — `StringLiteral` was classified as a plain operand, so
2980 // a GString counted the wrapping literal AND descended into its
2981 // interpolated expression, double-counting the inner identifier
2982 // in N2. The fix routes `StringLiteral` through
2983 // `string_operand_type` with both GString interpolation child
2984 // kinds (`gstring_brace_interpolation` / `gstring_dollar_-
2985 // interpolation`), so the wrapper is Unknown and only the inner
2986 // expression contributes.
2987 //
2988 // `def greet(name) {\n return "Hi ${name}"\n}\n`
2989 // operands by token text: `greet` × 1, `name` × 2 (param +
2990 // inside `${name}`). The wrapping `"Hi ${name}"` is suppressed
2991 // → u_operands = 2 (`greet`, `name`), N2 = 3. Without the fix
2992 // the wrapping literal would also count → u_operands = 3,
2993 // N2 = 4.
2994 let src = "def greet(name) {\n return \"Hi ${name}\"\n}\n";
2995 check_metrics::<GroovyParser>(src, "foo.groovy", |metric| {
2996 assert_eq!(metric.halstead.unique_operands(), 2);
2997 assert_eq!(metric.halstead.total_operands(), 3);
2998 });
2999 assert_ops_operands::<GroovyParser>(src, "foo.groovy", 2, vec!["greet", "name"]);
3000 }
3001
3002 #[cfg(feature = "groovy")]
3003 #[test]
3004 fn groovy_gstring_dollar_form_no_double_count() {
3005 // Issue #454: the short `$name` GString form emits a distinct
3006 // `gstring_dollar_interpolation` child whose inner `identifier`
3007 // text is `$name` (the grammar's identifier node spans the
3008 // leading `$`). The wrapper is suppressed; the inner `$name`
3009 // operand is distinct from the bare `name` param.
3010 //
3011 // `def greet(name) {\n return "Hi $name"\n}\n`
3012 // operands: `greet`, `name` (param), `$name` (interp) →
3013 // u_operands = 3, N2 = 3. Without the fix the wrapping
3014 // `"Hi $name"` would also count → u_operands = 4, N2 = 4.
3015 let src = "def greet(name) {\n return \"Hi $name\"\n}\n";
3016 check_metrics::<GroovyParser>(src, "foo.groovy", |metric| {
3017 assert_eq!(metric.halstead.unique_operands(), 3);
3018 assert_eq!(metric.halstead.total_operands(), 3);
3019 });
3020 assert_ops_operands::<GroovyParser>(src, "foo.groovy", 3, vec!["greet", "name", "$name"]);
3021 }
3022
3023 #[cfg(feature = "groovy")]
3024 #[test]
3025 fn groovy_plain_string_still_operand() {
3026 // Counterpart to `groovy_gstring_no_double_count`: a plain
3027 // non-interpolated literal has neither GString interpolation
3028 // child and must still contribute exactly one operand.
3029 //
3030 // `def f() {\n return "plain"\n}\n`
3031 // operands: `f`, `"plain"` → u_operands = 2, N2 = 2.
3032 let src = "def f() {\n return \"plain\"\n}\n";
3033 check_metrics::<GroovyParser>(src, "foo.groovy", |metric| {
3034 assert_eq!(metric.halstead.unique_operands(), 2);
3035 assert_eq!(metric.halstead.total_operands(), 2);
3036 });
3037 assert_ops_operands::<GroovyParser>(src, "foo.groovy", 2, vec!["f", "\"plain\""]);
3038 }
3039
3040 #[cfg(feature = "groovy")]
3041 #[test]
3042 fn groovy_slashy_string_delimiter_is_not_an_operator() {
3043 // Regression: issue #1314, the Groovy sibling of Elixir #1256
3044 // and Ruby/Perl #1312. A slashy string is a `StringLiteral`
3045 // whose closing delimiter is a `SLASH` — the kind id real
3046 // division uses — so `def b = /xyz/` reported a `/` operator
3047 // with no division in the source. Only the closer is a child
3048 // (the grammar folds the opening `/` into the literal's span),
3049 // so this fabricated one `/` per literal rather than Ruby's two.
3050 //
3051 // expected: operators `def` × 3, `=` × 3 → n1 = 2, N1 = 6.
3052 // Operands `b`, `/xyz/` × 2, `c`, `s` → n2 = 4, N2 = 6. Before
3053 // the guard the two closers added `/` → n1 = 3, N1 = 8.
3054 check_metrics::<GroovyParser>(
3055 "def b = /xyz/\ndef c = /xyz/\ndef s = b\n",
3056 "foo.groovy",
3057 |metric| {
3058 assert_eq!(metric.halstead.unique_operators(), 2);
3059 assert_eq!(metric.halstead.total_operators(), 6);
3060 assert_eq!(metric.halstead.unique_operands(), 4);
3061 assert_eq!(metric.halstead.total_operands(), 6);
3062 },
3063 );
3064 }
3065
3066 #[cfg(feature = "groovy")]
3067 #[test]
3068 fn groovy_division_survives_the_slashy_guard() {
3069 // Control for #1314: the guard is scoped to a `StringLiteral`
3070 // parent, so real division must still count. Both sides are in
3071 // one fixture — two divisions and one slashy literal — so a
3072 // guard widened to every `SLASH` fails here rather than
3073 // silently passing the test above.
3074 //
3075 // expected: operators `def` × 2, `=` × 2, `/` × 2 → n1 = 3,
3076 // N1 = 6. Operands `q`, `a`, `b`, `c`, `r`, `/x/` → n2 = N2 = 6.
3077 check_metrics::<GroovyParser>("def q = a / b / c\ndef r = /x/\n", "foo.groovy", |metric| {
3078 assert_eq!(metric.halstead.unique_operators(), 3);
3079 assert_eq!(metric.halstead.total_operators(), 6);
3080 assert_eq!(metric.halstead.unique_operands(), 6);
3081 assert_eq!(metric.halstead.total_operands(), 6);
3082 });
3083 }
3084
3085 #[cfg(feature = "groovy")]
3086 #[test]
3087 fn groovy_slashy_guard_is_parent_scoped_not_ancestor_scoped() {
3088 // The input that separates the parent-scoped guard from the
3089 // ancestor-scanning mutant of it — the mutant #1256's
3090 // post-mortem says survives every ordinary fixture. Groovy is
3091 // one of only two languages in #1314 where such an input
3092 // exists at all: a slashy string may carry a GString
3093 // interpolation, so `/x${a / b}y/` puts a real division under a
3094 // `StringLiteral` *ancestor* while its parent is the
3095 // `binary_expression`. An ancestor scan swallows it; the parent
3096 // check leaves it alone. (The JS, C++ and Tcl/iRules guards
3097 // have no such input — see
3098 // `js_regex_delimiter_guard_is_parent_scoped_is_unobservable`.)
3099 //
3100 // expected: operators `def` × 2, `=` × 2, `/` (the
3101 // interpolated division) → n1 = 3, N1 = 5. The wrapping literal
3102 // is not an operand — it carries an interpolation, so
3103 // `string_operand_type` yields `Unknown` and the inner
3104 // expression's operands carry the count (#454) — leaving `r`,
3105 // `a`, `b`, `s` → n2 = 4, with `a` twice → N2 = 5. Under the
3106 // ancestor-scoped mutant the division vanishes: n1 = 2, N1 = 4.
3107 check_metrics::<GroovyParser>(
3108 "def r = /x${a / b}y/\ndef s = a\n",
3109 "foo.groovy",
3110 |metric| {
3111 assert_eq!(metric.halstead.unique_operators(), 3);
3112 assert_eq!(metric.halstead.total_operators(), 5);
3113 assert_eq!(metric.halstead.unique_operands(), 4);
3114 assert_eq!(metric.halstead.total_operands(), 5);
3115 },
3116 );
3117 }
3118
3119 #[cfg(feature = "groovy")]
3120 #[test]
3121 fn groovy_every_string_spelling_scores_alike() {
3122 // Companion to the three above (#1314). Groovy has five ways to
3123 // write an inert one-character string, and the choice is
3124 // spelling rather than semantics, so all five must score
3125 // identically. Before the guard the two slashy forms reported
3126 // an extra `/` operator that the other three did not — the
3127 // author's delimiter choice moved n1/N1.
3128 //
3129 // The dollar-slashy and quoted forms are no-change controls:
3130 // `$/…/$` closes with `/$` (kind 144) and `"…"` with `"` (134),
3131 // neither of which the operator arm classifies. `'x'` is a
3132 // childless leaf. The escaped-slash row is the one that would
3133 // regress if the guard were ever narrowed to a literal whose
3134 // *only* child is the closer.
3135 //
3136 // expected per spelling: operators `def` × 3, `=` × 3 → n1 = 2,
3137 // N1 = 6; operands `a`, the literal, `b`, `c` → n2 = 4, with
3138 // `a` used three times → N2 = 6.
3139 for literal in ["/x/", "$/x/$", "'x'", "\"x\"", r"/esc\/aped/"] {
3140 assert_halstead_counts::<GroovyParser>(
3141 &format!("def a = {literal}\ndef b = a\ndef c = a\n"),
3142 "foo.groovy",
3143 [2, 6, 4, 6],
3144 &format!("literal {literal}"),
3145 );
3146 }
3147 }
3148
3149 #[cfg(feature = "csharp")]
3150 #[test]
3151 fn csharp_operators_and_operands() {
3152 // After issue #286, `void`, `string`, and `int` count as three
3153 // distinct Halstead operators rather than collapsing into one
3154 // `PredefinedType` kind_id entry, lifting u_operators from 13
3155 // to 15. Total operators (N1) is unchanged because the same
3156 // nodes are still counted, just keyed by lexeme.
3157 check_metrics::<CsharpParser>(
3158 "public class Main {
3159 public static void Run(string[] args) {
3160 int a, b, c, avg;
3161 a = 5; b = 5; c = 5;
3162 avg = (a + b + c) / 3;
3163 System.Console.WriteLine(\"{0}\", avg);
3164 }
3165 }",
3166 "foo.cs",
3167 |metric| {
3168 assert_eq!(metric.halstead.unique_operators(), 15);
3169 assert_eq!(metric.halstead.total_operators(), 32);
3170 assert_eq!(metric.halstead.unique_operands(), 13);
3171 assert_eq!(metric.halstead.total_operands(), 23);
3172 // Pin every Halstead field; values are whatever the
3173 // classifier produces and become the regression spec.
3174 insta::assert_json_snapshot!(metric.halstead);
3175 },
3176 );
3177 }
3178
3179 // Issue #1263: C#'s three name *containers* — `qualified_name`
3180 // (`System.Text`), `generic_name` (`List<int>`) and
3181 // `alias_qualified_name` (`global::Foo`) — were operands alongside
3182 // every leaf the walker already reached, so one occurrence of each
3183 // billed twice. `member_access_expression` never was, which is why
3184 // `csharp_operators_and_operands`' `System.Console.WriteLine` is
3185 // unaffected by this change: the bug lived in the *name* grammar,
3186 // not in member access.
3187 //
3188 // Three fixtures rather than one, so a regression names which
3189 // container came back. Each is hand-tallied; the removed composite
3190 // is called out per case.
3191 #[cfg(feature = "csharp")]
3192 #[test]
3193 fn csharp_name_containers_count_leaves_not_the_composite_1263() {
3194 // expected: operators `using`, `.`, `;` (3/3); operands
3195 // `System`, `Text` (2/2). Pre-fix the `qualified_name` added
3196 // `System.Text`, making the operand counts 3/3.
3197 check_metrics::<CsharpParser>("using System.Text;", "foo.cs", |metric| {
3198 assert_eq!(metric.halstead.unique_operators(), 3);
3199 assert_eq!(metric.halstead.total_operators(), 3);
3200 assert_eq!(metric.halstead.unique_operands(), 2);
3201 assert_eq!(metric.halstead.total_operands(), 2);
3202 });
3203
3204 // expected: operators `class`, `{`×2, `void`, `(`, `;`, `<`,
3205 // `>`, `int` — 9 total, 8 unique (`int` is the text-keyed
3206 // primitive operator, per #286). Operands `C`, `M`, `List`, `l`
3207 // — 4/4. Pre-fix the `generic_name` added `List<int>`, making
3208 // them 5/5.
3209 check_metrics::<CsharpParser>(
3210 "class C { void M() { List<int> l; } }",
3211 "foo.cs",
3212 |metric| {
3213 assert_eq!(metric.halstead.unique_operators(), 8);
3214 assert_eq!(metric.halstead.total_operators(), 9);
3215 assert_eq!(metric.halstead.unique_operands(), 4);
3216 assert_eq!(metric.halstead.total_operands(), 4);
3217 },
3218 );
3219
3220 // expected: operators `class`, `{`×2, `void`, `(`, `;`, `=`,
3221 // `::`, `.` — 9 total, 8 unique. `var` has no operator arm.
3222 // Operands `C`, `M`, `x`, `global`, `Foo`, `Bar` — 6/6. Pre-fix
3223 // the `alias_qualified_name` added `global::Foo`, making them
3224 // 7/7. The `::` staying an operator is what makes the leaf-only
3225 // tally lossless here, so it is asserted by the operator count
3226 // rather than assumed.
3227 check_metrics::<CsharpParser>(
3228 "class C { void M() { var x = global::Foo.Bar; } }",
3229 "foo.cs",
3230 |metric| {
3231 assert_eq!(metric.halstead.unique_operators(), 8);
3232 assert_eq!(metric.halstead.total_operators(), 9);
3233 assert_eq!(metric.halstead.unique_operands(), 6);
3234 assert_eq!(metric.halstead.total_operands(), 6);
3235 },
3236 );
3237 }
3238
3239 #[cfg(feature = "csharp")]
3240 #[test]
3241 fn csharp_primitive_types_and_booleans() {
3242 // After issue #286: each of `byte`, `short`, `int`, `long`,
3243 // `char`, `float`, `double`, `bool`, `object` is now a distinct
3244 // Halstead operator (9 primitives) rather than collapsing into
3245 // one `PredefinedType` kind_id entry. u_operators rises from 6
3246 // to 14 (5 non-primitive operators + 9 distinct primitives);
3247 // total operators (N1) is unchanged because the same nodes are
3248 // still counted, just keyed by lexeme.
3249 //
3250 // N2 dropped 23 → 21 with issue #1253: `true` and `false` each
3251 // reached the walker twice — once as `boolean_literal`, once as
3252 // the keyword leaf under it — so each added one spurious
3253 // occurrence. n2 is unchanged at 21 because operands are keyed
3254 // by source text, so the duplicate collapsed into the existing
3255 // vocabulary entry; that is exactly why the inflation was
3256 // invisible in n2. Every operand here is distinct, so
3257 // N2 == n2 == 21 after the fix.
3258 check_metrics::<CsharpParser>(
3259 "public class Prims {
3260 byte a = 1;
3261 short b = 2;
3262 int c = 3;
3263 long d = 4;
3264 char e = 'x';
3265 float f = 1.0f;
3266 double g = 2.0;
3267 bool h = true;
3268 bool i = false;
3269 object j = null;
3270 }",
3271 "foo.cs",
3272 |metric| {
3273 assert_eq!(metric.halstead.unique_operators(), 14);
3274 assert_eq!(metric.halstead.total_operators(), 33);
3275 assert_eq!(metric.halstead.unique_operands(), 21);
3276 assert_eq!(metric.halstead.total_operands(), 21);
3277 insta::assert_json_snapshot!(metric.halstead);
3278 },
3279 );
3280 }
3281
3282 #[cfg(feature = "csharp")]
3283 #[test]
3284 fn csharp_boolean_literal_counts_once() {
3285 // Regression: issue #1253. `boolean_literal: choice('true',
3286 // 'false')` wraps the keyword leaf, and both kinds sat in the
3287 // operand arm, so every `true` / `false` occurrence added +1 to
3288 // N2. Operands are keyed by source text, so the duplicate
3289 // collapsed into the same vocabulary entry and n2 stayed
3290 // correct — which is why nothing caught it.
3291 //
3292 // Source repeats `true` so N2 exceeds n2 and the assertions can
3293 // tell "counted once per occurrence" from "deduplicated into
3294 // the vocabulary".
3295 //
3296 // Operands by text key: `A`, `M`, `a`, `b`, `c`, `d`, `true` × 2,
3297 // `false`, `null` ⇒ n2 = 9, N2 = 10. Before the fix the keyword
3298 // leaves added one occurrence per boolean ⇒ N2 = 13.
3299 check_metrics::<CsharpParser>(
3300 "class A {\n void M() {\n bool a = true;\n bool b = false;\n bool c = true;\n object d = null;\n }\n}\n",
3301 "foo.cs",
3302 |metric| {
3303 assert_eq!(metric.halstead.unique_operands(), 9);
3304 assert_eq!(metric.halstead.total_operands(), 10);
3305 },
3306 );
3307 }
3308
3309 #[cfg(feature = "csharp")]
3310 #[test]
3311 fn csharp_boolean_keyword_outside_a_literal_still_counts() {
3312 // Companion to the test above (#1253): the suppression fires on
3313 // the *parent* kind, never on `True` / `False` alone. C#'s
3314 // overloadable-operator list emits a bare `true` / `false` token
3315 // with no `boolean_literal` wrapper — `operator_declaration` is
3316 // the grammar's only such position — so a blanket exclusion
3317 // would drop the operand that is the sole difference between
3318 // `operator true` and `operator false`, leaving two such
3319 // declarations with identical Halstead vocabularies whenever
3320 // their bodies match.
3321 //
3322 // Each declaration names one boolean and returns the other, so
3323 // the fixture exercises both the guarded and the unguarded
3324 // position for each keyword.
3325 //
3326 // Operands: `A` × 3 (class name, two parameter types), `a` × 2,
3327 // `true` × 2 (operator name + literal), `false` × 2 (likewise)
3328 // ⇒ n2 = 4, N2 = 9. A blanket exclusion gives N2 = 7; no guard
3329 // at all restores the double count at N2 = 11.
3330 check_metrics::<CsharpParser>(
3331 "class A {\n public static bool operator true(A a) => false;\n public static bool operator false(A a) => true;\n}\n",
3332 "foo.cs",
3333 |metric| {
3334 assert_eq!(metric.halstead.unique_operands(), 4);
3335 assert_eq!(metric.halstead.total_operands(), 9);
3336 },
3337 );
3338 }
3339
3340 #[cfg(feature = "csharp")]
3341 #[test]
3342 fn csharp_predefined_types_keyed_by_lexeme() {
3343 // Regression: issue #286. The C# grammar emits one `PredefinedType`
3344 // kind_id for every keyword type (`int`, `string`, `bool`, …).
3345 // Without keying by source text the entire family collapses into
3346 // a single Halstead operator (n1 += 1) instead of one per distinct
3347 // keyword. This test pins the post-fix behaviour using four
3348 // distinct primitives — `int`, `string`, `bool`, `object` —
3349 // appearing as parameter types so no other operators interact
3350 // with the count.
3351 //
3352 // expected: operators are `class`, `void`, `M`, `{}`, `()`, `,`
3353 // (×3 between 4 params), plus the four distinct predefined types
3354 // → u_operators = 5 + 4 = 9. Without the fix the four primitives
3355 // collapse to one entry, giving u_operators = 6.
3356 check_metrics::<CsharpParser>(
3357 "class C { void M(int a, string b, bool c, object d) {} }",
3358 "foo.cs",
3359 |metric| {
3360 // The headline assertion: four distinct primitive
3361 // keywords contribute four distinct operators, not one.
3362 assert_eq!(metric.halstead.unique_operators(), 9);
3363 },
3364 );
3365 }
3366
3367 #[cfg(feature = "csharp")]
3368 #[test]
3369 fn csharp_interpolated_string_no_double_count() {
3370 // Regression: issue #183. A C# `$"Hi {name}!"` used to be
3371 // classified as a Halstead operand (the wrapping
3372 // `InterpolatedStringExpression`) AND have its inner
3373 // `Interpolation`'s identifier classified as an operand too.
3374 // The fix routes `InterpolatedStringExpression` through a
3375 // conditional: when it has an `Interpolation` child, the inner
3376 // identifier already carries the operand contribution and the
3377 // wrapper is treated as `Unknown`; when it does not (static
3378 // `$"hello"`), the wrapper still counts as one operand.
3379 //
3380 // expected: operand contributions for
3381 // `class C { void M(string name) { string s = $"Hi {name}!"; } }`
3382 // — `C` (class), `M` (method), `name` (param), `s` (local),
3383 // and the inner `name` (inside `{...}`). With the fix,
3384 // u_operands = 4 (C, M, name, s); N2 = 5 (`name` twice).
3385 // Without the fix, the wrapping `$"Hi {name}!"` would also
3386 // count → u_operands = 5, N2 = 6.
3387 check_metrics::<CsharpParser>(
3388 "class C { void M(string name) { string s = $\"Hi {name}!\"; } }",
3389 "foo.cs",
3390 |metric| {
3391 assert_eq!(metric.halstead.unique_operands(), 4);
3392 assert_eq!(metric.halstead.total_operands(), 5);
3393 },
3394 );
3395 }
3396
3397 #[cfg(feature = "csharp")]
3398 #[test]
3399 fn csharp_static_interpolated_string_is_operand() {
3400 // Regression: issue #183. A `$"..."` with no `{...}` is
3401 // semantically identical to `"..."` and must still contribute
3402 // exactly one operand — the conditional `is_child(Interpolation)`
3403 // check distinguishes it from a true interpolation. expected:
3404 // operands are `C`, `M`, `s`, `$"hello"` → u_operands = 4, N2 = 4.
3405 // A naive "always Unknown" fix would yield u_operands = 3, N2 = 3,
3406 // diverging from the plain-string equivalent below.
3407 check_metrics::<CsharpParser>(
3408 "class C { void M() { string s = $\"hello\"; } }",
3409 "foo.cs",
3410 |metric| {
3411 assert_eq!(metric.halstead.unique_operands(), 4);
3412 assert_eq!(metric.halstead.total_operands(), 4);
3413 },
3414 );
3415 }
3416
3417 #[cfg(feature = "csharp")]
3418 #[test]
3419 fn csharp_plain_string_still_operand() {
3420 // The fix for #183 only changes how `InterpolatedStringExpression`
3421 // is classified; plain `StringLiteral` (and `VerbatimStringLiteral`
3422 // / `RawStringLiteral`) must still contribute exactly one operand
3423 // each. expected: operands are `C`, `M`, `s`, `"hi"` →
3424 // u_operands = 4, N2 = 4.
3425 check_metrics::<CsharpParser>(
3426 "class C { void M() { string s = \"hi\"; } }",
3427 "foo.cs",
3428 |metric| {
3429 assert_eq!(metric.halstead.unique_operands(), 4);
3430 assert_eq!(metric.halstead.total_operands(), 4);
3431 },
3432 );
3433 }
3434
3435 // C# half of #1380 — see `java_self_and_super_references_are_operands`
3436 // for the structural argument. `base` moves with `this`: both are
3437 // receivers of a member access.
3438 #[cfg(feature = "csharp")]
3439 #[test]
3440 fn csharp_self_and_base_references_are_operands() {
3441 let source = "class T {\n int F() { return this.x + base.y; }\n}";
3442 // Operators (n1=8, N1=10): class, {} x2, int, (), return, . x2, +, ;
3443 // Operands (n2=6, N2=6): T, F, this, x, base, y
3444 // Before the fix this read (10, 12, 4, 4).
3445 assert_halstead_counts::<CsharpParser>(
3446 source,
3447 "foo.cs",
3448 [8, 10, 6, 6],
3449 "csharp self/base references",
3450 );
3451 assert_keywords_are_operands_only::<CsharpParser>(source, "foo.cs", &["this", "base"]);
3452 }
3453
3454 // An `indexer_declaration` spells the member's *name* with the same
3455 // `this` token kind the receiver uses (both are kind 91), so a
3456 // blanket move would have billed a declarator keyword as a value.
3457 // The declaration keeps the operator classification that the
3458 // `operator` keyword of an overload declaration already has (#1380).
3459 //
3460 // The fixture carries both uses, which is what makes the gate
3461 // observable: `this` has to reach *both* vocabularies from one file.
3462 // Asserting only the operand side would pass with the gate deleted,
3463 // and only the operator side would pass with the whole #1380 change
3464 // reverted (grammar-dispatch section 11).
3465 #[cfg(feature = "csharp")]
3466 #[test]
3467 fn csharp_indexer_declaration_keyword_is_not_a_self_reference() {
3468 let source = "class C {\n int[] _a;\n \
3469 public int this[int i] { get { return this._a[i]; } }\n}";
3470
3471 // Asserted per space, not through the root. The root merges
3472 // every space's vocabulary, and the fixture has one declarator
3473 // `this` and one receiver `this`, so at the root `this` is a
3474 // member of *both* vocabularies whether the gate is right or
3475 // exactly backwards — and every Halstead count is bit-identical
3476 // under the swap, one occurrence on each side either way. The
3477 // space tree is where the two uses stay apart: the declarator
3478 // sits in the class's own vocabulary and the receiver in the
3479 // accessor's.
3480 let mut roles = Vec::new();
3481 collect_keyword_roles(
3482 &ops_of::<CsharpParser>(source, "foo.cs"),
3483 "this",
3484 &mut roles,
3485 );
3486
3487 // `Both` can only arise from two `this` nodes in one space that
3488 // the getter classifies differently — it is the gate's whole
3489 // effect, and it disappears the moment `this` gets one role.
3490 // `OperandOnly` is the accessor, the innermost space and the one
3491 // holding the receiver alone; it is what flips when the gate is
3492 // inverted rather than dropped. Correct gives
3493 // `[Both, Both, OperandOnly]`; inverting gives
3494 // `[Both, Both, OperatorOnly]`; dropping the gate gives three
3495 // `OperandOnly`; reverting #1380 gives three `OperatorOnly`.
3496 // Asserting the whole sequence rather than two `contains` calls:
3497 // the comment above already derives it, and a `contains` pair
3498 // leaves the length unasserted, so a space appearing or vanishing
3499 // goes unnoticed and `[Both, X, OperandOnly]` passes for any `X`.
3500 assert_eq!(
3501 roles,
3502 vec![Role::Both, Role::Both, Role::OperandOnly],
3503 "the declarator and the receiver must disagree in the two outer spaces \
3504 and the accessor must bill `this` as an operand alone",
3505 );
3506 }
3507
3508 /// One `this` and one `base` per container the pinned
3509 /// `tree-sitter-c-sharp` can put them in: a delegating constructor
3510 /// initializer, a base-constructor initializer, a member access, an
3511 /// element access, and a call argument. No `indexer_declaration` —
3512 /// that position is the gated one and is an operator.
3513 #[cfg(feature = "csharp")]
3514 const CSHARP_SELF_POSITIONS: &str = "class Pos : B {
3515 int[] _a;
3516 public Pos() : this(1) { }
3517 public Pos(int x) : base(x) { }
3518 int A() { return this._a[0]; }
3519 int C() { return base.H(); }
3520 int E() { return base[0]; }
3521 int F() { return this[0]; }
3522 void G() { M(this); }
3523 void M(object o) { }
3524 }";
3525
3526 #[cfg(feature = "csharp")]
3527 #[test]
3528 fn csharp_self_and_base_leaves_are_unaliased_and_unconditional() {
3529 assert_self_reference_leaves::<CsharpCode>(
3530 CSHARP_SELF_POSITIONS,
3531 &[
3532 ("this", Csharp::This as u16, 4),
3533 ("base", Csharp::Base as u16, 3),
3534 ],
3535 "csharp",
3536 );
3537 }
3538
3539 /// Every spelling of the six keywords `_parameter_type_with_modifiers`
3540 /// aliases to `modifier`, plus two controls that were billed before
3541 /// #1418: the bare `params` of a parameter array (kind 94, never
3542 /// aliased) and the bare `out` of a call argument (kind 56).
3543 ///
3544 /// `D` is declared *and* called so the argument `out` has a caller
3545 /// to sit in, which is what makes the two spellings of one keyword
3546 /// meet in one file.
3547 #[cfg(feature = "csharp")]
3548 const CSHARP_PARAMETER_MODIFIERS: &str = "static class E {
3549 static void A(this Foo f) { }
3550 static void B(scoped ref Foo f) { }
3551 static void C(ref readonly Foo f) { }
3552 static void D(out int x) { x = 1; }
3553 static void G(in Foo f) { }
3554 static void H(params int[] xs) { }
3555 static void I(ref int y) { D(out y); }
3556}";
3557
3558 /// Each of the five operator keywords in *both* of its kind
3559 /// spellings — the childless `modifier` of a parameter, and the
3560 /// keyword's own token elsewhere: `ref` as a ref local, ref
3561 /// initializer and ref argument (kind 28); `readonly` as the leaf
3562 /// under a field's `modifier` wrapper (48); `in` as the `foreach`
3563 /// separator and an `in` argument (55); `out` as an out argument
3564 /// (56); `scoped` as a scoped ref local (93).
3565 ///
3566 /// Separate from [`CSHARP_PARAMETER_MODIFIERS`] because that fixture
3567 /// pairs the two spellings for `out` alone, which left four of the
3568 /// five bare kinds `CsharpCode::is_primitive` lists with no input
3569 /// (#1418).
3570 #[cfg(feature = "csharp")]
3571 const CSHARP_MODIFIER_BOTH_SPELLINGS: &str = "class S {
3572 static readonly int q = 0;
3573 static void M(scoped ref int a, ref readonly int b, in int c, out int d) { d = q; }
3574 static void N(int[] xs) {
3575 ref int r = ref xs[0];
3576 scoped ref int s = ref xs[1];
3577 foreach (var e in xs) { }
3578 M(ref r, in s, in s, out r);
3579 }
3580}";
3581
3582 /// The six aliased parameter modifiers reach a Halstead half, and
3583 /// each reaches the same one its bare token reaches elsewhere
3584 /// (#1418).
3585 #[cfg(feature = "csharp")]
3586 #[test]
3587 fn csharp_parameter_modifiers_are_classified_1418() {
3588 // Operators (n1 = 15, N1 = 48): class 1; {} 8 (the class body
3589 // and seven method bodies); static 8 (the class and seven
3590 // methods); void 7; () 8 (seven parameter lists and the call);
3591 // int 3 (`out int x`, `int[]`, `ref int y`); [] 1; = 1; ; 2;
3592 // scoped 1; ref 3; readonly 1; out 2 (the parameter and the
3593 // argument); in 1; params 1.
3594 //
3595 // Operands (n2 = 15, N2 = 24): E 1; A/B/C/G/H/I 1 each; D 2
3596 // (declaration and call); this 1; Foo 4; f 4; x 2; 1 1; xs 1;
3597 // y 2.
3598 //
3599 // Before the fix this read (11, 41, 14, 23). The eight childless
3600 // `modifier` nodes were billed nowhere, which cost seven
3601 // operator occurrences and one operand occurrence, and emptied
3602 // `scoped`, `ref`, `readonly` and `in` out of the operator
3603 // vocabulary entirely — `out` survived only because the call
3604 // argument spells it with the unaliased kind.
3605 assert_halstead_counts::<CsharpParser>(
3606 CSHARP_PARAMETER_MODIFIERS,
3607 "foo.cs",
3608 [15, 48, 15, 24],
3609 "csharp parameter modifiers",
3610 );
3611
3612 // The receiver is an operand, matching every other C# `this`
3613 // (#1380). Both halves matter: an arm listing it twice would
3614 // still put it among the operands.
3615 assert_keywords_are_operands_only::<CsharpParser>(
3616 CSHARP_PARAMETER_MODIFIERS,
3617 "foo.cs",
3618 &["this"],
3619 );
3620
3621 let ops = ops_of::<CsharpParser>(CSHARP_PARAMETER_MODIFIERS, "foo.cs");
3622 for keyword in ["ref", "out", "in", "scoped", "readonly", "params"] {
3623 assert!(
3624 ops.operators.iter().any(|o| o == keyword),
3625 "`{keyword}` must be an operator; operators were {:?}",
3626 ops.operators
3627 );
3628 assert!(
3629 !ops.operands.iter().any(|o| o == keyword),
3630 "`{keyword}` must not be an operand; operands were {:?}",
3631 ops.operands
3632 );
3633 }
3634
3635 // The vocabulary is not deduplicated — it is the concatenation
3636 // of the kind-keyed and the lexeme-keyed operator maps — so a
3637 // keyword appearing twice is the #453 shape: one keyword split
3638 // across the two maps and counted twice in `n1`. Keeping both
3639 // spellings in one map is what `CsharpCode::is_primitive`
3640 // listing the five bare kinds buys, and this is the only
3641 // assertion that can see it.
3642 //
3643 // It needs a fixture carrying *both* spellings of each keyword,
3644 // which `CSHARP_PARAMETER_MODIFIERS` does for `out` alone: its
3645 // `ref`, `in`, `scoped` and `readonly` are all in parameter
3646 // position, so for those the count can only ever be one and the
3647 // assertion is dead. That left four of the five bare kinds with
3648 // no input at all — dropping `Csharp::Ref`, `In`, `Readonly` or
3649 // `Scoped` from `is_primitive` failed no test in the workspace.
3650 let both_code = CSHARP_MODIFIER_BOTH_SPELLINGS.as_bytes();
3651 let spellings = [
3652 ("ref", Csharp::Ref as u16),
3653 ("out", Csharp::Out as u16),
3654 ("in", Csharp::In as u16),
3655 ("scoped", Csharp::Scoped as u16),
3656 ("readonly", Csharp::Readonly as u16),
3657 ];
3658 let (mut bare, mut aliased) = ([0_usize; 5], [0_usize; 5]);
3659
3660 for_each_node_with_chain::<CsharpCode>(both_code, |node, _| {
3661 let text = &both_code[node.start_byte()..node.end_byte()];
3662 let Some(index) = spellings.iter().position(|(kw, _)| kw.as_bytes() == text) else {
3663 return;
3664 };
3665 if node.kind_id() == Csharp::Modifier as u16 && node.child_count() == 0 {
3666 aliased[index] += 1;
3667 } else if node.kind_id() == spellings[index].1 {
3668 bare[index] += 1;
3669 }
3670 });
3671
3672 // Without this the count below is vacuous for any keyword the
3673 // fixture stopped spelling both ways — the same decay that hid
3674 // the four unguarded kinds in the first place.
3675 for (index, (keyword, _)) in spellings.iter().enumerate() {
3676 assert!(
3677 bare[index] > 0 && aliased[index] > 0,
3678 "fixture must spell `{keyword}` both ways; it has {} bare and {} aliased",
3679 bare[index],
3680 aliased[index]
3681 );
3682 }
3683
3684 let both = ops_of::<CsharpParser>(CSHARP_MODIFIER_BOTH_SPELLINGS, "both.cs");
3685 for (keyword, _) in spellings {
3686 assert_eq!(
3687 both.operators.iter().filter(|o| *o == keyword).count(),
3688 1,
3689 "`{keyword}` must be one operator across both of its kind spellings; \
3690 operators were {:?}",
3691 both.operators
3692 );
3693 }
3694 }
3695
3696 /// Pins the grammar shape the #1418 arm gates on, in both
3697 /// directions.
3698 ///
3699 /// The arm reads `modifier` nodes with no children as aliased bare
3700 /// keywords and leaves the rest to their classified keyword leaf.
3701 /// That is a claim about two populations, and a grammar bump can
3702 /// break either: giving an alias a token child would double-count
3703 /// it, and flattening a declaration modifier to a childless node
3704 /// would bill `public` / `static` as an unknown keyword. Both are
3705 /// checked here, so neither can move quietly.
3706 ///
3707 /// Childlessness says "aliased", *not* "parameter modifier" —
3708 /// `_lambda_expression_init` and `anonymous_method_expression` alias
3709 /// a bare `static` / `async` onto the same kind, which the second
3710 /// walk below pins so the distinction cannot be forgotten. Their
3711 /// `Unknown` is today's behaviour rather than a contract: it is what
3712 /// they were before #1418, and billing them is a decision about
3713 /// lambdas. It is asserted only so making that decision has to come
3714 /// through this test.
3715 #[cfg(feature = "csharp")]
3716 #[test]
3717 fn csharp_aliased_modifier_is_childless() {
3718 let code = CSHARP_PARAMETER_MODIFIERS.as_bytes();
3719 let (mut aliased, mut declaration) = (0_usize, 0_usize);
3720
3721 for_each_node_with_chain::<CsharpCode>(code, |node, chain| {
3722 if node.kind_id() != Csharp::Modifier as u16 {
3723 return;
3724 }
3725 let role = CsharpCode::get_op_type_with_code(node, code, Ancestors::known(chain));
3726 let text = &code[node.start_byte()..node.end_byte()];
3727
3728 if node.child_count() == 0 {
3729 aliased += 1;
3730 // Spelled out rather than `this`-versus-everything-else,
3731 // because "everything else is an operator" is false of
3732 // the kind at large — a lambda's aliased `static` is
3733 // neither, and the second walk below owns it.
3734 let (correct, expected) = match text {
3735 b"this" => (matches!(role, TokenRole::Operand), "an operand"),
3736 b"scoped" | b"ref" | b"out" | b"in" | b"readonly" => {
3737 (matches!(role, TokenRole::Operator), "an operator")
3738 }
3739 other => panic!(
3740 "`{}` is not one of the six parameter spellings this fixture carries",
3741 String::from_utf8_lossy(other)
3742 ),
3743 };
3744 assert!(
3745 correct,
3746 "`{}` is an aliased parameter modifier and must be {expected}",
3747 String::from_utf8_lossy(text)
3748 );
3749 // Grammar-dispatch section 5: nothing containing the
3750 // token is classified, so billing the token cannot
3751 // count the same source text twice. Every ancestor,
3752 // not just `parameter` — `parameter_list` and the
3753 // declaration above it contain it too.
3754 for (depth, ancestor) in chain.iter().enumerate() {
3755 assert!(
3756 matches!(
3757 CsharpCode::get_op_type_with_code(
3758 ancestor,
3759 code,
3760 Ancestors::known(&chain[..depth])
3761 ),
3762 TokenRole::Unknown
3763 ),
3764 "`{}` contains an aliased modifier and is itself classified, so the \
3765 keyword now counts twice",
3766 ancestor.kind()
3767 );
3768 }
3769 } else {
3770 declaration += 1;
3771 assert_eq!(
3772 node.child_count(),
3773 1,
3774 "a declaration `modifier` wraps exactly one keyword leaf"
3775 );
3776 assert!(
3777 matches!(role, TokenRole::Unknown),
3778 "the `{}` wrapper must stay unclassified; its leaf carries the operator",
3779 String::from_utf8_lossy(text)
3780 );
3781 let leaf = node.child(0).expect("child_count is 1");
3782 let mut leaf_chain = chain.to_vec();
3783 leaf_chain.push(*node);
3784 assert!(
3785 matches!(
3786 CsharpCode::get_op_type_with_code(
3787 &leaf,
3788 code,
3789 Ancestors::known(&leaf_chain)
3790 ),
3791 TokenRole::Operator
3792 ),
3793 "the leaf under a declaration `modifier` must carry the operator"
3794 );
3795 }
3796 });
3797
3798 // Eight of each: `this`, `scoped`, `ref` x3, `readonly`, `out`,
3799 // `in` in parameter position, and the `static` of the class and
3800 // of its seven methods. Asserted so a fixture that stopped
3801 // carrying one population cannot pass having checked nothing.
3802 assert_eq!(aliased, 8, "fixture lost an aliased parameter modifier");
3803 assert_eq!(declaration, 8, "fixture lost a declaration modifier");
3804
3805 // The second alias site. `static (int x) => …` and `async
3806 // delegate (int y) { … }` put a bare keyword under the same
3807 // childless `modifier`, so `csharp_is_aliased_modifier` holds
3808 // for a node that is not in a parameter at all — which is why
3809 // the getter dispatches on the text and not on the kind.
3810 let lambda = "static class L {
3811 static void M() {
3812 var a = static (int x) => x + 1;
3813 var b = async delegate (int y) { return y; };
3814 }
3815}"
3816 .as_bytes();
3817 let mut lambda_aliases = 0_usize;
3818
3819 for_each_node_with_chain::<CsharpCode>(lambda, |node, chain| {
3820 if node.kind_id() != Csharp::Modifier as u16 || node.child_count() != 0 {
3821 return;
3822 }
3823 lambda_aliases += 1;
3824 let text = &lambda[node.start_byte()..node.end_byte()];
3825 assert!(
3826 matches!(text, b"static" | b"async"),
3827 "`{}` is not a lambda modifier the grammar aliases",
3828 String::from_utf8_lossy(text)
3829 );
3830 assert!(
3831 matches!(
3832 CsharpCode::get_op_type_with_code(node, lambda, Ancestors::known(chain)),
3833 TokenRole::Unknown
3834 ),
3835 "`{}` on a lambda is unbilled today, as it was before #1418; billing it is a \
3836 deliberate change and belongs in this assertion, not around it",
3837 String::from_utf8_lossy(text)
3838 );
3839 });
3840
3841 assert_eq!(
3842 lambda_aliases, 2,
3843 "fixture lost a lambda / anonymous-method modifier alias"
3844 );
3845 }
3846
3847 #[cfg(feature = "go")]
3848 #[test]
3849 fn go_operators_and_operands() {
3850 check_metrics::<GoParser>(
3851 "package main
3852 func sum(a, b int) int {
3853 return a + b
3854 }",
3855 "foo.go",
3856 |metric| {
3857 insta::assert_json_snapshot!(
3858 metric.halstead,
3859 @r#"
3860 {
3861 "unique_operators": 7,
3862 "total_operators": 7,
3863 "unique_operands": 5,
3864 "total_operands": 8,
3865 "length": 15,
3866 "estimated_program_length": 31.26112492884004,
3867 "purity_ratio": 2.0840749952560027,
3868 "vocabulary": 12,
3869 "volume": 53.77443751081734,
3870 "difficulty": 5.6,
3871 "level": 0.17857142857142858,
3872 "effort": 301.1368500605771,
3873 "time": 16.729825003365395,
3874 "bugs": 0.014975730436275946
3875 }
3876 "#
3877 );
3878 },
3879 );
3880 }
3881
3882 #[cfg(feature = "perl")]
3883 #[test]
3884 fn perl_operators_and_operands() {
3885 check_metrics::<PerlParser>(
3886 "sub sum {
3887 my ($a, $b) = @_;
3888 return $a + $b;
3889 }",
3890 "foo.pl",
3891 |metric| {
3892 insta::assert_json_snapshot!(
3893 metric.halstead,
3894 @r#"
3895 {
3896 "unique_operators": 10,
3897 "total_operators": 14,
3898 "unique_operands": 4,
3899 "total_operands": 6,
3900 "length": 20,
3901 "estimated_program_length": 41.219280948873624,
3902 "purity_ratio": 2.0609640474436812,
3903 "vocabulary": 14,
3904 "volume": 76.14709844115208,
3905 "difficulty": 7.5,
3906 "level": 0.13333333333333333,
3907 "effort": 571.1032383086406,
3908 "time": 31.727957683813365,
3909 "bugs": 0.02294502281013948
3910 }
3911 "#
3912 );
3913 },
3914 );
3915 }
3916
3917 #[cfg(feature = "perl")]
3918 #[test]
3919 fn perl_interpolated_string_no_double_count() {
3920 // Regression: issue #199. A `string_double_quoted` (and
3921 // `string_qq_quoted` / `backtick_quoted` / `command_qx_quoted`)
3922 // wrapping an `interpolation` child used to be counted as a
3923 // Halstead operand while the inner scalar/array/hash variable
3924 // was also walked and counted — double-counting the inner
3925 // variable's contribution to `N2`. Mirrors #180 (Bash/Elixir),
3926 // #183 (C#), #184 (PHP), #191 (Kotlin).
3927 //
3928 // expected: for
3929 // sub greet { my $name = shift; my $msg = "Hi $name"; return $msg; }
3930 // — operands are `greet`, `$name`, `shift`, `$msg`. With the
3931 // fix the wrapping `"Hi $name"` is skipped (has `Interpolation`
3932 // child), so u_operands = 4 and N2 = 6 (`$name` x2 from the
3933 // `my` binding and the interpolation; `$msg` x2 from the `my`
3934 // binding and `return`; `greet`, `shift` once each). Without
3935 // the fix the wrapping literal would also be counted, lifting
3936 // u_operands to 5 and N2 to 7.
3937 check_metrics::<PerlParser>(
3938 "sub greet { my $name = shift; my $msg = \"Hi $name\"; return $msg; }",
3939 "foo.pl",
3940 |metric| {
3941 assert_eq!(metric.halstead.unique_operands(), 4);
3942 assert_eq!(metric.halstead.total_operands(), 6);
3943 insta::assert_json_snapshot!(metric.halstead);
3944 },
3945 );
3946 }
3947
3948 #[cfg(feature = "perl")]
3949 #[test]
3950 fn perl_plain_string_still_operand() {
3951 // The fix for #199 only skips wrapping literals that carry an
3952 // `Interpolation` child; a plain `"hello"` (no `$…` inside)
3953 // must still contribute exactly one operand. expected: operands
3954 // `greet`, `$msg`, `"hello"` → u_operands = 3, N2 = 4 (`$msg`
3955 // appears in the `my` binding and the `return`).
3956 check_metrics::<PerlParser>(
3957 "sub greet { my $msg = \"hello\"; return $msg; }",
3958 "foo.pl",
3959 |metric| {
3960 assert_eq!(metric.halstead.unique_operands(), 3);
3961 assert_eq!(metric.halstead.total_operands(), 4);
3962 },
3963 );
3964 }
3965
3966 #[cfg(feature = "perl")]
3967 #[test]
3968 fn perl_single_quoted_string_never_interpolates() {
3969 // Single-quoted (`'…'`) and `q{…}` literals are not subject to
3970 // interpolation in Perl, so even when their text contains a
3971 // `$name`-shaped sequence the wrapper is still counted as one
3972 // operand and the inner text is not parsed as a variable.
3973 // expected: operands `greet`, `$msg`, `'Hi $name'` →
3974 // u_operands = 3, N2 = 4 (`$msg` x2).
3975 check_metrics::<PerlParser>(
3976 "sub greet { my $msg = 'Hi $name'; return $msg; }",
3977 "foo.pl",
3978 |metric| {
3979 assert_eq!(metric.halstead.unique_operands(), 3);
3980 assert_eq!(metric.halstead.total_operands(), 4);
3981 },
3982 );
3983 }
3984
3985 #[cfg(feature = "perl")]
3986 #[test]
3987 fn perl_plain_heredoc_counts_as_one_operand() {
3988 // Regression: issue #287. A plain (non-interpolating) Perl
3989 // heredoc body used to be classified `TokenRole::Unknown`,
3990 // so its visible `HeredocBodyStatement` node contributed
3991 // nothing to N2 even though it is a string literal. The fix
3992 // adds `HeredocBodyStatement` to the interpolation-aware
3993 // operand arm, so an inert heredoc counts as one operand.
3994 //
3995 // Source (heredoc body lives at the source_file level, not
3996 // inside any sub):
3997 // my $msg = <<END;
3998 // hello world
3999 // END
4000 //
4001 // Operands traversed:
4002 // * `$msg` (`scalar_variable`) × 1
4003 // * heredoc body (`heredoc_body_statement`) × 1
4004 // expected: u_operands = 2, N2 = 2.
4005 check_metrics::<PerlParser>("my $msg = <<END;\nhello world\nEND\n", "foo.pl", |metric| {
4006 assert_eq!(metric.halstead.unique_operands(), 2);
4007 assert_eq!(metric.halstead.total_operands(), 2);
4008 });
4009 }
4010
4011 #[cfg(feature = "perl")]
4012 #[test]
4013 fn perl_interpolated_heredoc_no_double_count() {
4014 // Regression: issue #287. An interpolating Perl heredoc
4015 // (`<<"TAG"` or bare `<<TAG`) carries an `Interpolation` child
4016 // when its body contains a `$var`. The wrapper must drop to
4017 // `Unknown` so the inner scalar variable carries the operand
4018 // count — same dispatch as the existing double-quoted /
4019 // backtick / qx wrappers (issue #199) and the PHP heredoc fix
4020 // (issue #184).
4021 //
4022 // Source:
4023 // my $name = "x";
4024 // my $msg = <<"END";
4025 // hi $name
4026 // END
4027 //
4028 // Operands by text key:
4029 // * `$name` × 2 (my-binding + interpolation inside heredoc)
4030 // * `"x"` × 1 (inert double-quoted string)
4031 // * `$msg` × 1
4032 // expected: u_operands = 3, N2 = 4. Without the
4033 // interpolation-aware drop the wrapping heredoc body would
4034 // also count, lifting u_operands to 4 and N2 to 5.
4035 check_metrics::<PerlParser>(
4036 "my $name = \"x\";\nmy $msg = <<\"END\";\nhi $name\nEND\n",
4037 "foo.pl",
4038 |metric| {
4039 assert_eq!(metric.halstead.unique_operands(), 3);
4040 assert_eq!(metric.halstead.total_operands(), 4);
4041 },
4042 );
4043 }
4044
4045 #[cfg(feature = "perl")]
4046 #[test]
4047 fn perl_bare_pattern_delimiters_are_not_operators() {
4048 // Regression: issue #1312, the Perl sibling of Elixir #1256.
4049 // The bare match form is the only one of Perl's regex literals
4050 // whose delimiters are spelled with an operator token kind —
4051 // `bca dump` shows `/abc/` emitting two `SLASH` under
4052 // `PatternMatcher` — so `$s =~ /abc/;` reported a `/` operator
4053 // with no division in the source.
4054 //
4055 // expected: operators `$` (the `scalar_variable` sigil), `=~`
4056 // and `;` → n1 = N1 = 3. Operands `$s` and the `/abc/` pattern
4057 // → n2 = N2 = 2. Before the guard the two delimiters added `/`
4058 // → n1 = 4, N1 = 5; the pattern operand arrived with #1314,
4059 // which promoted all three pattern spellings together (see
4060 // `perl_every_pattern_value_spelling_scores_alike`).
4061 check_metrics::<PerlParser>("$s =~ /abc/;\n", "foo.pl", |metric| {
4062 assert_eq!(metric.halstead.unique_operators(), 3);
4063 assert_eq!(metric.halstead.total_operators(), 3);
4064 assert_eq!(metric.halstead.unique_operands(), 2);
4065 assert_eq!(metric.halstead.total_operands(), 2);
4066 });
4067 }
4068
4069 #[cfg(feature = "perl")]
4070 #[test]
4071 fn perl_every_pattern_value_spelling_scores_alike() {
4072 // Companion to the test above (#1312, extended by #1314).
4073 // `m/abc/` is exactly `/abc/` in Perl and `qr/abc/` is the same
4074 // pattern as a value, so the three spellings of a pattern
4075 // *value* must score identically whatever delimiters they use.
4076 //
4077 // Until #1314 they scored alike at *zero*: no Perl pattern
4078 // wrapper was in the operand arm, so the literal never counted,
4079 // unlike Ruby's `Regex` and Elixir's `Sigil`. #1312 declined to
4080 // promote the bare form on its own precisely because that would
4081 // have scored `/abc/` at one operand and its synonyms at zero,
4082 // reintroducing the spelling sensitivity this test pins.
4083 // Promoting all three together closes the gap and keeps the
4084 // equality, which is what this test now asserts.
4085 //
4086 // `s///` and `tr///` are deliberately *not* rows here any more.
4087 // They are operations applied to a target rather than pattern
4088 // values, so #1314 made them operators; their own equality is
4089 // pinned by `perl_every_pattern_operation_spelling_scores_alike`
4090 // below. Splitting the one loop in two is the substantive
4091 // disagreement #1314 had with the reasoning recorded here: this
4092 // test governs *synonyms*, and `s///` is not a synonym of
4093 // `/abc/`.
4094 //
4095 // The fixture matches twice against one variable so that no two
4096 // of `[n1, N1, n2, N2]` are equal. A square tuple would leave
4097 // `assert_halstead_counts`' unique-vs-total axes unpinned —
4098 // transposing n1 with N1 inside the helper failed no test while
4099 // all three of its callers expected a square tuple.
4100 //
4101 // expected per variant: operators `$` × 2 (one per
4102 // `scalar_variable`), `=~` × 2, `and`, `;` → n1 = 4, N1 = 6.
4103 // The pattern contributes no *operator* — that is #1312's half
4104 // — and one operand, so with `$s` twice and the pattern twice
4105 // → n2 = 2, N2 = 4.
4106 for pattern in ["/abc/", "m/abc/", "m{abc}", "qr/abc/"] {
4107 assert_halstead_counts::<PerlParser>(
4108 &format!("$s =~ {pattern} and $s =~ {pattern};\n"),
4109 "foo.pl",
4110 [4, 6, 2, 4],
4111 &format!("pattern {pattern}"),
4112 );
4113 }
4114 }
4115
4116 #[cfg(feature = "perl")]
4117 #[test]
4118 fn perl_every_pattern_operation_spelling_scores_alike() {
4119 // The other half of the split (#1314). Substitution and
4120 // transliteration are operations applied to a target, so they
4121 // are operators — and like the value spellings, their delimiter
4122 // choice must not move the count. `y///` is a synonym of
4123 // `tr///` and shares `TransliterationTrOrY`, so the two fold to
4124 // one operator entry, which is why all four rows agree on n1.
4125 //
4126 // expected per variant: operators `$` × 2, `=~` × 2, `and`,
4127 // `;`, and the operation itself × 2 → n1 = 5, N1 = 8. The
4128 // pattern and replacement text is invisible to this grammar —
4129 // `substitution_pattern_s` emits only its keyword and
4130 // delimiters, no content node — so the sole operand is `$s`,
4131 // twice → n2 = 1, N2 = 2.
4132 for pattern in ["s/a/b/", "s{a}{b}", "tr/a/b/", "y/a/b/"] {
4133 assert_halstead_counts::<PerlParser>(
4134 &format!("$s =~ {pattern} and $s =~ {pattern};\n"),
4135 "foo.pl",
4136 [5, 8, 1, 2],
4137 &format!("pattern {pattern}"),
4138 );
4139 }
4140 }
4141
4142 #[cfg(feature = "perl")]
4143 #[test]
4144 fn perl_interpolated_pattern_operands_agree_but_operators_do_not() {
4145 // Two things at once (#1314), because they are the same
4146 // measurement: why the three pattern-value spellings route
4147 // through `string_operand_type` rather than a plain operand
4148 // arm, and what that routing does *not* fix.
4149 //
4150 // `m/$x/` and `qr/$x/` emit a real `Interpolation` wrapping a
4151 // `scalar_variable`, while the bare form keeps its `$x` inside
4152 // an unclassified `regex_pattern_content`. So:
4153 //
4154 // * Operands agree at n2 = N2 = 2 (`$s` plus one contribution
4155 // from the pattern) only because of the interpolation guard.
4156 // A plain operand arm would count the wrapper *and* the inner
4157 // `$x` for the suffixed forms — n2 = 3 — reintroducing
4158 // through the back door the divergence
4159 // `perl_every_pattern_value_spelling_scores_alike` exists to
4160 // prevent. Which node carries the one operand still differs
4161 // by spelling: the wrapper for the bare form, the inner `$x`
4162 // for the other two.
4163 // * Operators do *not* agree: the exposed `scalar_variable`
4164 // brings a `$` sigil, an operator here, that the bare form
4165 // has no node for. Over two matches N1 is 6 for `/$x/` and
4166 // 8 for the other two.
4167 //
4168 // The operator asymmetry is a grammar gap this classifier
4169 // cannot repair — there is nothing to classify in the bare
4170 // form — so it is pinned rather than papered over, the same
4171 // treatment `perl_division_emits_no_slash_token` gives the
4172 // missing division token. A bump that starts exposing the bare
4173 // form's interpolation turns this red, at which point the
4174 // expectations above need re-deriving.
4175 // Each fixture matches twice, so `N1 > n1` and `N2 > n2` and no
4176 // row is a square tuple that a transposition inside
4177 // `assert_halstead_counts` could pass (#1312).
4178 assert_halstead_counts::<PerlParser>(
4179 "$s =~ /$x/ and $s =~ /$x/;\n",
4180 "foo.pl",
4181 [4, 6, 2, 4],
4182 "bare /$x/",
4183 );
4184 for pattern in ["m/$x/", "qr/$x/"] {
4185 assert_halstead_counts::<PerlParser>(
4186 &format!("$s =~ {pattern} and $s =~ {pattern};\n"),
4187 "foo.pl",
4188 [4, 8, 2, 4],
4189 &format!("suffixed {pattern}"),
4190 );
4191 }
4192 }
4193
4194 #[cfg(feature = "perl")]
4195 #[test]
4196 fn perl_division_emits_no_slash_token() {
4197 // Drift marker, not an endorsement. Ruby's counterpart
4198 // (`ruby_division_survives_the_regex_guard`) proves #1312's
4199 // guard cannot swallow a real division; Perl has no such
4200 // fixture to write, because at the pinned grammar `$a / $b`
4201 // emits *no* `SLASH` token at all — `binary_expression`'s
4202 // children skip straight from one `scalar_variable` to the
4203 // other. Perl division therefore counts zero operators today,
4204 // a pre-existing grammar gap this fix neither causes nor
4205 // repairs.
4206 //
4207 // Pinning it keeps the gap in CI: a bump that starts emitting
4208 // the token turns this red, at which point the division would
4209 // begin counting (its parent is `BinaryExpression`, not
4210 // `PatternMatcher`, so the guard leaves it alone) and the
4211 // expectations above need re-deriving.
4212 //
4213 // The same gap is why no Perl test can distinguish the
4214 // parent-scoped guard from an ancestor-scoped one: with no
4215 // `SLASH` reachable below a `PatternMatcher`, that mutant is
4216 // unobservable here — measured, not assumed. Perl's guard is
4217 // parent-scoped for correctness by construction and for
4218 // symmetry with Ruby's, where the distinction *is* observable
4219 // and is pinned by
4220 // `ruby_regex_guard_is_parent_scoped_not_ancestor_scoped`.
4221 let path = PathBuf::from("foo.pl");
4222 let source = "my $z = $a / $b;\n";
4223 let parser = PerlParser::new(source.as_bytes().to_vec(), &path, None);
4224 assert!(
4225 !ast_has_kind_id(&parser, Perl::SLASH as u16),
4226 "tree-sitter-perl still emits no SLASH for `{source}`"
4227 );
4228 // Anchor the negative assertion to *this* fixture. Without it
4229 // the test stays green when `source` is edited to something
4230 // containing no division at all — measured: swapping in
4231 // `my $z = 1;` failed nothing.
4232 //
4233 // expected: operators `my`, `=`, `$` × 3 (one per
4234 // `scalar_variable`), `;` → n1 = 4, N1 = 6, with no `/` among
4235 // them. Operands `$z`, `$a`, `$b` → n2 = N2 = 3.
4236 check_metrics::<PerlParser>(source, "foo.pl", |metric| {
4237 assert_eq!(metric.halstead.unique_operators(), 4);
4238 assert_eq!(metric.halstead.total_operators(), 6);
4239 assert_eq!(metric.halstead.unique_operands(), 3);
4240 assert_eq!(metric.halstead.total_operands(), 3);
4241 });
4242 // Positive control: the same kind *is* reachable in this
4243 // grammar, so the assertion above is about division and not
4244 // about `Perl::SLASH` being enum-only dead weight.
4245 let matcher = PerlParser::new(b"$s =~ /abc/;\n".to_vec(), &path, None);
4246 assert!(
4247 ast_has_kind_id(&matcher, Perl::SLASH as u16),
4248 "Perl::SLASH must be the bare pattern delimiter kind"
4249 );
4250 }
4251
4252 #[cfg(feature = "perl")]
4253 #[test]
4254 fn perl_readline_closer_alias_never_reaches_kind_id() {
4255 // Drift marker for `Perl::GT2` (#1395). The grammar closes
4256 // `<FH>` / `<$fh>` with `token.immediate('>')`, which the
4257 // generated parser gives its own symbol — hence the `GT2`
4258 // variant sitting next to `FileHandle` in the enum — but whose
4259 // `public_symbol_map` entry collapses it onto `GT` before
4260 // `kind_id()`, exactly like `LPAREN2` in #768 and Ruby's
4261 // `SLASH2` in #1312. Nothing in `getter/perl.rs` names it, and
4262 // nothing should: `compute_halstead` keys a non-primitive
4263 // operator on `kind_id()`, so two live kinds both rendering
4264 // `">"` would split one `>` across two `n1` entries. If a bump
4265 // ever starts emitting it, the repair is to route `>` through
4266 // the lexeme-keyed map, not to list both — and this test is
4267 // what says so.
4268 //
4269 // The same collapse makes the `GT2` arm in `getter/cpp.rs` and
4270 // `getter/mozcpp.rs` a defensive arm rather than a live one:
4271 // tree-sitter-cpp maps its `>>`-closing `GT2` the same way, so
4272 // `vector<vector<int>>` reports two `GT`s.
4273 let path = PathBuf::from("foo.pl");
4274 for source in ["my $l = <FH>;\n", "my $l = <$fh>;\n", "my $b = $x > $y;\n"] {
4275 let parser = PerlParser::new(source.as_bytes().to_vec(), &path, None);
4276 assert!(
4277 !ast_has_kind_id(&parser, Perl::GT2 as u16),
4278 "Perl::GT2 must stay collapsed to Perl::GT for `{source}`"
4279 );
4280 // Positive control: the id the operator arm fires on is
4281 // present, so the assertion above cannot pass merely
4282 // because the fixture grew no `>` at all.
4283 assert!(
4284 ast_has_kind_id(&parser, Perl::GT as u16),
4285 "Perl::GT must be the `>` kind for `{source}`"
4286 );
4287 }
4288 }
4289
4290 #[cfg(feature = "perl")]
4291 #[test]
4292 fn perl_readline_brackets_are_operators() {
4293 // #1395: a readline's `<` / `>` delimit syntax, not a literal,
4294 // so under the policy on `Getter::get_op_type` they are
4295 // ordinary operators — even though ABC's `conditions` excludes
4296 // them (#1297). `<STDIN>` is the one spelling that cannot show
4297 // this: the grammar lexes it as a single `standard_input`
4298 // token, so there are no brackets to classify.
4299 //
4300 // expected: operators `sub`, `{}`, `my`, `$` × 2 (one per
4301 // `scalar_variable`), `=`, `<`, `>`, `;` × 2, `return` →
4302 // n1 = 9, N1 = 11. Operands `r`, `$l` × 2, `FH` →
4303 // n2 = 3, N2 = 4.
4304 check_metrics::<PerlParser>("sub r { my $l = <FH>; return $l; }\n", "foo.pl", |metric| {
4305 assert_eq!(metric.halstead.unique_operators(), 9);
4306 assert_eq!(metric.halstead.total_operators(), 11);
4307 assert_eq!(metric.halstead.unique_operands(), 3);
4308 assert_eq!(metric.halstead.total_operands(), 4);
4309 });
4310 }
4311
4312 /// Every (name wrapper, contained operand) pairing tree-sitter-perl's
4313 /// node-types.json admits, and the single source of truth for both
4314 /// halves of #1355's guard: its parent set is the distinct first
4315 /// components, the kinds it subsumes the distinct second ones.
4316 /// `perl_name_wrappers_bill_the_name_once_1355` witnesses every row
4317 /// and fails on an eleventh pairing.
4318 #[cfg(feature = "perl")]
4319 const PERL_NAME_WRAPPER_PAIRINGS: [(Perl, Perl); 10] = [
4320 (Perl::PackageName, Perl::Identifier),
4321 (Perl::PackageName, Perl::ScalarVariable),
4322 (Perl::PackageName, Perl::ArrayVariable),
4323 (Perl::PackageName, Perl::HashVariable),
4324 (Perl::PackageName, Perl::SpecialScalarVariable),
4325 (Perl::PackageName, Perl::Typeglob),
4326 (Perl::PackageName, Perl::PackageVariable),
4327 (Perl::PackageVariable, Perl::PackageName),
4328 (Perl::PackageVariable, Perl::ScalarVariable),
4329 (Perl::Typeglob, Perl::Identifier),
4330 ];
4331
4332 /// The anonymous tokens those wrappers also hold. `::` and `*` are
4333 /// operators (matched above the guard, so they keep that reading);
4334 /// `{` folds into the `{}` glyph and `}` has never been classified
4335 /// at all. Listing them is what lets
4336 /// `perl_name_wrappers_bill_the_name_once_1355` police *every*
4337 /// child rather than only the named ones — the operand arm carries
4338 /// token-shaped kinds too (`True`, `FILE`, `SUB`, …), and a bump
4339 /// that let one of those inside a wrapper would otherwise be
4340 /// silenced with nothing failing.
4341 #[cfg(feature = "perl")]
4342 const PERL_NAME_WRAPPER_TOKENS: [Perl; 4] =
4343 [Perl::COLONCOLON, Perl::STAR, Perl::LBRACE, Perl::RBRACE];
4344
4345 /// The occurrences #1355's guard suppresses in `source`, paired
4346 /// with the (wrapper kind, child kind) pairings they witness.
4347 ///
4348 /// Walks with `for_each_node_with_chain`, which maintains the
4349 /// ancestor chain exactly as `spaces::compute` does, so "parent"
4350 /// here means what `Ancestors::parent` means inside the guard
4351 /// rather than what a differently-built chain would say.
4352 #[cfg(feature = "perl")]
4353 fn perl_subsumed_operands(source: &str) -> (Vec<String>, HashSet<(u16, u16)>) {
4354 let wrappers: HashSet<u16> = PERL_NAME_WRAPPER_PAIRINGS
4355 .map(|(wrapper, _)| wrapper as u16)
4356 .into();
4357 let subsumed: HashSet<u16> = PERL_NAME_WRAPPER_PAIRINGS
4358 .map(|(_, child)| child as u16)
4359 .into();
4360 let tokens: HashSet<u16> = PERL_NAME_WRAPPER_TOKENS.map(|kind| kind as u16).into();
4361 let code = source.as_bytes();
4362 let mut hidden = Vec::new();
4363 let mut pairings = HashSet::new();
4364 for_each_node_with_chain::<PerlCode>(code, |node, chain| {
4365 let Some(parent) = chain.last() else { return };
4366 if !wrappers.contains(&parent.kind_id()) {
4367 return;
4368 }
4369 assert!(
4370 subsumed.contains(&node.kind_id()) || tokens.contains(&node.kind_id()),
4371 "`{source}`: a `{}` inside a `{}` is a child this guard was not \
4372 derived against; re-read node-types.json before trusting it",
4373 node.kind(),
4374 parent.kind(),
4375 );
4376 if subsumed.contains(&node.kind_id()) {
4377 pairings.insert((parent.kind_id(), node.kind_id()));
4378 hidden.push(
4379 node.utf8_text(code)
4380 .expect("fixture is valid UTF-8")
4381 .to_owned(),
4382 );
4383 }
4384 });
4385 (hidden, pairings)
4386 }
4387
4388 /// One row of `perl_name_wrappers_bill_the_name_once_1355`'s
4389 /// table: a fixture, what it must measure now, what it measured
4390 /// before #1355, and the operand text behind the counts.
4391 struct PerlNameWrapperCase {
4392 source: &'static str,
4393 /// `[n1, N1, n2, N2]` with the guard in place.
4394 counts: [u64; 4],
4395 /// `[n2, N2]` without it. Re-derived by the loop rather than
4396 /// trusted, so a stale row fails instead of misinforming.
4397 before: [u64; 2],
4398 operands: &'static [&'static str],
4399 }
4400
4401 /// Regression for #1355. `package_name`, `package_variable` and
4402 /// `typeglob` are operands spanning a whole name, so every
4403 /// operand-classified node *inside* one was billed a second time:
4404 /// `use strict;` scored `N2` 2 for one name, `our $Foo::count = 3;`
4405 /// n2 5 / N2 6 for two, and the vocabulary grew a bare `::` entry
4406 /// because a `package_variable`'s qualifier slot is itself a
4407 /// childless `package_name`.
4408 ///
4409 /// Each row's `before` column is what it measured without the
4410 /// guard, and the loop re-derives both halves from the current
4411 /// parse rather than trusting the column — the guard is the only
4412 /// difference between the two classifications, so the occurrences
4413 /// it removes are exactly the subsumed-kind children of a wrapper:
4414 ///
4415 /// - `N2` before minus `N2` after must equal how many of those
4416 /// there are. That identity *is* the defect: one spurious operand
4417 /// per contained name part.
4418 /// - `n2` before is the post-fix vocabulary unioned with their
4419 /// texts. It exceeds `n2` after wherever a part's spelling is not
4420 /// already an operand on its own (`Data`, `::`, `count`).
4421 ///
4422 /// The walk doubles as the grammar-dispatch §1 / §2 drift marker.
4423 /// It asserts that every *named* child of a wrapper is one of the
4424 /// eight subsumed kinds — which is what makes keying the arm on the
4425 /// parent alone safe — and that all ten pairings node-types.json
4426 /// admits are exercised here, so a bump that renumbers or re-parents
4427 /// one fails loudly instead of leaving the counts below measuring a
4428 /// construct the arm no longer reaches.
4429 ///
4430 /// Two mutants this does *not* catch, measured rather than assumed.
4431 /// Widening the guard from parent- to ancestor-scoped fails nothing,
4432 /// for the reason `perl_division_emits_no_slash_token` already
4433 /// records about the other guard in this getter: every operand-kinded
4434 /// descendant of a wrapper is also a direct child of one, and the
4435 /// intervening sigil tokens are matched by the operator arm above
4436 /// before the guard is reached. Parent-scoping stands on
4437 /// grammar-dispatch §5 and on symmetry with that guard, not on a
4438 /// test. What *is* pinned is the arm's position: moving it above the
4439 /// operator arm swallows `::`, `*` and the typeglob's opening brace,
4440 /// and the operator columns below fail.
4441 #[cfg(feature = "perl")]
4442 #[test]
4443 fn perl_name_wrappers_bill_the_name_once_1355() {
4444 let cases: [PerlNameWrapperCase; 12] = [
4445 // identifier under package_name, the single-segment form.
4446 PerlNameWrapperCase {
4447 source: "use strict;\n",
4448 counts: [2, 2, 1, 1],
4449 before: [1, 2],
4450 operands: &["strict"],
4451 },
4452 // …and the multi-segment one, twice over.
4453 PerlNameWrapperCase {
4454 source: "require Data::Dumper;\n",
4455 counts: [3, 3, 1, 1],
4456 before: [3, 3],
4457 operands: &["Data::Dumper"],
4458 },
4459 PerlNameWrapperCase {
4460 source: "package Foo::Bar;\n",
4461 counts: [3, 3, 1, 1],
4462 before: [3, 3],
4463 operands: &["Foo::Bar"],
4464 },
4465 // The `bar` of a qualified call is a *sibling* of the
4466 // `package_name`, not a child, so it still counts while the
4467 // `Foo` inside the wrapper does not. That is the row saying
4468 // the guard reads position and not kind: keying it on the
4469 // child kinds instead fails 14 tests here, this one among
4470 // them.
4471 PerlNameWrapperCase {
4472 source: "Foo::bar();\n",
4473 counts: [3, 3, 2, 2],
4474 before: [2, 3],
4475 operands: &["Foo", "bar"],
4476 },
4477 // identifier under typeglob, bare and brace-delimited.
4478 PerlNameWrapperCase {
4479 source: "my $g = *STDOUT;\n",
4480 counts: [5, 5, 2, 2],
4481 before: [3, 3],
4482 operands: &["$g", "*STDOUT"],
4483 },
4484 PerlNameWrapperCase {
4485 source: "my $t = *{Foo};\n",
4486 counts: [6, 6, 2, 2],
4487 before: [3, 3],
4488 operands: &["$t", "*{Foo}"],
4489 },
4490 // package_name and scalar_variable under package_variable,
4491 // and scalar_variable under package_name — the reported
4492 // fixture, where `$Foo` was billed twice and `::` once.
4493 PerlNameWrapperCase {
4494 source: "our $Foo::count = 3;\n",
4495 counts: [4, 4, 2, 2],
4496 before: [5, 6],
4497 operands: &["$Foo::count", "3"],
4498 },
4499 // array_variable / hash_variable / special_scalar_variable
4500 // under package_name: the same shape with the other sigils.
4501 PerlNameWrapperCase {
4502 source: "my @l = @Foo::list;\n",
4503 counts: [3, 3, 2, 2],
4504 before: [5, 6],
4505 operands: &["@l", "@Foo::list"],
4506 },
4507 PerlNameWrapperCase {
4508 source: "my %h = %Foo::hash;\n",
4509 counts: [3, 3, 2, 2],
4510 before: [5, 6],
4511 operands: &["%h", "%Foo::hash"],
4512 },
4513 PerlNameWrapperCase {
4514 source: "my $z = $_::x;\n",
4515 counts: [4, 5, 2, 2],
4516 before: [5, 6],
4517 operands: &["$z", "$_::x"],
4518 },
4519 // typeglob under package_name.
4520 PerlNameWrapperCase {
4521 source: "*Foo::glob = 1;\n",
4522 counts: [3, 3, 2, 2],
4523 before: [6, 7],
4524 operands: &["*Foo::glob", "1"],
4525 },
4526 // package_variable under package_name: the nesting that
4527 // makes qualifier depth unbounded. One variable reference
4528 // used to spell seven vocabulary entries.
4529 PerlNameWrapperCase {
4530 source: "my $x = $Foo::Bar::baz;\n",
4531 counts: [4, 5, 2, 2],
4532 before: [7, 10],
4533 operands: &["$x", "$Foo::Bar::baz"],
4534 },
4535 ];
4536
4537 let mut witnessed: HashSet<(u16, u16)> = HashSet::new();
4538 for PerlNameWrapperCase {
4539 source,
4540 counts,
4541 before: [n2_before, total_before],
4542 operands,
4543 } in cases
4544 {
4545 let (hidden, pairings) = perl_subsumed_operands(source);
4546 assert!(
4547 !hidden.is_empty(),
4548 "row {source:?} contains no name-wrapper child, so it witnesses nothing",
4549 );
4550 witnessed.extend(pairings);
4551
4552 // Phrased as an addition rather than a subtraction so a
4553 // future edit that inverts the two underflows nothing and
4554 // fails with the message below.
4555 assert_eq!(
4556 total_before,
4557 counts[3] + hidden.len() as u64,
4558 "row {source:?} must shed exactly one operand per contained \
4559 name part; recorded N2_before {total_before}, parts {hidden:?}",
4560 );
4561 let mut vocabulary: HashSet<&str> = operands.iter().copied().collect();
4562 vocabulary.extend(hidden.iter().map(String::as_str));
4563 assert_eq!(
4564 vocabulary.len() as u64,
4565 n2_before,
4566 "row {source:?}: n2 before the fix is the post-fix vocabulary \
4567 plus the contained name parts; got {vocabulary:?}",
4568 );
4569
4570 assert_halstead_counts::<PerlParser>(source, "foo.pl", counts, source);
4571 assert_ops_operands::<PerlParser>(source, "foo.pl", operands.len(), operands.to_vec());
4572 }
4573
4574 let mut got: Vec<(u16, u16)> = witnessed.into_iter().collect();
4575 got.sort_unstable();
4576 let mut expected_pairings: Vec<(u16, u16)> = PERL_NAME_WRAPPER_PAIRINGS
4577 .map(|(wrapper, child)| (wrapper as u16, child as u16))
4578 .into();
4579 expected_pairings.sort_unstable();
4580 assert_eq!(
4581 got, expected_pairings,
4582 "the table must exercise every (wrapper, child) pairing \
4583 node-types.json admits, and no other",
4584 );
4585 }
4586
4587 /// The over-suppression half of #1355 (grammar-dispatch §6 and §11).
4588 /// The guard is keyed on the parent, so the same kinds it silences
4589 /// inside a name wrapper have to keep counting everywhere else —
4590 /// otherwise "one operand per name" would have been bought by
4591 /// zeroing ordinary variables and calls.
4592 ///
4593 /// `module_name` rides along because the issue asserted it shares
4594 /// the `identifier` leaf and would be collateral damage. It does
4595 /// not: `use 'Foo.pm'` parses to a leaf holding only its two quote
4596 /// tokens, so it wraps nothing and is untouched either way.
4597 #[cfg(feature = "perl")]
4598 #[test]
4599 fn perl_qw_list_bills_one_operand_per_element() {
4600 // `qw(a b c)` was invisible to Halstead — neither the elements,
4601 // the wrapper nor the `qw` keyword had an arm — so it billed
4602 // nothing where its synonym `("a", "b", "c")` billed three
4603 // operands. Each `list_item` is now one operand and the
4604 // `word_list_qw` wrapper is gated on holding one, the #1353
4605 // Ruby `%w[]` rule: one operand per element, or one for the
4606 // empty literal.
4607 assert_ops_operands::<PerlParser>(
4608 "my @a = qw(a b c);\n",
4609 "foo.pl",
4610 4,
4611 vec!["@a", "a", "b", "c"],
4612 );
4613 assert_ops_operands::<PerlParser>("my @a = qw();\n", "foo.pl", 2, vec!["@a", "qw()"]);
4614 assert_ops_operands::<PerlParser>(
4615 "use POSIX qw(floor ceil);\n",
4616 "foo.pl",
4617 3,
4618 vec!["POSIX", "floor", "ceil"],
4619 );
4620 // expected: [n1, N1, n2, N2] = [3, 3, 4, 4] for every delimiter —
4621 // operators `my`, `=`, `;`; the `qw` keyword and its delimiters
4622 // are unclassified, as Ruby's `%w[` is, so the choice of
4623 // delimiter cannot move the score (#1312).
4624 for spelling in [
4625 "qw(a b c)",
4626 "qw/a b c/",
4627 "qw{a b c}",
4628 "qw[a b c]",
4629 "qw<a b c>",
4630 ] {
4631 assert_halstead_counts::<PerlParser>(
4632 &format!("my @a = {spelling};\n"),
4633 "foo.pl",
4634 [3, 3, 4, 4],
4635 spelling,
4636 );
4637 }
4638 // The synonym: the same four operands, plus the `()` and `,`
4639 // operators the list spelling carries.
4640 assert_halstead_counts::<PerlParser>(
4641 "my @a = (\"a\", \"b\", \"c\");\n",
4642 "foo.pl",
4643 [5, 6, 4, 4],
4644 "list literal",
4645 );
4646 }
4647
4648 #[cfg(feature = "perl")]
4649 #[test]
4650 fn perl_qualified_name_leaves_still_count_elsewhere_1355() {
4651 // expected: operators `my` × 4, `$` × 3 (one per `$`-sigilled
4652 // variable), `=` × 4, `;` × 5, `()` × 3 and the fat comma
4653 // → n1 = 6, N1 = 20. Operands are the four declared variables,
4654 // the three integers, the hash key, `$_` and the call target
4655 // → n2 = N2 = 10. Five of the eight kinds the guard silences
4656 // under a name wrapper appear among them — `scalar_variable`,
4657 // `array_variable`, `hash_variable`, `special_scalar_variable`
4658 // and `identifier` — and all five still count here.
4659 let bare = "my $x = 1;\nmy @a = (2);\nmy %h = (k => 3);\nmy $u = $_;\nfoo();\n";
4660 assert_halstead_counts::<PerlParser>(bare, "foo.pl", [6, 20, 10, 10], bare);
4661 assert_ops_operands::<PerlParser>(
4662 bare,
4663 "foo.pl",
4664 10,
4665 vec!["$x", "1", "@a", "2", "%h", "k", "3", "$u", "$_", "foo"],
4666 );
4667 let (hidden, _) = perl_subsumed_operands(bare);
4668 assert!(
4669 hidden.is_empty(),
4670 "no name wrapper appears here, so the guard must be inert; got {hidden:?}",
4671 );
4672
4673 // expected: operators `use`, `;` → n1 = N1 = 2; the quoted
4674 // module name is the sole operand → n2 = N2 = 1.
4675 let quoted = "use 'Some.pm';\n";
4676 assert_halstead_counts::<PerlParser>(quoted, "foo.pl", [2, 2, 1, 1], quoted);
4677 assert_ops_operands::<PerlParser>(quoted, "foo.pl", 1, vec!["'Some.pm'"]);
4678 assert!(
4679 ast_has_kind_id(
4680 &PerlParser::new(quoted.as_bytes().to_vec(), &PathBuf::from("foo.pl"), None),
4681 Perl::ModuleName as u16,
4682 ),
4683 "the quoted `use` form no longer parses to `module_name`, so this \
4684 row no longer says anything about it",
4685 );
4686 }
4687
4688 #[cfg(feature = "lua")]
4689 #[test]
4690 fn lua_operators_and_operands() {
4691 check_metrics::<LuaParser>(
4692 "local function add(a, b)
4693 local result = a + b
4694 if result > 0 then
4695 return result
4696 end
4697 return 0
4698end",
4699 "foo.lua",
4700 |metric| {
4701 // n1=11: local,function,(,,,=,+,if,>,then,return,end
4702 // (after #695 the `)` closer no longer counts — only the
4703 // folded `(` opener does; was n1=12).
4704 // n2=5: add,a,b,result,0
4705 insta::assert_json_snapshot!(metric.halstead, @r#"
4706 {
4707 "unique_operators": 11,
4708 "total_operators": 14,
4709 "unique_operands": 5,
4710 "total_operands": 10,
4711 "length": 24,
4712 "estimated_program_length": 49.66338827944708,
4713 "purity_ratio": 2.0693078449769615,
4714 "vocabulary": 16,
4715 "volume": 96.0,
4716 "difficulty": 11.0,
4717 "level": 0.09090909090909091,
4718 "effort": 1056.0,
4719 "time": 58.666666666666664,
4720 "bugs": 0.03456644293839657
4721 }
4722 "#);
4723 },
4724 );
4725 }
4726
4727 /// Regression for #695. Lua/Bash/Tcl/iRules/PHP/Ruby/Elixir used to
4728 /// classify the *closing* delimiter (`)`/`]`/`}`) as a separate
4729 /// operator, while the C-family majority folds each balanced pair to a
4730 /// single glyph via `get_operator_id_as_str` and counts only the
4731 /// opener. A balanced `(1)` therefore double-counted as `()` + `)`,
4732 /// inflating n1 and N1. With the fix only the folded `(` opener counts:
4733 /// `local x = (1)` yields operators `local`, `=`, `()` — n1 = N1 = 3,
4734 /// with no standalone `)`.
4735 #[cfg(feature = "lua")]
4736 #[test]
4737 fn lua_balanced_paren_counts_opener_only() {
4738 let source = "local x = (1)\n";
4739 let path = PathBuf::from("foo.lua");
4740 let parser = LuaParser::new(source.as_bytes().to_vec(), &path, None);
4741 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
4742 let paren = ops.operators.iter().filter(|o| o.as_str() == "()").count();
4743 assert_eq!(
4744 paren, 1,
4745 "balanced `(1)` must be one `()` operator; operators were {:?}",
4746 ops.operators
4747 );
4748 assert!(
4749 !ops.operators.iter().any(|o| o.as_str() == ")"),
4750 "the closing `)` must not be a separate operator; operators were {:?}",
4751 ops.operators
4752 );
4753 }
4754
4755 /// Guard for #768. Several `get_op_type` impls (Cpp/C/Objc/Mozcpp/
4756 /// Tcl/iRules/Php/Elixir/Ruby) classify a grammar's *second-alias*
4757 /// opener — `LPAREN2`, and for Elixir/Ruby `LBRACK2`/`LBRACK3` — as a
4758 /// Halstead operator alongside the base `LPAREN`/`LBRACK`. #768 worried
4759 /// that an alias opener would reach `compute_halstead` with a kind_id
4760 /// distinct from the base, inflating n1 (a second `()` entry) and
4761 /// rendering a bare `"("` instead of the folded `"()"`.
4762 ///
4763 /// That cannot happen: tree-sitter's runtime collapses each alias to
4764 /// its base via the grammar's `public_symbol_map` *before*
4765 /// `Node::kind_id()` (`ts_node_symbol`) ever returns. So the alias
4766 /// kind_id is unobservable to the metric layer and the alias match arms
4767 /// are defensive — they only fire if a future grammar bump drops that
4768 /// collapse. This test pins the invariant: parsing the exact
4769 /// constructs each grammar produces the alias for internally
4770 /// (pp-conditional `defined(...)` for Cpp; call arg-list / subscript /
4771 /// constant-array-pattern for Ruby) must yield **no** node carrying the
4772 /// alias kind_id, and the balanced opener must count once and render as
4773 /// the pair glyph. If a grammar bump makes an alias id observable, this
4774 /// goes red and signals that the alias arms must additionally fold to
4775 /// the base in `get_operator_id_as_str` (the fix #768 proposed).
4776 #[cfg(all(feature = "c", feature = "cpp", feature = "elixir", feature = "ruby"))]
4777 #[test]
4778 fn second_alias_opener_collapses_to_base_kind_id() {
4779 #[cfg(any(feature = "c", feature = "cpp", feature = "elixir", feature = "ruby"))]
4780 fn assert_no_alias<T: crate::ParserTrait>(
4781 source: &str,
4782 file: &str,
4783 alias_id: u16,
4784 alias_name: &str,
4785 ) {
4786 let path = PathBuf::from(file);
4787 let parser = T::new(source.as_bytes().to_vec(), &path, None);
4788 let mut stack = vec![parser.root()];
4789 while let Some(node) = stack.pop() {
4790 assert_ne!(
4791 node.kind_id(),
4792 alias_id,
4793 "{alias_name} (kind_id {alias_id}) must never reach kind_id() \
4794 for `{source}`; the runtime public_symbol_map should have \
4795 collapsed it to the base opener. If this fires after a \
4796 grammar bump, fold {alias_name} to its pair glyph in \
4797 get_operator_id_as_str (issue #768)."
4798 );
4799 for child in node.children() {
4800 stack.push(child);
4801 }
4802 }
4803 }
4804
4805 // Balanced openers must count once and render folded (no bare
4806 // `(`/`[`, no n1 inflation) — the property #768 feared was broken.
4807 #[cfg(any(feature = "c", feature = "cpp", feature = "elixir", feature = "ruby"))]
4808 fn assert_folded_openers<T: crate::MetricSuite>(source: &str, file: &str) {
4809 let path = PathBuf::from(file);
4810 let parser = T::new(source.as_bytes().to_vec(), &path, None);
4811 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
4812 assert!(
4813 !ops.operators.iter().any(|o| o.as_str() == "("),
4814 "no bare `(` operator (must fold to `()`); operators were {:?}",
4815 ops.operators
4816 );
4817 assert!(
4818 !ops.operators.iter().any(|o| o.as_str() == "["),
4819 "no bare `[` operator (must fold to `[]`); operators were {:?}",
4820 ops.operators
4821 );
4822 // Each pair glyph appears at most once — the alias does not add
4823 // a second `()`/`[]` entry to n1.
4824 assert!(
4825 ops.operators.iter().filter(|o| o.as_str() == "()").count() <= 1,
4826 "`()` must be a single n1 entry; operators were {:?}",
4827 ops.operators
4828 );
4829 assert!(
4830 ops.operators.iter().filter(|o| o.as_str() == "[]").count() <= 1,
4831 "`[]` must be a single n1 entry; operators were {:?}",
4832 ops.operators
4833 );
4834 }
4835
4836 // Cpp/C/Mozcpp: LPAREN2 = 20. The grammar emits it internally only
4837 // inside preprocessor-conditional expressions (`#if defined(FOO)`).
4838 assert_no_alias::<crate::CppParser>(
4839 "#if defined(FOO)\n#endif\n",
4840 "a.cpp",
4841 20,
4842 "Cpp::LPAREN2",
4843 );
4844 assert_no_alias::<crate::CParser>("#if defined(FOO)\n#endif\n", "a.c", 20, "C::LPAREN2");
4845
4846 // Ruby: LPAREN2 = 47 (call arg-list), LBRACK3 = 155 (element-
4847 // reference subscript), LBRACK2 = 46 (constant array pattern).
4848 assert_no_alias::<crate::RubyParser>("f(1)\n", "a.rb", 47, "Ruby::LPAREN2");
4849 assert_no_alias::<crate::RubyParser>("a[0]\n", "a.rb", 155, "Ruby::LBRACK3");
4850 assert_no_alias::<crate::RubyParser>(
4851 "case p\nin Point[1, 2] then 1\nend\n",
4852 "a.rb",
4853 46,
4854 "Ruby::LBRACK2",
4855 );
4856
4857 // Elixir: LPAREN2 = 95 (immediate call paren), LBRACK2 = 96
4858 // (access / subscript).
4859 assert_no_alias::<crate::ElixirParser>("f(1)\n", "a.ex", 95, "Elixir::LPAREN2");
4860 assert_no_alias::<crate::ElixirParser>("x[0]\n", "a.ex", 96, "Elixir::LBRACK2");
4861
4862 assert_folded_openers::<crate::CppParser>("int main(){ int a[3]; return a[0]; }", "b.cpp");
4863 assert_folded_openers::<crate::RubyParser>("f(1)\nb = [1]\nb[0]\n", "b.rb");
4864 }
4865
4866 #[cfg(feature = "kotlin")]
4867 #[test]
4868 fn kotlin_halstead_basic() {
4869 check_metrics::<KotlinParser>(
4870 "fun add(a: Int, b: Int): Int {
4871 val result = a + b
4872 return result
4873 }",
4874 "foo.kt",
4875 |metric| {
4876 insta::assert_json_snapshot!(
4877 metric.halstead,
4878 @r#"
4879 {
4880 "unique_operators": 9,
4881 "total_operators": 11,
4882 "unique_operands": 5,
4883 "total_operands": 10,
4884 "length": 21,
4885 "estimated_program_length": 40.13896548741762,
4886 "purity_ratio": 1.9113793089246487,
4887 "vocabulary": 14,
4888 "volume": 79.9544533632097,
4889 "difficulty": 9.0,
4890 "level": 0.1111111111111111,
4891 "effort": 719.5900802688873,
4892 "time": 39.97722668160485,
4893 "bugs": 0.026767153565498338
4894 }
4895 "#
4896 );
4897 },
4898 );
4899 }
4900
4901 #[cfg(feature = "kotlin")]
4902 #[test]
4903 fn kotlin_string_template_no_double_count() {
4904 // Re-anchored for issue #454. The pre-#454 comment claimed
4905 // kotlin-ng emits an `identifier` node for the short `$name`
4906 // form whose bytes include the leading `$`. That is factually
4907 // false: AST dump shows the short form produces bare
4908 // `string_content` tokens (`$`, then `name`) with **no**
4909 // structured node. The old assertion (u_operands = 4, N2 = 5)
4910 // passed for the wrong reason (lesson 6): the wrapping literal
4911 // was counted (+1) and the inner `name` was dropped (-1), and
4912 // the two errors cancelled. The `$name!` it used also defeats
4913 // recovery because the grammar glues the trailing `!` onto the
4914 // name token.
4915 //
4916 // Correct mechanism (clean end-of-segment short form):
4917 // `fun greet(name: String): String {\n return "Hi $name"\n}\n`
4918 // operators: fun, (, ), :, {}, return → as classified.
4919 // operands by token text:
4920 // `greet` × 1, `name` × 2 (param + recovered short-interp),
4921 // `String` × 2 (param type + return type).
4922 // The wrapping `"Hi $name"` literal is suppressed and the
4923 // inner `name` recovered → u_operands = 3 (`greet`, `name`,
4924 // `String`), N2 = 5. Pre-#454: wrapper counted, inner dropped
4925 // → u_operands = 4, N2 = 6.
4926 check_metrics::<KotlinParser>(
4927 "fun greet(name: String): String {\n return \"Hi $name\"\n}\n",
4928 "foo.kt",
4929 |metric| {
4930 assert_eq!(metric.halstead.unique_operands(), 3);
4931 assert_eq!(metric.halstead.total_operands(), 5);
4932 },
4933 );
4934 // Lesson 4: the ops store agrees on n2 and the exact operand set
4935 // (inner `name` present, wrapper absent).
4936 assert_ops_operands::<KotlinParser>(
4937 "fun greet(name: String): String {\n return \"Hi $name\"\n}\n",
4938 "foo.kt",
4939 3,
4940 vec!["greet", "name", "String"],
4941 );
4942 }
4943
4944 #[cfg(feature = "kotlin")]
4945 #[test]
4946 fn kotlin_short_interpolation_counts_inner_not_wrapper() {
4947 // Issue #454: the short `$name` template — distinct from the
4948 // long `${expr}` form, which the kotlin-ng grammar gives a
4949 // structured `interpolation` node (see
4950 // `kotlin_string_template_long_form_no_double_count`). The short
4951 // form has no such node; the variable arrives as a bare
4952 // `string_content` token preceded by a `$` `string_content`.
4953 // The fix recovers the clean-identifier variable as an operand
4954 // and suppresses the opaque wrapper.
4955 //
4956 // `fun f() { val x = 1; println("v=$x") }\n`
4957 // operands by token text: `f`, `x` × 2 (decl + recovered),
4958 // `println`, `1`. The wrapping `"v=$x"` is suppressed →
4959 // u_operands = 4 (`f`, `x`, `println`, `1`), N2 = 5.
4960 // Pre-#454 the wrapper `"v=$x"` counted and the inner `x` was
4961 // dropped → u_operands = 4 but the wrapper, not `x`, was the
4962 // fourth operand, and N2 = 5 with the wrong member — the ops
4963 // assertion below pins the exact set so the cancellation cannot
4964 // hide it.
4965 let src = "fun f() { val x = 1; println(\"v=$x\") }\n";
4966 check_metrics::<KotlinParser>(src, "foo.kt", |metric| {
4967 assert_eq!(metric.halstead.unique_operands(), 4);
4968 assert_eq!(metric.halstead.total_operands(), 5);
4969 });
4970 assert_ops_operands::<KotlinParser>(src, "foo.kt", 4, vec!["f", "x", "println", "1"]);
4971 }
4972
4973 #[cfg(feature = "kotlin")]
4974 #[test]
4975 fn kotlin_short_interpolation_space_separated() {
4976 // Issue #454 follow-up: tree-sitter-kotlin-ng splits the literal
4977 // only at each `$`, so a `$name` segment's name token absorbs any
4978 // trailing inter-segment text into its byte range. For `"$a $b"`
4979 // the token after the first `$` is `"a "` (with the trailing
4980 // space). Pre-fix `kotlin_is_identifier("a ")` returned false and
4981 // the leading variable `a` was silently dropped, yielding
4982 // operands `{b, f, s}` (verified: `a` missing) — breaking parity
4983 // with the long form `"${a} ${b}"`, which recovers `{a, b, f, s}`.
4984 //
4985 // The fix takes the maximal leading-identifier prefix of the name
4986 // token, recovering `a` and keying it as the bare `"a"` (not
4987 // `"a "`). Short and long forms must now agree exactly.
4988 //
4989 // `fun f() { val s = "$a $b" }\n`
4990 // operands by token text: `f`, `s`, `a` (recovered), `b`
4991 // (recovered). Wrapper suppressed → u_operands = 4, N2 = 4.
4992 let short = "fun f() { val s = \"$a $b\" }\n";
4993 let long = "fun f() { val s = \"${a} ${b}\" }\n";
4994 check_metrics::<KotlinParser>(short, "foo.kt", |metric| {
4995 assert_eq!(metric.halstead.unique_operands(), 4);
4996 assert_eq!(metric.halstead.total_operands(), 4);
4997 });
4998 // Both `a` and `b` present, wrapper absent, n2 == dedupe(operands).
4999 assert_ops_operands::<KotlinParser>(short, "foo.kt", 4, vec!["f", "s", "a", "b"]);
5000 // Exact parity with the long `${a} ${b}` form.
5001 assert_ops_operands::<KotlinParser>(long, "foo.kt", 4, vec!["f", "s", "a", "b"]);
5002
5003 // Comma after the name (`"$a, $b"`): the first name token is
5004 // `"a, "`; its leading identifier prefix is `a`.
5005 let comma = "fun f() { val s = \"$a, $b\" }\n";
5006 assert_ops_operands::<KotlinParser>(comma, "foo.kt", 4, vec!["f", "s", "a", "b"]);
5007
5008 // Name preceded by literal text and at end-of-segment (`"x=$a"`):
5009 // the `a` token has no trailing text, so recovery is unchanged.
5010 let prefixed = "fun f() { val s = \"x=$a\" }\n";
5011 assert_ops_operands::<KotlinParser>(prefixed, "foo.kt", 3, vec!["f", "s", "a"]);
5012
5013 // Mid-prose `"$x is "`: the name token is `"x is "`. The leading
5014 // identifier prefix is `x`, matching the long form `"${x} is "`,
5015 // which also recovers `x` and treats `" is "` as literal text.
5016 let prose_short = "fun f() { val s = \"$x is \" }\n";
5017 let prose_long = "fun f() { val s = \"${x} is \" }\n";
5018 assert_ops_operands::<KotlinParser>(prose_short, "foo.kt", 3, vec!["f", "s", "x"]);
5019 assert_ops_operands::<KotlinParser>(prose_long, "foo.kt", 3, vec!["f", "s", "x"]);
5020 }
5021
5022 #[cfg(feature = "kotlin")]
5023 #[test]
5024 fn kotlin_dollar_non_identifier_stays_literal() {
5025 // Issue #454 boundary: a `$` not followed by a clean identifier
5026 // is literal text, not an interpolation. `"price: $5"` (digit
5027 // after `$`) must keep the wrapping literal as a single operand
5028 // and recover nothing.
5029 //
5030 // `fun f() { val a = "price: $5" }\n`
5031 // operands: `f`, `a`, `"price: $5"` → u_operands = 3, N2 = 3.
5032 let src = "fun f() { val a = \"price: $5\" }\n";
5033 check_metrics::<KotlinParser>(src, "foo.kt", |metric| {
5034 assert_eq!(metric.halstead.unique_operands(), 3);
5035 assert_eq!(metric.halstead.total_operands(), 3);
5036 });
5037 assert_ops_operands::<KotlinParser>(src, "foo.kt", 3, vec!["f", "a", "\"price: $5\""]);
5038 }
5039
5040 #[cfg(feature = "kotlin")]
5041 #[test]
5042 fn kotlin_string_template_long_form_no_double_count() {
5043 // The `${expr}` long form of a Kotlin string template also
5044 // produces an `Interpolation` child. The fix must apply to it
5045 // identically.
5046 //
5047 // Source: `fun f(x: Int): String { return "v=${x}" }\n`
5048 // Operands by source-byte key:
5049 // `f` × 1, `x` × 2 (param + inside `${x}`),
5050 // `Int` × 1, `String` × 1.
5051 // With the fix u_operands = 4 (`f`, `x`, `Int`, `String`),
5052 // N2 = 5. Without the fix the wrapping `"v=${x}"` would also
5053 // count → u_operands = 5, N2 = 6.
5054 check_metrics::<KotlinParser>(
5055 "fun f(x: Int): String { return \"v=${x}\" }\n",
5056 "foo.kt",
5057 |metric| {
5058 assert_eq!(metric.halstead.unique_operands(), 4);
5059 assert_eq!(metric.halstead.total_operands(), 5);
5060 },
5061 );
5062 }
5063
5064 #[cfg(feature = "kotlin")]
5065 #[test]
5066 fn kotlin_plain_string_still_operand() {
5067 // The fix for #191 only skips wrapping templates that contain
5068 // an `Interpolation` child; a plain `"hello"` (no `$` interp)
5069 // must still contribute exactly one operand.
5070 //
5071 // Source: `fun f(): String { return "hello" }\n`
5072 // Operands: `f` × 1, `String` × 1, `"hello"` × 1 →
5073 // u_operands = 3, N2 = 3.
5074 check_metrics::<KotlinParser>(
5075 "fun f(): String { return \"hello\" }\n",
5076 "foo.kt",
5077 |metric| {
5078 assert_eq!(metric.halstead.unique_operands(), 3);
5079 assert_eq!(metric.halstead.total_operands(), 3);
5080 },
5081 );
5082 }
5083
5084 // Kotlin half of #1380 — see
5085 // `java_self_and_super_references_are_operands` for the structural
5086 // argument.
5087 #[cfg(feature = "kotlin")]
5088 #[test]
5089 fn kotlin_self_and_super_references_are_operands() {
5090 let source = "class T {\n fun f() = this.x + super.y\n}";
5091 // Operators (n1=7, N1=8): class, {}, fun, (), =, . x2, +
5092 // Operands (n2=6, N2=6): T, f, this, x, super, y
5093 // Before the fix this read (9, 10, 4, 4).
5094 assert_halstead_counts::<KotlinParser>(
5095 source,
5096 "foo.kt",
5097 [7, 8, 6, 6],
5098 "kotlin self/super references",
5099 );
5100 assert_keywords_are_operands_only::<KotlinParser>(source, "foo.kt", &["this", "super"]);
5101 }
5102
5103 // The label-qualified spellings are their own leaf kinds — `this@`
5104 // (106) and `super@` (107) — carrying no `this` / `super` leaf, so
5105 // listing only the two bare kinds would score every `this@Outer`
5106 // reference zero (grammar-dispatch section 1). The labels `Outer`
5107 // and `Inner` stay separate operands: the `this_expression` wrapper
5108 // whose span would have swallowed them is deliberately unclassified
5109 // (section 5).
5110 #[cfg(feature = "kotlin")]
5111 #[test]
5112 fn kotlin_labelled_self_and_super_references_are_operands() {
5113 let source = "class Outer {\n inner class Inner : A() {\n \
5114 fun f() = this@Outer.x + super@Inner.y\n }\n}";
5115 assert_keywords_are_operands_only::<KotlinParser>(source, "foo.kt", &["this@", "super@"]);
5116 assert_ops_operands::<KotlinParser>(
5117 source,
5118 "foo.kt",
5119 8,
5120 vec!["A", "Inner", "Outer", "f", "super@", "this@", "x", "y"],
5121 );
5122 // The metrics store is a second, independent walk — it keys
5123 // operands by `get_operand_id` where `ops_inner` keys by text —
5124 // so `assert_ops_operands` on its own pins the ops store against
5125 // a literal rather than against the metric (lesson 4).
5126 //
5127 // `N2` is what makes the "the labels stay separate operands"
5128 // claim above testable. Both labels duplicate an enclosing class
5129 // name, so dropping their contribution entirely leaves `n2` at 8
5130 // with the same eight strings; only `N2` falls, 10 to 8.
5131 check_metrics::<KotlinParser>(source, "foo.kt", |metric| {
5132 assert_eq!(metric.halstead.unique_operands(), 8);
5133 assert_eq!(metric.halstead.total_operands(), 10);
5134 });
5135 }
5136
5137 // The keeper question of grammar-dispatch section 6: a
5138 // `constructor_delegation_call` emits a bare `this` / `super` leaf
5139 // with no `this_expression` / `super_expression` wrapper around it,
5140 // so classifying the wrapper instead would score constructor
5141 // delegation zero. This is the only spelling that can tell the two
5142 // choices apart — every other `this` carries both nodes — so it is
5143 // the independent path grammar-dispatch section 11 asks for.
5144 #[cfg(feature = "kotlin")]
5145 #[test]
5146 fn kotlin_constructor_delegation_self_reference_is_an_operand() {
5147 let source = "class C(val n: Int) {\n constructor() : this(0)\n}";
5148 assert_keywords_are_operands_only::<KotlinParser>(source, "foo.kt", &["this"]);
5149 assert_ops_operands::<KotlinParser>(
5150 source,
5151 "foo.kt",
5152 5,
5153 vec!["0", "C", "Int", "n", "this"],
5154 );
5155 check_metrics::<KotlinParser>(source, "foo.kt", |metric| {
5156 assert_eq!(metric.halstead.unique_operands(), 5);
5157 assert_eq!(metric.halstead.total_operands(), 5);
5158 });
5159 }
5160
5161 /// One `this` / `super` per container the pinned
5162 /// `tree-sitter-kotlin-ng` can put them in: a bare expression, a
5163 /// call argument, a navigation receiver, a type-argument-qualified
5164 /// `super<P>`, both label-qualified forms, and the
5165 /// `constructor_delegation_call` that carries no wrapper.
5166 #[cfg(feature = "kotlin")]
5167 const KOTLIN_SELF_POSITIONS: &str = "class P {
5168 fun h() = 1
5169 }
5170 class Outer : P() {
5171 val x = 1
5172 fun c() = super.h()
5173 fun d() = this
5174 fun e(o: Any): Any = e(this)
5175 inner class Inner : P() {
5176 fun a() = this@Outer.x
5177 fun b() = this.hashCode()
5178 fun f() = super@Inner.h()
5179 fun g() = super<P>.h()
5180 }
5181 }
5182 class Del(val n: Int) {
5183 constructor() : this(0)
5184 }
5185";
5186
5187 #[cfg(feature = "kotlin")]
5188 #[test]
5189 fn kotlin_self_and_super_leaves_are_unaliased_and_unconditional() {
5190 assert_self_reference_leaves::<KotlinCode>(
5191 KOTLIN_SELF_POSITIONS,
5192 &[
5193 ("this", Kotlin::This as u16, 4),
5194 ("super", Kotlin::Super as u16, 2),
5195 ("this@", Kotlin::ThisAT as u16, 1),
5196 ("super@", Kotlin::SuperAT as u16, 1),
5197 ],
5198 "kotlin",
5199 );
5200 }
5201
5202 #[cfg(feature = "python")]
5203 #[test]
5204 fn python_fstring_no_double_count() {
5205 // Regression: issue #191. A Python f-string (`f"Hi {name}!"`)
5206 // wraps an `Interpolation` child whose inner identifier
5207 // `name` is walked and counted as its own operand. Without
5208 // the `is_child(Interpolation)` guard the wrapping `String`
5209 // would also count, double-counting `name`'s contribution to
5210 // `N2`. Same pattern as #180 (Bash/Elixir) and #184 (PHP).
5211 //
5212 // Source: `def greet(name):\n return f"Hi {name}!"\n`
5213 // Operands by source-byte key:
5214 // `greet` × 1, `name` × 2 (param + inside `{name}`).
5215 // With the fix the wrapping `f"Hi {name}!"` is skipped →
5216 // u_operands = 2 (`greet`, `name`), N2 = 3. Without the fix
5217 // the wrapping literal would also count → u_operands = 3,
5218 // N2 = 4.
5219 check_metrics::<PythonParser>(
5220 "def greet(name):\n return f\"Hi {name}!\"\n",
5221 "foo.py",
5222 |metric| {
5223 assert_eq!(metric.halstead.unique_operands(), 2);
5224 assert_eq!(metric.halstead.total_operands(), 3);
5225 },
5226 );
5227 }
5228
5229 #[cfg(feature = "python")]
5230 #[test]
5231 fn python_plain_string_still_operand() {
5232 // The fix for #191 only skips wrapping `String` nodes that
5233 // contain an `Interpolation` child; a plain `"hi"` must still
5234 // contribute exactly one operand.
5235 //
5236 // Source: `def f():\n return "hi"\n`
5237 // Operands: `f` × 1, `"hi"` × 1 → u_operands = 2, N2 = 2.
5238 // (The previous documentation-string filter is preserved:
5239 // a bare `"hi"` as a top-level `expression_statement` would
5240 // be skipped, but here it appears as `return "hi"`.)
5241 check_metrics::<PythonParser>("def f():\n return \"hi\"\n", "foo.py", |metric| {
5242 assert_eq!(metric.halstead.unique_operands(), 2);
5243 assert_eq!(metric.halstead.total_operands(), 2);
5244 });
5245 }
5246
5247 #[cfg(feature = "python")]
5248 #[test]
5249 fn python_concatenated_docstring_suppressed() {
5250 // Regression for #695. An implicit-concatenation docstring
5251 // (`"""doc""" "more"`) parses as `expression_statement >
5252 // concatenated_string > [string, string]`. The single-literal
5253 // docstring guard (`parent == expression_statement &&
5254 // child_count == 1`) never fired here, so each fragment counted
5255 // as a separate operand and the docstring's N2 contribution
5256 // depended on how many literals it was split into. With the fix,
5257 // every fragment of such a docstring is suppressed.
5258 //
5259 // Source: `def f():\n """doc""" "more"\n return 1\n`
5260 // Operands: `f`, `1` only — both docstring fragments suppressed →
5261 // u_operands = 2, N2 = 2.
5262 check_metrics::<PythonParser>(
5263 "def f():\n \"\"\"doc\"\"\" \"more\"\n return 1\n",
5264 "foo.py",
5265 |metric| {
5266 assert_eq!(metric.halstead.unique_operands(), 2);
5267 assert_eq!(metric.halstead.total_operands(), 2);
5268 },
5269 );
5270 }
5271
5272 #[cfg(feature = "python")]
5273 #[test]
5274 fn python_concatenated_non_docstring_still_counts() {
5275 // The #695 fix must only suppress concatenated literals in the
5276 // *docstring* position (sole child of an `expression_statement`).
5277 // A concatenated string used as a value (`x = "a" "b"`) is not a
5278 // docstring — its `concatenated_string` parent's grandparent is
5279 // an assignment, not a single-child statement — so both fragments
5280 // must still be operands.
5281 //
5282 // Source: `def f():\n x = "a" "b"\n return x\n`
5283 // Operands: `f`, `x` (twice: assign + return), `"a"`, `"b"` →
5284 // u_operands = 4, N2 = 5.
5285 check_metrics::<PythonParser>(
5286 "def f():\n x = \"a\" \"b\"\n return x\n",
5287 "foo.py",
5288 |metric| {
5289 assert_eq!(metric.halstead.unique_operands(), 4);
5290 assert_eq!(metric.halstead.total_operands(), 5);
5291 },
5292 );
5293 }
5294
5295 #[cfg(feature = "python")]
5296 #[test]
5297 fn python_empty_file_halstead() {
5298 check_metrics::<PythonParser>("", "empty.py", |metric| {
5299 let h = &metric.halstead;
5300 assert_eq!(h.unique_operators(), 0);
5301 assert_eq!(h.total_operands(), 0);
5302 assert_eq!(h.estimated_program_length(), 0.0);
5303 assert_eq!(h.purity_ratio(), 0.0);
5304 assert_eq!(h.volume(), 0.0);
5305 assert_eq!(h.difficulty(), 0.0);
5306 assert_eq!(h.level(), 0.0);
5307 assert_eq!(h.effort(), 0.0);
5308 assert_eq!(h.time(), 0.0);
5309 assert_eq!(h.bugs(), 0.0);
5310 });
5311 }
5312
5313 /// Regression #413, sub-fix (1): `await` was double-counted because the
5314 /// operator arm listed both the await-expression node (Await=237) and the
5315 /// nested `await` keyword token (Await2=95). Only the node should count,
5316 /// mirroring how `yield` counts only the Yield node.
5317 #[cfg(feature = "python")]
5318 #[test]
5319 fn python_await_counted_once_per_use() {
5320 check_metrics::<PythonParser>(
5321 "async def f():\n await a()\n await b()\n await c()\n",
5322 "foo.py",
5323 |metric| {
5324 // expected operators: async, def, await (3 unique)
5325 // await used three times -> N1 counts: async(1) def(1) await(3) = 5
5326 // Before #413, Await + Await2 both matched, so `await` was a
5327 // distinct operator twice: n1=4, N1=8.
5328 assert_eq!(metric.halstead.unique_operators(), 3);
5329 assert_eq!(metric.halstead.total_operators(), 5);
5330 },
5331 );
5332 }
5333
5334 /// Regression #413, sub-fix (3): `lambda` was dropped entirely. Only the
5335 /// `lambda` keyword token (Lambda3=73) is classified, not the wrapping
5336 /// Lambda/Lambda2 expression nodes, to avoid an await-style double count.
5337 #[cfg(feature = "python")]
5338 #[test]
5339 fn python_lambda_counted_once() {
5340 check_metrics::<PythonParser>("g = lambda x: x + 1\n", "foo.py", |metric| {
5341 // expected operators: =, lambda, + (3 unique, each used once)
5342 // Before #413, lambda was absent: only =, + were counted.
5343 assert_eq!(metric.halstead.unique_operators(), 3);
5344 assert_eq!(metric.halstead.total_operators(), 3);
5345 });
5346 }
5347
5348 /// Regression #413, sub-fix (2): `match` / `case` keyword tokens
5349 /// (Match=26, Case=27) were dropped. Each should now count as an operator,
5350 /// matching the cyclomatic metric which already counts every `case`.
5351 #[cfg(feature = "python")]
5352 #[test]
5353 fn python_match_case_counted() {
5354 check_metrics::<PythonParser>(
5355 "match x:\n case 1:\n pass\n case _:\n pass\n",
5356 "foo.py",
5357 |metric| {
5358 // expected operators: match, case, pass (3 unique)
5359 // match(1) + case(2) + pass(2) = 5 total occurrences.
5360 // Before #413, neither match nor case was counted (only pass).
5361 assert_eq!(metric.halstead.unique_operators(), 3);
5362 assert_eq!(metric.halstead.total_operators(), 5);
5363 },
5364 );
5365 }
5366
5367 /// Regression #413, sub-fix (2): `nonlocal` (Nonlocal=41) was dropped while
5368 /// `global` was already classified. Both should count, for parity.
5369 #[cfg(feature = "python")]
5370 #[test]
5371 fn python_nonlocal_and_global_counted() {
5372 check_metrics::<PythonParser>(
5373 "def f():\n global a\n nonlocal b\n",
5374 "foo.py",
5375 |metric| {
5376 // expected operators: def, global, nonlocal (3 unique)
5377 // Before #413, nonlocal was absent: only def, global counted.
5378 assert_eq!(metric.halstead.unique_operators(), 3);
5379 assert_eq!(metric.halstead.total_operators(), 3);
5380 },
5381 );
5382 }
5383
5384 /// Regression #413, sub-fix (4): `not in` (Notin=193) and `is not`
5385 /// (Isnot=194) are single compound operators. The parent-guard suppresses
5386 /// the inner Not/In/Is leaves only under those compounds, so standalone
5387 /// `not x`, `a in b`, `a is b`, and `for x in y` still count their leaves.
5388 #[cfg(feature = "python")]
5389 #[test]
5390 fn python_not_in_is_not_counted_as_single_operator() {
5391 check_metrics::<PythonParser>(
5392 "a not in b\na is not b\nnot c\nd in e\nf is g\nfor h in i:\n pass\n",
5393 "foo.py",
5394 |metric| {
5395 // expected operators (7 unique):
5396 // "not in" (compound, once), "is not" (compound, once),
5397 // "not" (standalone `not c`, once),
5398 // "in" (standalone `d in e` + `for h in i` = twice),
5399 // "is" (standalone `f is g`, once),
5400 // "for" (once), "pass" (once)
5401 // Total occurrences: 1+1+1+2+1+1+1 = 8.
5402 // Before #413, `a not in b` counted not+in (two) and
5403 // `a is not b` counted is+not (two); the compounds were
5404 // never classified.
5405 assert_eq!(metric.halstead.unique_operators(), 7);
5406 assert_eq!(metric.halstead.total_operators(), 8);
5407 },
5408 );
5409 }
5410
5411 #[cfg(feature = "bash")]
5412 #[test]
5413 fn bash_operators_and_operands() {
5414 check_metrics::<BashParser>(
5415 "#!/bin/bash
5416f() {
5417 local x=1
5418 if [ $x -eq 1 ]; then
5419 echo 'one'
5420 fi
5421}",
5422 "foo.sh",
5423 |metric| {
5424 // Operators (9 unique, 9 occurrences): the opening
5425 // delimiters `()`/`{}`/`[]` (each folded to one glyph and
5426 // counted once per balanced pair, #695 — the closers no
5427 // longer add a second operator), `local`, `=`, `if`,
5428 // `then`, `fi`, `;`.
5429 // Operands (6 unique, 7 occurrences): `f`, `x` (the
5430 // assignment LHS `variable_name`, kind 160), `1` (twice:
5431 // `=1` and `-eq 1`), `$x` (the `simple_expansion` — its
5432 // inner `variable_name` leaf is now suppressed so `$x`
5433 // counts once, #695), `echo`, `'one'`.
5434 // N2 was 8 before #1351: `echo` counted twice, once as the
5435 // `command_name` wrapper and once as the `word` it wraps.
5436 assert_eq!(metric.halstead.unique_operators(), 9);
5437 assert_eq!(metric.halstead.total_operators(), 9);
5438 assert_eq!(metric.halstead.unique_operands(), 6);
5439 assert_eq!(metric.halstead.total_operands(), 7);
5440 insta::assert_json_snapshot!(metric.halstead);
5441 },
5442 );
5443 }
5444
5445 #[cfg(feature = "bash")]
5446 #[test]
5447 fn bash_interpolated_string_no_double_count() {
5448 // Regression: issue #180. A double-quoted Bash string containing
5449 // `$name`, `${name[…]}`, or `$(cmd)` used to be classified as a
5450 // Halstead operand AND have its inner `simple_expansion` /
5451 // `expansion` / `command_substitution` children classified as
5452 // operands too. We now skip the wrapping literal when it has an
5453 // expansion child so only the inner expansion contributes.
5454 //
5455 // expected: operands across `a="plain"\nb="$x"\n` —
5456 // line 1: variable_name `a`, plain string `"plain"` (no
5457 // expansion, still operand) → 2.
5458 // line 2: variable_name `b`, wrapping `"$x"` skipped (has
5459 // expansion), `simple_expansion` `$x` (its inner
5460 // variable_name `x` leaf is suppressed under #695) → 2.
5461 // Total unique operands: 4 (`a`, `b`, `"plain"`, `$x`), each
5462 // appearing once → N2 = 4. Before #695 the inner `x` leaf of
5463 // `$x` was also counted (u_operands = 5, N2 = 5); before the
5464 // earlier #180 fix the wrapping `"$x"` literal was counted too.
5465 // The `=` is the only operator; appears twice (N1 = 2, n1 = 1).
5466 check_metrics::<BashParser>("a=\"plain\"\nb=\"$x\"\n", "foo.sh", |metric| {
5467 assert_eq!(metric.halstead.unique_operators(), 1);
5468 assert_eq!(metric.halstead.total_operators(), 2);
5469 assert_eq!(metric.halstead.unique_operands(), 4);
5470 assert_eq!(metric.halstead.total_operands(), 4);
5471 insta::assert_json_snapshot!(metric.halstead);
5472 });
5473 }
5474
5475 #[cfg(feature = "elixir")]
5476 #[test]
5477 fn elixir_interpolated_string_no_double_count() {
5478 // Regression: issue #180. Without the fix, an interpolated
5479 // Elixir `String` was classified as a single operand while its
5480 // inner `interpolation` identifier was also walked and
5481 // classified as its own operand — double-counting the
5482 // interpolated identifier's contribution to `N2`.
5483 //
5484 // expected: operand contributions for
5485 // `def greet(name) do\n msg = "Hi #{name}"\nend\n` —
5486 // `def`, `greet`, `name` (param), `msg`, and the inner `name`
5487 // (inside `#{...}`). With the fix, the wrapping
5488 // `"Hi #{name}"` literal is skipped (has `Interpolation`
5489 // child), so `name` is the only repeated operand:
5490 // u_operands = 4 (def, greet, name, msg), N2 = 5. Without the
5491 // fix, the wrapping literal would also count → u_operands = 5,
5492 // N2 = 6. Operators: `do`, `end`, `(`, `=` → u = N = 4.
5493 // Only the *opening* delimiters count after #695, so the `)`
5494 // and the `}` interpolation closer add no operator; #1314 then
5495 // dropped the `#{` opener too, on the rule that an
5496 // interpolation opener is spelling rather than an operation
5497 // (was 5 here, and 7 before #695).
5498 check_metrics::<ElixirParser>(
5499 "def greet(name) do\n msg = \"Hi #{name}\"\nend\n",
5500 "foo.ex",
5501 |metric| {
5502 assert_eq!(metric.halstead.unique_operators(), 4);
5503 assert_eq!(metric.halstead.total_operators(), 4);
5504 assert_eq!(metric.halstead.unique_operands(), 4);
5505 assert_eq!(metric.halstead.total_operands(), 5);
5506 insta::assert_json_snapshot!(metric.halstead);
5507 },
5508 );
5509 }
5510
5511 #[cfg(feature = "elixir")]
5512 #[test]
5513 fn elixir_plain_string_still_operand() {
5514 // The fix for #180 only skips wrapping literals that contain
5515 // interpolation; a plain `"hello"` must still contribute exactly
5516 // one operand. expected: `def`, `f`, `"hello"` → 3 unique
5517 // operands (n2 = 3), each appearing once (N2 = 3).
5518 check_metrics::<ElixirParser>("def f do\n \"hello\"\nend\n", "foo.ex", |metric| {
5519 assert_eq!(metric.halstead.unique_operands(), 3);
5520 assert_eq!(metric.halstead.total_operands(), 3);
5521 });
5522 }
5523
5524 #[cfg(feature = "elixir")]
5525 #[test]
5526 fn elixir_boolean_and_nil_literals_count_once() {
5527 // Regression: issue #1253. `boolean: choice("true", "false")`
5528 // and `nil: "nil"` each wrap a keyword leaf, and both the
5529 // wrapper and the leaf sat in the operand arm — so every
5530 // literal occurrence added +1 to N2. Operands are keyed by
5531 // source text, so the duplicate collapsed into the same
5532 // vocabulary entry and n2 stayed correct, which is why nothing
5533 // caught it.
5534 //
5535 // Source is the issue's reproducer plus a repeat of `true` and
5536 // `nil`, so N2 exceeds n2 and the assertions can tell "counted
5537 // once per occurrence" from "deduplicated into the vocabulary".
5538 // All three keywords appear, so restoring any one of `True`,
5539 // `False`, or `Nil2` to the operand arm trips this test.
5540 //
5541 // Operands by text key: `x`, `y`, `z`, `w`, `v`, `true` × 2,
5542 // `nil` × 2, `false` ⇒ n2 = 8, N2 = 10. Before the fix each of
5543 // the five literals counted twice ⇒ N2 = 15.
5544 //
5545 // This also guards the drift in the other direction. Elixir
5546 // classifies the wrapper and drops the leaf outright rather
5547 // than parent-guarding it, so a grammar bump that stopped
5548 // emitting `boolean` / `nil` would leave the leaves unclassified
5549 // and the literals would vanish from N2 entirely (⇒ 5 / 5)
5550 // rather than merely being miscounted.
5551 check_metrics::<ElixirParser>(
5552 "x = true\ny = nil\nz = false\nw = true\nv = nil\n",
5553 "foo.ex",
5554 |metric| {
5555 assert_eq!(metric.halstead.unique_operands(), 8);
5556 assert_eq!(metric.halstead.total_operands(), 10);
5557 },
5558 );
5559 }
5560
5561 #[cfg(feature = "elixir")]
5562 #[test]
5563 fn elixir_reserved_word_after_a_dot_stays_an_operand() {
5564 // Companion to the test above (#1253). Elixir drops `True` /
5565 // `False` / `Nil2` from the operand arm outright, which is only
5566 // safe because the one grammar position that accepts a reserved
5567 // word outside the `boolean` / `nil` wrapper — the right-hand
5568 // side of a remote dot — aliases it to `identifier`. This pins
5569 // that alias: if a grammar bump emitted the bare keyword there
5570 // instead, `Foo.nil` and `Foo.true` would silently stop
5571 // contributing an operand.
5572 //
5573 // Source: a = Foo.nil / b = Foo.true / c = nil
5574 //
5575 // Operands by text key: `a`, `Foo` × 2, `nil` × 2 (the aliased
5576 // identifier and the real literal, which share a text key),
5577 // `b`, `true`, `c` ⇒ n2 = 6, N2 = 8. Losing the alias drops the
5578 // two dotted references ⇒ N2 = 6.
5579 check_metrics::<ElixirParser>("a = Foo.nil\nb = Foo.true\nc = nil\n", "foo.ex", |metric| {
5580 assert_eq!(metric.halstead.unique_operands(), 6);
5581 assert_eq!(metric.halstead.total_operands(), 8);
5582 });
5583 }
5584
5585 #[cfg(feature = "elixir")]
5586 #[test]
5587 fn elixir_interpolated_sigil_no_double_count() {
5588 // Sigils mirror strings under #180. For `~r/foo#{name}/`, the
5589 // wrapping `Sigil` is skipped, but `SigilName` (`r`) and the
5590 // inner `name` identifier each contribute one operand.
5591 // expected: `def`, `f`, `name` (param), `re`, `r` (sigil name),
5592 // `name` (inside `#{...}`) → u_operands = 5, N2 = 6 (`name`
5593 // twice).
5594 check_metrics::<ElixirParser>(
5595 "def f(name) do\n re = ~r/foo#{name}/\nend\n",
5596 "foo.ex",
5597 |metric| {
5598 assert_eq!(metric.halstead.unique_operands(), 5);
5599 assert_eq!(metric.halstead.total_operands(), 6);
5600 },
5601 );
5602 }
5603
5604 #[cfg(feature = "elixir")]
5605 #[test]
5606 fn elixir_interpolated_charlist_no_double_count() {
5607 // Charlists mirror strings and sigils under #180. The
5608 // `E::String | E::Charlist | E::Sigil` arm in `get_op_type`
5609 // skips any wrapping literal that has an `Interpolation`
5610 // child; this test exercises the `Charlist` branch
5611 // specifically.
5612 //
5613 // expected: for `def f(name) do\n cl = 'Hi #{name}'\nend\n` —
5614 // `def`, `f`, `name` (param), `cl`, and the inner `name`
5615 // (inside `#{...}`). With the fix, the wrapping
5616 // `'Hi #{name}'` is skipped → u_operands = 4 (def, f, name,
5617 // cl), N2 = 5 (`name` twice).
5618 check_metrics::<ElixirParser>(
5619 "def f(name) do\n cl = 'Hi #{name}'\nend\n",
5620 "foo.ex",
5621 |metric| {
5622 assert_eq!(metric.halstead.unique_operands(), 4);
5623 assert_eq!(metric.halstead.total_operands(), 5);
5624 },
5625 );
5626 }
5627
5628 #[cfg(feature = "elixir")]
5629 #[test]
5630 fn elixir_sigil_delimiters_are_not_operators() {
5631 // Regression: issue #1256. Sigil delimiter tokens share their
5632 // kind ids with real operators (`SLASH`, `LPAREN`, `LBRACE`,
5633 // …) and were classified unconditionally, so `~r/abc/`
5634 // fabricated two division operators and the author's delimiter
5635 // choice moved n1/N1. The parent guard suppresses them under
5636 // `Sigil`; `~` stays the single per-sigil operator.
5637 //
5638 // expected: operators `=` × 3 and `~` × 3 → n1 = 2, N1 = 6.
5639 // Without the guard the delimiters added `/` × 2, `(`, `{` →
5640 // n1 = 5, N1 = 10. Operands: `a`, `~r/abc/i`, `r`, `i` (sigil
5641 // modifiers), `b`, `~w(one two)`, `w`, `c`, `~s{hi}`, `s` →
5642 // n2 = N2 = 10.
5643 check_metrics::<ElixirParser>(
5644 "a = ~r/abc/i\nb = ~w(one two)\nc = ~s{hi}\n",
5645 "foo.ex",
5646 |metric| {
5647 assert_eq!(metric.halstead.unique_operators(), 2);
5648 assert_eq!(metric.halstead.total_operators(), 6);
5649 assert_eq!(metric.halstead.unique_operands(), 10);
5650 assert_eq!(metric.halstead.total_operands(), 10);
5651 },
5652 );
5653 }
5654
5655 #[cfg(feature = "elixir")]
5656 #[test]
5657 fn elixir_sigil_delimiter_choice_is_invariant() {
5658 // Companion to the test above (#1256): two sigils differing
5659 // only in delimiter are the same literal, so every delimiter
5660 // choice must produce identical Halstead counts. `(` `[` `{`
5661 // `<` `/` `|` are the operator-kind delimiters the guard
5662 // covers; `"` and `'` never had an operator arm and pin the
5663 // already-correct path.
5664 //
5665 // expected per variant: operators `=`, `~` → n1 = 2, N1 = 2;
5666 // operands `x`, the sigil literal text, `w` (sigil name) →
5667 // n2 = 3, N2 = 3.
5668 for (open, close) in [
5669 ("(", ")"),
5670 ("[", "]"),
5671 ("{", "}"),
5672 ("<", ">"),
5673 ("/", "/"),
5674 ("|", "|"),
5675 ("\"", "\""),
5676 ("'", "'"),
5677 ] {
5678 assert_halstead_counts::<ElixirParser>(
5679 &format!("x = ~w{open}one two{close}\n"),
5680 "foo.ex",
5681 [2, 2, 3, 3],
5682 &format!("delimiter pair {open} {close}"),
5683 );
5684 }
5685 }
5686
5687 #[cfg(feature = "elixir")]
5688 #[test]
5689 fn elixir_standalone_operators_survive_the_sigil_guard() {
5690 // Control for #1256: the guard is parent-scoped, so the same
5691 // token kinds outside a sigil still count. Covers every guarded
5692 // kind standalone: `/` (division), `<` / `>` (comparison), `[`
5693 // and `|` (list cons), `(` (call), `{` (map literal, with its
5694 // `%`).
5695 //
5696 // expected: operators `=` × 6, `/`, `<`, `>`, `[`, `|`, `(`,
5697 // `%`, `{` → n1 = 9, N1 = 14. Operands: `x`, `a`, `b`, `y`,
5698 // `c`, `d`, `z`, `e`, `f`, `q`, `h`, `t`, `p`, `g`, `1`, `m`,
5699 // the `k:` keyword, `2` → n2 = N2 = 18.
5700 check_metrics::<ElixirParser>(
5701 "x = a / b\ny = c < d\nz = e > f\nq = [h | t]\np = g(1)\nm = %{k: 2}\n",
5702 "foo.ex",
5703 |metric| {
5704 assert_eq!(metric.halstead.unique_operators(), 9);
5705 assert_eq!(metric.halstead.total_operators(), 14);
5706 assert_eq!(metric.halstead.unique_operands(), 18);
5707 assert_eq!(metric.halstead.total_operands(), 18);
5708 },
5709 );
5710 }
5711
5712 #[cfg(feature = "elixir")]
5713 #[test]
5714 fn elixir_interpolated_sigil_keeps_inner_nodes_counting() {
5715 // Interpolation inside a sigil after #1256: the `{` delimiter
5716 // is suppressed (its parent is the `Sigil`), while the
5717 // `interpolation` child is a separate node whose inner
5718 // identifier must still count — the guard must not reach past
5719 // the delimiter tokens.
5720 //
5721 // expected: operators `=`, `~` → n1 = N1 = 2. Operands:
5722 // `v`, `s` (sigil name), `b` (interpolated identifier); the
5723 // wrapping sigil is skipped (`Interpolation` child, #180) and
5724 // `quoted_content` is unclassified → n2 = N2 = 3. The `#{`
5725 // marker was a third operator until #1314 dropped it.
5726 check_metrics::<ElixirParser>("v = ~s{a#{b} c}\n", "foo.ex", |metric| {
5727 assert_eq!(metric.halstead.unique_operators(), 2);
5728 assert_eq!(metric.halstead.total_operators(), 2);
5729 assert_eq!(metric.halstead.unique_operands(), 3);
5730 assert_eq!(metric.halstead.total_operands(), 3);
5731 });
5732
5733 // A guarded kind *inside* the interpolation: the `/` in
5734 // `#{a / b}` has `binary_operator` as its parent but the
5735 // `Sigil` as a further ancestor, so this input is the one
5736 // discriminator between the correct parent-scoped guard and a
5737 // wrong ancestor-scoped one that would swallow it.
5738 //
5739 // expected: operators `=`, `~`, `/` → n1 = N1 = 3 (the `#{`
5740 // opener stopped counting with #1314); operands `v`, `s`, `a`,
5741 // `b` → n2 = N2 = 4. The division is what this row is for, and
5742 // it still counts — the ancestor-scoped mutant drops it.
5743 check_metrics::<ElixirParser>("v = ~s{x #{a / b} y}\n", "foo.ex", |metric| {
5744 assert_eq!(metric.halstead.unique_operators(), 3);
5745 assert_eq!(metric.halstead.total_operators(), 3);
5746 assert_eq!(metric.halstead.unique_operands(), 4);
5747 assert_eq!(metric.halstead.total_operands(), 4);
5748 });
5749 }
5750
5751 #[cfg(feature = "bash")]
5752 #[test]
5753 fn bash_all_expansion_kinds_skip_wrapper() {
5754 // Exercises every node kind tested by
5755 // `bash_string_has_expansion`: `simple_expansion` (`$v`),
5756 // `expansion` (`${v[0]}`), `command_substitution` (`$(date)`),
5757 // and `arithmetic_expansion` (`$((1+2))`). A typo replacing
5758 // one kind with an aliased neighbour in `language_bash.rs`
5759 // (e.g., `ExpansionBody` instead of `Expansion`) would leave
5760 // the corresponding wrapping string counted as an operand and
5761 // shift the totals.
5762 //
5763 // expected: operands across the four lines —
5764 // line 1 `a="$v"`: var_name `a`, simple_expansion `$v` (its
5765 // inner var_name `v` leaf is suppressed under #695; wrapper
5766 // skipped) → 2
5767 // line 2 `b="${v[0]}"`: var_name `b`, var_name `v` (inside
5768 // subscript — parent is `expansion`, not `simple_expansion`,
5769 // so it still counts), number `0` (wrapper skipped,
5770 // `expansion` itself is not in the operand list) → 3
5771 // line 3 `c="$(date)"`: var_name `c`, the `word` `date` under
5772 // the `command_name` (wrapper skipped, `command_substitution`
5773 // not in operand list, and since #1351 the `command_name`
5774 // wrapper is not either) → 2
5775 // line 4 `d="$((1+2))"`: var_name `d`, numbers `1` and `2`
5776 // (wrapper skipped, `arithmetic_expansion` not in operand
5777 // list) → 3
5778 // Unique operands: a, b, c, d, $v, v, 0, date, 1, 2 → 10. Total
5779 // occurrences: 10 (`v` appears once — only line 2's subscript
5780 // leaf; line 1's `$v` inner leaf is suppressed — and `date` once,
5781 // as the `word`; before #1351 the `command_name` wrapping it
5782 // added a second `date` and N2 was 11). Operators after
5783 // #695: only the openers `[` (folded `[]`) and `+`, plus `=` four
5784 // times — the `}`/`)`/`))`/`]` closers no longer count.
5785 check_metrics::<BashParser>(
5786 "a=\"$v\"\nb=\"${v[0]}\"\nc=\"$(date)\"\nd=\"$((1+2))\"\n",
5787 "foo.sh",
5788 |metric| {
5789 assert_eq!(metric.halstead.unique_operators(), 3);
5790 assert_eq!(metric.halstead.total_operators(), 6);
5791 assert_eq!(metric.halstead.unique_operands(), 10);
5792 assert_eq!(metric.halstead.total_operands(), 10);
5793 },
5794 );
5795 }
5796
5797 /// Regression for #695. A bare `$x` (outside any string) parses as a
5798 /// `simple_expansion` wrapping a `variable_name` leaf — and `$?` / `$1`
5799 /// as a `simple_expansion` wrapping a `special_variable_name` leaf. Both
5800 /// the wrapper and the inner leaf used to be classified as operands, so
5801 /// each bare variable reference double-counted (the same hazard Tcl
5802 /// guards with its `Id2` exclusion and iRules with a parent check). The
5803 /// `variable_name` / `special_variable_name` arm now yields `Unknown`
5804 /// when its parent is a `simple_expansion`, so `$x` contributes exactly
5805 /// one operand while the assignment LHS `variable_name` (`x` in `x=…`,
5806 /// parent is `variable_assignment`) still counts.
5807 #[cfg(feature = "bash")]
5808 #[test]
5809 fn bash_bare_variable_no_double_count() {
5810 let source = "x=1\necho $x\necho $?\n";
5811 let path = PathBuf::from("foo.sh");
5812 let parser = BashParser::new(source.as_bytes().to_vec(), &path, None);
5813 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
5814 let bare_x = ops.operands.iter().filter(|o| o.as_str() == "$x").count();
5815 let special = ops.operands.iter().filter(|o| o.as_str() == "$?").count();
5816 // Each bare reference is exactly one operand; the inner leaf is not
5817 // double-counted. If the guard regressed, the inner `variable_name`
5818 // `x` would add a second `x` occurrence (text-colliding with the
5819 // assignment LHS) and the inner `special_variable_name` `?` would
5820 // appear as a standalone `?` operand.
5821 assert_eq!(
5822 bare_x, 1,
5823 "bare $x must be one operand; operands were {:?}",
5824 ops.operands
5825 );
5826 assert_eq!(
5827 special, 1,
5828 "bare $? must be one operand; operands were {:?}",
5829 ops.operands
5830 );
5831 assert!(
5832 !ops.operands.iter().any(|o| o.as_str() == "?"),
5833 "the inner special_variable_name `?` leaf must be suppressed; operands were {:?}",
5834 ops.operands
5835 );
5836 // The assignment LHS `variable_name` `x` (parent `variable_assignment`,
5837 // not `simple_expansion`) must still be an operand.
5838 assert!(
5839 ops.operands.iter().any(|o| o.as_str() == "x"),
5840 "assignment LHS `x` must still be an operand; operands were {:?}",
5841 ops.operands
5842 );
5843 }
5844
5845 /// Drives one row of a Bash wrapper/leaf double-count table: a grammar
5846 /// wrapper whose entire contribution to `N2` was its single,
5847 /// already-counted child (`command_name`, #1351; `translated_string`,
5848 /// #1358).
5849 ///
5850 /// `expected` is the post-fix `[n1, N1, n2, N2]`. `(n2_before,
5851 /// N2_before)` is what the same source scored while `wrapper` was still
5852 /// classified as an operand. Both columns are checked against the
5853 /// *current* parse under the model "one spurious operand per wrapper
5854 /// node" — they are not measured against the old code, which no longer
5855 /// exists to run:
5856 ///
5857 /// - `N2_before - N2` must equal the number of `wrapper` nodes. That
5858 /// identity *is* the defect: one spurious operand per wrapper.
5859 /// - `n2_before` must equal the post-fix operand vocabulary unioned
5860 /// with the wrapper spellings. It exceeds `n2` wherever the wrapper's
5861 /// whole text is not already an operand in its own right — either
5862 /// because it differs from its single child's (`"$cmd"`, `${cmd}`,
5863 /// `$"ls"`) or because it spans several (`foo$x`, `$(which ls)`,
5864 /// `{1..3}`).
5865 ///
5866 /// So a mistyped or stale column fails rather than misinforming the next
5867 /// reader — one did, during review of #1351 — but neither check can
5868 /// catch a *production* regression, because both hold whatever
5869 /// `get_op_type` answers. `assert_halstead_counts` below is the only
5870 /// assertion here that fails when the wrapper arm comes back; measured
5871 /// by neutralising it under the pre-fix arm, which leaves both tables
5872 /// green.
5873 #[cfg(feature = "bash")]
5874 #[track_caller]
5875 fn assert_bash_wrapper_sheds_one_operand(
5876 source: &str,
5877 expected: [u64; 4],
5878 (n2_before, total_before): (u64, u64),
5879 wrapper: Bash,
5880 ) {
5881 let name: &'static str = wrapper.clone().into();
5882 let kind = wrapper as u16;
5883 let code = source.as_bytes();
5884 let parser = BashParser::new(code.to_vec(), &PathBuf::from("foo.sh"), None);
5885 let spellings: Vec<&str> = parser
5886 .root()
5887 .preorder()
5888 .filter(|node| node.kind_id() == kind)
5889 .filter_map(|node| node.utf8_text(code))
5890 .collect();
5891 assert!(
5892 !spellings.is_empty(),
5893 "row {source:?} parses without a {name}, so it witnesses nothing",
5894 );
5895 // Phrased as an addition rather than `total_before - expected[3]`: a
5896 // future edit that inverts the two would underflow `u64` and panic
5897 // with a raw overflow message instead of the one below.
5898 assert_eq!(
5899 total_before,
5900 expected[3] + spellings.len() as u64,
5901 "row {source:?} must shed exactly one operand per {name}; \
5902 recorded N2_before {total_before}",
5903 );
5904
5905 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
5906 let mut vocabulary: HashSet<&str> = ops.operands.iter().map(String::as_str).collect();
5907 vocabulary.extend(spellings);
5908 assert_eq!(
5909 vocabulary.len() as u64,
5910 n2_before,
5911 "row {source:?}: n2 before the fix is the post-fix vocabulary \
5912 plus the {name} spellings; got {vocabulary:?}",
5913 );
5914
5915 assert_halstead_counts::<BashParser>(source, "foo.sh", expected, source);
5916 }
5917
5918 /// Regression for #1351, the command-name sibling of #695's bare
5919 /// `$x`. `command_name` is a pure wrapper: the grammar gives it
5920 /// exactly one required child (a `_primary_expression` or a
5921 /// `concatenation`) and it adds no text of its own, so classifying it
5922 /// as an operand *and* letting the walk reach the child counted every
5923 /// command name twice in `N2`.
5924 ///
5925 /// The table is the grammar-dispatch §6 evidence that deleting the arm
5926 /// zeroes nothing: it names every kind `command_name` can wrap, and in
5927 /// every row the command name still contributes at least one operand
5928 /// once the trailing `arg` is discounted.
5929 ///
5930 /// `n2_before` / `N2_before` are what each row scores with the
5931 /// `command_name` arm restored *against today's tree*, not a historical
5932 /// measurement — the `$"ls" arg` row has since lost a
5933 /// `translated_string` wrapper to #1358 as well.
5934 /// `assert_bash_wrapper_sheds_one_operand` re-derives both from the
5935 /// current parse rather than trusting them.
5936 #[cfg(feature = "bash")]
5937 #[test]
5938 fn bash_command_name_wrapper_no_double_count() {
5939 // (source, [n1, N1, n2, N2], (n2_before, N2_before))
5940 let cases: [(&str, [u64; 4], (u64, u64)); 14] = [
5941 // word
5942 ("ls bar\n", [0, 0, 2, 2], (2, 3)),
5943 // number
5944 ("1 arg\n", [0, 0, 2, 2], (2, 3)),
5945 // string, inert
5946 ("\"ls\" arg\n", [0, 0, 2, 2], (2, 3)),
5947 // string wrapping an expansion: the wrapper string is already
5948 // skipped (#180), so before #1351 the `command_name` was the
5949 // only thing counting the quoted spelling — which also planted
5950 // a spurious `"$cmd"` entry in n2 beside `$cmd`.
5951 ("\"$cmd\" arg\n", [0, 0, 2, 2], (3, 3)),
5952 // raw_string
5953 ("'ls' arg\n", [0, 0, 2, 2], (2, 3)),
5954 // ansi_c_string
5955 ("$'ls' arg\n", [0, 0, 2, 2], (2, 3)),
5956 // translated_string. Two wrappers stacked here — `command_name`
5957 // over `translated_string` — so the pre-#1351 column below is
5958 // measured against a tree #1358 has already thinned to
5959 // `"ls"` + `arg`.
5960 ("$\"ls\" arg\n", [0, 0, 2, 2], (3, 3)),
5961 // simple_expansion
5962 ("$cmd arg\n", [0, 0, 2, 2], (2, 3)),
5963 // brace expansion: counts through its inner `variable_name`,
5964 // whose `SimpleExpansion` parent guard does not apply here.
5965 ("${cmd} arg\n", [0, 0, 2, 2], (3, 3)),
5966 // command_substitution: counts through the nested command.
5967 // Two command names here — the outer one and `which`.
5968 ("$(which ls) arg\n", [0, 0, 3, 3], (4, 5)),
5969 // process_substitution, likewise two command names.
5970 ("<(ls) arg\n", [0, 0, 2, 2], (3, 4)),
5971 // arithmetic_expansion
5972 ("$((1+1)) arg\n", [1, 1, 2, 3], (3, 4)),
5973 // brace_expression
5974 ("{1..3} arg\n", [1, 1, 3, 3], (4, 4)),
5975 // concatenation
5976 ("foo$x arg\n", [0, 0, 3, 3], (4, 4)),
5977 ];
5978 for (source, expected, before) in cases {
5979 assert!(
5980 expected[3] > 1,
5981 "row {source:?} must leave the command name at least one \
5982 operand beside `arg`; a zero here means the deleted arm \
5983 was load-bearing for this spelling",
5984 );
5985 assert_bash_wrapper_sheds_one_operand(source, expected, before, Bash::CommandName);
5986 }
5987 }
5988
5989 /// Regression for #1358, the sibling of #1351 in the same `match`.
5990 /// `translated_string: $ => seq('$', $.string)` carries exactly one
5991 /// required `string` child and no text of its own beyond the `$`, so
5992 /// classifying the wrapper *and* letting the walk reach the child
5993 /// counted every `$"…"` twice in `N2`.
5994 ///
5995 /// The rows assert the total, `N2`. `n2` alone cannot witness this
5996 /// double count: the wrapper and its child spell *different* text
5997 /// (`$"x"` against `"x"`), so the pre-fix tree split one operand into
5998 /// two vocabulary entries rather than repeating one — which is why the
5999 /// `n2_before` column is carried too, and checked against the wrapper
6000 /// spellings the row actually parses to.
6001 #[cfg(feature = "bash")]
6002 #[test]
6003 fn bash_translated_string_wrapper_no_double_count() {
6004 // (source, [n1, N1, n2, N2], (n2_before, N2_before))
6005 let cases: [(&str, [u64; 4], (u64, u64)); 7] = [
6006 // Assignment RHS.
6007 ("a=$\"x\"\n", [1, 1, 2, 2], (3, 3)),
6008 // Empty translated string. The grammar makes the inner `string`
6009 // required, so the wrapper is never childless — the condition
6010 // grammar-dispatch §6 warns that deletion would regress.
6011 ("a=$\"\"\n", [1, 1, 2, 2], (3, 3)),
6012 // The one spelling the deleted arm *was* load-bearing for: the
6013 // inner `string` wraps an operandless brace `expansion`, so the
6014 // whole `$"…"` now scores zero. That is #1351's residue one
6015 // level down rather than a new gap — a plain `a="${#}"` already
6016 // scored `[1, 1, 1, 1]` — and
6017 // `bash_translated_string_scores_alike_in_both_positions` pins
6018 // that both positions agree on it.
6019 ("a=$\"${#}\"\n", [1, 1, 1, 1], (2, 2)),
6020 // `${!}` is the same residue plus a `!`, which the operator arm
6021 // counts; it must not turn into an operand along the way.
6022 ("a=$\"${!}\"\n", [2, 2, 1, 1], (2, 2)),
6023 // `case` subject.
6024 ("case $\"y\" in *) ;; esac\n", [4, 4, 1, 1], (2, 2)),
6025 // Command-name position, where #1351's wrapper stacks on top of
6026 // this one; the `_before` columns here price the
6027 // `translated_string` alone.
6028 ("$\"ls\" arg\n", [0, 0, 2, 2], (3, 3)),
6029 // Interpolating form. `bash_string_has_expansion` inspects the
6030 // node's own children, and a `translated_string` has only `$`
6031 // and `string`, so it never saw the `simple_expansion` one level
6032 // down: the wrapper reintroduced exactly the double count #180
6033 // removed, for the `$"…"` spelling alone. Only `$y` and `b` may
6034 // survive.
6035 ("b=$\"$y\"\n", [1, 1, 2, 2], (3, 3)),
6036 ];
6037 for (source, expected, before) in cases {
6038 assert_bash_wrapper_sheds_one_operand(source, expected, before, Bash::TranslatedString);
6039 }
6040 }
6041
6042 /// The load-bearing half of #1358's grammar-dispatch §6 argument, which
6043 /// nothing else asserts. In ordinary argument position the grammar emits
6044 /// no `translated_string` at all — just a `$` token and a `string` — so
6045 /// the `string` is the node present for *every* spelling, and dropping
6046 /// the wrapper makes the wrapper-bearing positions agree with argument
6047 /// position rather than newly disagree. If a grammar bump starts
6048 /// emitting the wrapper here, the deletion has to be re-derived.
6049 #[cfg(feature = "bash")]
6050 #[test]
6051 fn bash_translated_string_scores_alike_in_both_positions() {
6052 // `${#}` carries the residue: it contributes no operand of its own,
6053 // so the second row pins that the deletion left *both* positions at
6054 // zero rather than only the wrapper-bearing one.
6055 //
6056 // Only the command-name half moves under the pre-fix arm; the
6057 // argument-position half is the reference the parity is measured
6058 // against, and `cmd $"…"` is asserted nowhere else. Measured — with
6059 // the argument assertions dropped this test still fails pre-fix,
6060 // with the command-name ones dropped it passes — so do not
6061 // "simplify" it down to the failing half.
6062 for (spelling, expected) in [("$\"hi\"", [0, 0, 2, 2]), ("$\"${#}\"", [0, 0, 1, 1])] {
6063 let as_command_name = format!("{spelling} arg\n");
6064 let as_argument = format!("cmd {spelling}\n");
6065 let parse = |source: &str| {
6066 BashParser::new(source.as_bytes().to_vec(), &PathBuf::from("foo.sh"), None)
6067 };
6068 assert!(
6069 !ast_has_kind_id(&parse(&as_argument), Bash::TranslatedString as u16),
6070 "argument position must still parse {spelling} as a bare `$` \
6071 plus a `string`; a grammar bump that emits the wrapper here \
6072 invalidates the parity this test rests on",
6073 );
6074 assert!(
6075 ast_has_kind_id(&parse(&as_command_name), Bash::TranslatedString as u16),
6076 "command-name position must still emit the wrapper, or the \
6077 pair witnesses nothing",
6078 );
6079 assert_halstead_counts::<BashParser>(
6080 &as_command_name,
6081 "foo.sh",
6082 expected,
6083 &as_command_name,
6084 );
6085 assert_halstead_counts::<BashParser>(&as_argument, "foo.sh", expected, &as_argument);
6086 }
6087 }
6088
6089 /// Pins the one residue of #1351: a brace `expansion` with no
6090 /// `variable_name` leaf. `${#}` (positional-parameter count) and `${!}`
6091 /// (last background PID) hold only anonymous tokens, and
6092 /// `Bash::Expansion` is not an operand, so the whole expansion
6093 /// contributes nothing.
6094 ///
6095 /// That was already true in *argument* position before the fix, which
6096 /// is the reason grammar-dispatch §6's gate-don't-delete rule did not
6097 /// apply to the `command_name` arm: deleting it made command-name
6098 /// position agree with argument position rather than newly disagree.
6099 /// The parity is the load-bearing half of that argument and nothing
6100 /// else asserts it, so if a future arm starts classifying these the
6101 /// two positions have to move together.
6102 #[cfg(feature = "bash")]
6103 #[test]
6104 fn bash_operandless_expansion_scores_alike_in_both_positions() {
6105 // `${!}` carries a `!`, which the operator arm counts; `${#}`'s `#`
6106 // is not an operator. Both positions must agree per spelling.
6107 for (spelling, expected) in [("${#}", [0, 0, 1, 1]), ("${!}", [1, 1, 1, 1])] {
6108 let as_command_name = format!("{spelling} arg\n");
6109 let as_argument = format!("cmd {spelling}\n");
6110 assert_halstead_counts::<BashParser>(
6111 &as_command_name,
6112 "foo.sh",
6113 expected,
6114 &as_command_name,
6115 );
6116 assert_halstead_counts::<BashParser>(&as_argument, "foo.sh", expected, &as_argument);
6117 }
6118 }
6119
6120 /// Drift marker for #1351 (lesson 34 / grammar-dispatch §2). `_concat`
6121 /// (`Bash::Concat`) is a hidden zero-width external token the scanner
6122 /// emits between `concatenation` parts; the parser never surfaces it as
6123 /// a node, and it spells no operand, so `BashCode::get_op_type` lists
6124 /// neither it nor the visible `concatenation` wrapper. If a grammar
6125 /// bump starts emitting it, this fails and the classification must be
6126 /// re-derived rather than assumed still absent.
6127 ///
6128 /// Measured, not assumed: putting `Bash::Concat` back in the operand
6129 /// arm fails no test in the suite, because the token is unreachable.
6130 /// Unreachability is the only coverage such an arm can have, which is
6131 /// why this test asserts it directly instead of asserting a count.
6132 #[cfg(feature = "bash")]
6133 #[test]
6134 fn bash_hidden_concat_token_is_unreachable() {
6135 let source = "a=foo$x\nb=pre\"$y\"post\ncmd bar$z\n";
6136 let path = PathBuf::from("foo.sh");
6137 let parser = BashParser::new(source.as_bytes().to_vec(), &path, None);
6138 // Non-vacuity: the visible `concatenation` this source is written
6139 // to produce must actually be in the parse, so the negative below
6140 // is about `_concat` and not about a source that concatenates
6141 // nothing.
6142 assert!(
6143 ast_has_kind_id(&parser, Bash::Concatenation as u16),
6144 "expected a visible `concatenation` node in the parse",
6145 );
6146 assert!(
6147 !ast_has_kind_id(&parser, Bash::Concat as u16),
6148 "the hidden `_concat` token surfaced; re-derive its \
6149 classification in BashCode::get_op_type against the new grammar",
6150 );
6151 }
6152
6153 #[cfg(feature = "tcl")]
6154 #[test]
6155 fn tcl_operators_and_operands() {
6156 check_metrics::<TclParser>(
6157 "proc f {a b} {
6158 set x [expr {$a + $b}]
6159 if {$x > 0 && $x != 0} {
6160 return $x
6161 }
6162 return 0
6163}",
6164 "foo.tcl",
6165 |metric| {
6166 // Anchored per the snapshot policy in AGENTS.md, which
6167 // this call predates. Operators `proc`, `set`, `[]`,
6168 // `{}`, `expr`, `+`, `if`, `>`, `&&`, `!=` → n1 = 10,
6169 // N1 = 14 (`{}` × 4 for the proc parameter list, the
6170 // proc body, the two `expr`/`if` conditions and the
6171 // `if` body — the `expr` braces are an `Expr`, the
6172 // bodies a `BracedWord`). Operands `f`, `a`, `b`, `x`,
6173 // `$a`, `$b`, `$x`, `0` and `return` → n2 = 9,
6174 // N2 = 14. Before #1354 the proc body and the `if`
6175 // body were operands too → 11 / 16.
6176 assert_eq!(metric.halstead.unique_operators(), 10);
6177 assert_eq!(metric.halstead.total_operators(), 14);
6178 assert_eq!(metric.halstead.unique_operands(), 9);
6179 assert_eq!(metric.halstead.total_operands(), 14);
6180 insta::assert_json_snapshot!(metric.halstead);
6181 },
6182 );
6183 }
6184
6185 #[cfg(feature = "tcl")]
6186 #[test]
6187 fn tcl_bitwise_ternary_string_ops() {
6188 // Exercises operator families not covered by tcl_operators_and_operands:
6189 // bitwise (&, |, ^, ~, <<, >>), ternary (?), and string-comparison (eq, ne, in, ni).
6190 check_metrics::<TclParser>(
6191 "proc f {a b} {
6192 set bits [expr {$a & $b | $a ^ ~$b}]
6193 set sh [expr {$a << 1 | $b >> 1}]
6194 set t [expr {$a > 0 ? $a : $b}]
6195 if {$a eq {x} || $a ne {y}} {
6196 return $a
6197 }
6198 return $b
6199}",
6200 "foo.tcl",
6201 |metric| {
6202 // Anchored per the snapshot policy in AGENTS.md, which
6203 // this call predates. N1 fell 33 → 31 with #1314: the
6204 // `if` condition's `{x}` and `{y}` are braced *words*,
6205 // so their openers stopped fabricating a `{}` operator.
6206 // n1 is unchanged at 18 because the `{}` entry survives
6207 // on the proc body and the `expr` braces — which is
6208 // exactly why the fabrication was invisible in n1 and
6209 // is the reason to assert N1 as well (#1294).
6210 //
6211 // The operand columns fell 17 / 30 → 13 / 26 with
6212 // #1354, and the operator columns did not move: the two
6213 // script bodies (the proc's and the `if`'s) stopped
6214 // being operands, and the `x` / `y` inside the braced
6215 // words `{x}` and `{y}` are now part of the one operand
6216 // each word contributes.
6217 assert_eq!(metric.halstead.unique_operators(), 18);
6218 assert_eq!(metric.halstead.total_operators(), 31);
6219 assert_eq!(metric.halstead.unique_operands(), 13);
6220 assert_eq!(metric.halstead.total_operands(), 26);
6221 insta::assert_json_snapshot!(metric.halstead);
6222 },
6223 );
6224 }
6225
6226 #[cfg(feature = "tcl")]
6227 #[test]
6228 fn tcl_array_reference_bills_the_reference_and_the_index() {
6229 // `$arr($i)` is the reference plus the index Tcl substitutes
6230 // inside the parens, and `arr(k)` as a `set` target is the name
6231 // plus the literal index; the `array_index` wrapper is neither.
6232 // The quoted spelling is deliberate — the vendored grammar
6233 // mis-parses a bare `$arr(k)` in command-word position. Pinned
6234 // per dialect and as a parity in `tests/parity/`, because the
6235 // iRules twin listed the wrapper as a third operand.
6236 assert_ops_operands::<TclParser>(
6237 "set arr(k) 1\nset z \"$arr($i)\"\n",
6238 "foo.tcl",
6239 6,
6240 vec!["arr", "k", "1", "z", "$arr($i)", "$i"],
6241 );
6242 }
6243
6244 #[cfg(feature = "tcl")]
6245 #[test]
6246 fn tcl_bare_variable_operand() {
6247 // Bare `$varname` produces a VariableSubstitution node (already an operand).
6248 // Its anonymous Id2 child must NOT be counted separately; each reference is 1 operand.
6249 check_metrics::<TclParser>(
6250 "proc f {x} {
6251 return $x
6252}",
6253 "foo.tcl",
6254 |metric| {
6255 // Anchored per the snapshot policy in AGENTS.md, which
6256 // this call predates. Operators `proc` and `{}` × 2
6257 // (the parameter list and the body) → n1 = 2, N1 = 3.
6258 // Operands `f`, the parameter `x`, `return` and `$x` —
6259 // one occurrence each, so a re-counted `x` leaf inside
6260 // `$x` would show up in N2 even though it collides with
6261 // the parameter in n2. Before #1354 the proc body was a
6262 // fifth operand.
6263 assert_eq!(metric.halstead.unique_operators(), 2);
6264 assert_eq!(metric.halstead.total_operators(), 3);
6265 assert_eq!(metric.halstead.unique_operands(), 4);
6266 assert_eq!(metric.halstead.total_operands(), 4);
6267 insta::assert_json_snapshot!(metric.halstead);
6268 },
6269 );
6270 }
6271
6272 #[cfg(feature = "tcl")]
6273 #[test]
6274 fn tcl_inert_quoted_word_counts_as_operand() {
6275 // Regression for #277. A `"..."` literal with no `$var` / `[cmd]`
6276 // interpolation must contribute exactly one operand (the wrapping
6277 // `QuotedWord`). The string content `hello world` is exposed as a
6278 // single `_quoted_word_content` token (not itself classified by
6279 // `get_op_type`), so the only operands here are `f`, `s`, and the
6280 // quoted string. `set` is the anonymous `Set2` keyword and is
6281 // classified as an operator, not an operand.
6282 check_metrics::<TclParser>(
6283 "proc f {} {
6284 set s \"hello world\"
6285}",
6286 "foo.tcl",
6287 |metric| {
6288 // Operands: `f`, the `set` target `s`, `"hello world"` —
6289 // 3 unique, 3 total. Before #1294 this read 3/3 for a
6290 // different reason, with `s` missing and the proc-body
6291 // `braced_word` making the count coincidentally
6292 // plausible; #1354 removed that body operand, so the
6293 // three named here are now the whole list. The wrapping
6294 // `QuotedWord` must still contribute exactly one operand
6295 // when it carries no interpolation children; dropping to 2
6296 // would mean the inert case was over-guarded.
6297 assert_eq!(metric.halstead.unique_operands(), 3);
6298 assert_eq!(metric.halstead.total_operands(), 3);
6299 insta::assert_json_snapshot!(metric.halstead);
6300 },
6301 );
6302 }
6303
6304 #[cfg(feature = "tcl")]
6305 #[test]
6306 fn tcl_interpolated_quoted_word_no_double_count() {
6307 // Regression for #277. Before the fix, `"$x is $y"` produced an
6308 // extra operand for the wrapping `QuotedWord` on top of the two
6309 // inner `VariableSubstitution` operands (`$x`, `$y`), giving 7.
6310 // After the fix, the wrapper is `TokenRole::Unknown` whenever
6311 // it carries an interpolation child, so operand attribution
6312 // belongs solely to the inner substitutions.
6313 check_metrics::<TclParser>(
6314 "proc f {x y} {
6315 set s \"$x is $y\"
6316}",
6317 "foo.tcl",
6318 |metric| {
6319 // Operands: `f`, `x`, `y` (proc args), the `set` target
6320 // `s`, `$x`, `$y` — 6 unique, 6 total. The wrapping
6321 // `QuotedWord` contributes nothing, and since #1354
6322 // neither does the proc-body `braced_word`. Before #277
6323 // the wrapper double-counted.
6324 assert_eq!(metric.halstead.unique_operands(), 6);
6325 assert_eq!(metric.halstead.total_operands(), 6);
6326 insta::assert_json_snapshot!(metric.halstead);
6327 },
6328 );
6329 }
6330
6331 #[cfg(feature = "tcl")]
6332 #[test]
6333 fn tcl_command_substitution_quoted_word_no_double_count() {
6334 // Regression for #277. A `"...[cmd]..."` literal exposes the
6335 // bracketed command as a `command_substitution` child whose inner
6336 // identifiers/literals contribute their own operands. The wrapping
6337 // `QuotedWord` must not also be classified as an operand, or the
6338 // command's identifier would be counted alongside a phantom
6339 // wrapper operand.
6340 check_metrics::<TclParser>(
6341 "proc f {} {
6342 set s \"result: [foo]\"
6343}",
6344 "foo.tcl",
6345 |metric| {
6346 // Operands: `f`, the `set` target `s`, `foo` — 3 unique,
6347 // 3 total. The wrapping `QuotedWord` and the inert text
6348 // `result: ` do not contribute extra operands, and since
6349 // #1354 neither does the proc-body `braced_word`. Before
6350 // #277 the wrapper double-counted.
6351 assert_eq!(metric.halstead.unique_operands(), 3);
6352 assert_eq!(metric.halstead.total_operands(), 3);
6353 insta::assert_json_snapshot!(metric.halstead);
6354 },
6355 );
6356 }
6357
6358 /// Regression for #1294. The `set` target parses as the anonymous
6359 /// `id` token (`Tcl::Id2`) — the same kind as the leaf inside a
6360 /// `variable_substitution` — and the getter used to exclude that kind
6361 /// wholesale, so every variable a Tcl script assigned was absent from
6362 /// n2/N2. The guard is now parent-scoped: a target `id` counts, a
6363 /// var-sub leaf does not. Exact occurrence counts distinguish this
6364 /// fix from a regression in either direction: a re-blanketed
6365 /// exclusion drops `s`/`t` (total 2), while losing the guard
6366 /// double-counts the `$s` leaf as a second `s` (total 5).
6367 #[cfg(feature = "tcl")]
6368 #[test]
6369 fn tcl_set_target_is_operand() {
6370 let source = "set s 1\nset t $s\n";
6371 let path = PathBuf::from("foo.tcl");
6372 let parser = TclParser::new(source.as_bytes().to_vec(), &path, None);
6373 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
6374 // expected operands: targets `s` and `t`, literal `1`, reference
6375 // `$s` (wrapper only) — 4 total, each exactly once.
6376 for operand in ["s", "t", "1", "$s"] {
6377 assert_eq!(
6378 ops.operands
6379 .iter()
6380 .filter(|o| o.as_str() == operand)
6381 .count(),
6382 1,
6383 "`{operand}` must be exactly one operand; operands were {:?}",
6384 ops.operands
6385 );
6386 }
6387 assert_eq!(
6388 ops.operands.len(),
6389 4,
6390 "operands must be exactly s, t, 1, $s; got {:?}",
6391 ops.operands
6392 );
6393
6394 check_metrics::<TclParser>(source, "foo.tcl", |metric| {
6395 // expected: n2 = 4 (s, t, 1, $s), N2 = 4; operators are the
6396 // two `set` keywords — n1 = 1, N1 = 2.
6397 assert_eq!(metric.halstead.unique_operands(), 4);
6398 assert_eq!(metric.halstead.total_operands(), 4);
6399 assert_eq!(metric.halstead.unique_operators(), 1);
6400 assert_eq!(metric.halstead.total_operators(), 2);
6401 });
6402 }
6403
6404 /// Drift marker for #1294 (lesson 34 / grammar-dispatch §2): the
6405 /// *named* `id` rule (`Tcl::Id`, kind_id 84) never surfaces at the
6406 /// pinned tree-sitter-tcl — the parser emits the anonymous `Id2` in
6407 /// both positions the getter guards (the `set` target and the
6408 /// var-sub leaf). The `Tcl::Id` arm in `get_op_type` is therefore
6409 /// defensive; if a grammar bump starts emitting 84 this fails and
6410 /// the arm's classification must be re-derived instead of trusted.
6411 #[cfg(feature = "tcl")]
6412 #[test]
6413 fn tcl_named_id_variant_is_unreachable() {
6414 let source = "proc f {x} {\n set s $x\n foreach v {1 2} { puts \"$v\" }\n}\n";
6415 let path = PathBuf::from("foo.tcl");
6416 let parser = TclParser::new(source.as_bytes().to_vec(), &path, None);
6417 // Non-vacuity: the anonymous token must be present in this parse
6418 // (both the `set` target and the `$x` / `$v` leaves emit it).
6419 assert!(
6420 ast_has_kind_id(&parser, Tcl::Id2 as u16),
6421 "expected the anonymous Tcl::Id2 token to appear in the parse",
6422 );
6423 assert!(
6424 !ast_has_kind_id(&parser, Tcl::Id as u16),
6425 "the named Tcl::Id rule surfaced; re-derive the defensive \
6426 `Tcl::Id` arm in TclCode::get_op_type against the new grammar",
6427 );
6428 }
6429
6430 #[cfg(feature = "tcl")]
6431 #[test]
6432 fn tcl_braced_word_delimiter_is_not_an_operator() {
6433 // Regression: issue #1314, the Tcl sibling of Elixir #1256 and
6434 // Ruby/Perl #1312. A braced *word* — a literal value, not a
6435 // script — carries its `{` as an `LBRACE` child, the kind id a
6436 // real block uses, so `set a {braced word}` reported a `{}`
6437 // operator with no block in the source.
6438 //
6439 // expected: operator `set` × 3 → n1 = 1, N1 = 3. Operands
6440 // `a`, `b`, `c`, `$a` and `{braced word}` × 2 → n2 = 5,
6441 // N2 = 6. Before the guard the two openers added `{}` →
6442 // n1 = 2, N1 = 5; before #1354 the inner `braced` / `word`
6443 // counted alongside the word containing them → n2 = 7,
6444 // N2 = 10.
6445 check_metrics::<TclParser>(
6446 "set a {braced word}\nset b {braced word}\nset c $a\n",
6447 "foo.tcl",
6448 |metric| {
6449 assert_eq!(metric.halstead.unique_operators(), 1);
6450 assert_eq!(metric.halstead.total_operators(), 3);
6451 assert_eq!(metric.halstead.unique_operands(), 5);
6452 assert_eq!(metric.halstead.total_operands(), 6);
6453 },
6454 );
6455 }
6456
6457 #[cfg(feature = "tcl")]
6458 #[test]
6459 fn tcl_script_bodies_keep_their_braces() {
6460 // Control for #1314, and the reason a kind-scoped guard is safe
6461 // in Tcl where it would not be elsewhere: the grammar gives the
6462 // literal and the block *different* kinds. A `proc` body, an
6463 // `if` body and an `if` condition are `BracedWord` (88) and
6464 // `Expr` (97); only the value form is `BracedWordSimple` (89).
6465 // This fixture nests a braced word inside a real script body,
6466 // so a guard that keyed on the brace alone would drop the
6467 // block's `{}` and fail here.
6468 //
6469 // expected: operators `proc`, `set` × 2, `if`, `>`, and `{}`
6470 // × 4 (the proc parameter list, the proc body, the `if`
6471 // condition, the `if` body) → n1 = 5, N1 = 9. Operands, all
6472 // distinct → n2 = N2 = 8: `p`, `x`, `a`, `$x`, `1`, `b` and
6473 // the two braced *words* `{v w}` and `{y}`, each one operand
6474 // rather than one per inner word.
6475 //
6476 // Before #1354 this read 13 / 13. The five extra entries were
6477 // the inner words `v`, `w`, `y` and the two *script* bodies,
6478 // which were `BracedWord` operands in their own right — so a
6479 // block counted twice over, once as the operand and once as
6480 // the `{}` operator this test is about. Both halves are gone;
6481 // the operator columns are what this test guards and they did
6482 // not move.
6483 check_metrics::<TclParser>(
6484 "proc p {x} {\n set a {v w}\n if {$x > 1} { set b {y} }\n}\n",
6485 "foo.tcl",
6486 |metric| {
6487 assert_eq!(metric.halstead.unique_operators(), 5);
6488 assert_eq!(metric.halstead.total_operators(), 9);
6489 assert_eq!(metric.halstead.unique_operands(), 8);
6490 assert_eq!(metric.halstead.total_operands(), 8);
6491 },
6492 );
6493 }
6494
6495 #[cfg(feature = "tcl")]
6496 #[test]
6497 fn tcl_braced_word_guard_is_parent_scoped_not_ancestor_scoped() {
6498 // The input that separates the parent-scoped guard from the
6499 // ancestor-scanning mutant. I first recorded this distinction
6500 // as *unobservable* in Tcl, reasoning that a braced word holds
6501 // only simple words and nested braced words. `bca dump` says
6502 // otherwise: the grammar parses a `[…]` command substitution
6503 // inside a braced word, and the `if` inside it brings an `Expr`
6504 // condition and a `BracedWord` body, each with its own `{`,
6505 // both of them non-immediate descendants of the
6506 // `BracedWordSimple`. An ancestor scan swallows both.
6507 //
6508 // (Real Tcl does not substitute inside braces — this is the
6509 // grammar modelling structure it will not evaluate. What the
6510 // classifier sees is what the metric reports, so it is the
6511 // right fixture regardless.)
6512 //
6513 // expected: operators `set`, `[]`, `if`, and `{}` × 2 (the
6514 // `if` condition's `Expr` and its `BracedWord` body; the outer
6515 // value word's own `{` is suppressed) → n1 = 4, N1 = 5.
6516 // Operands `z`, the whole braced word, and `$q` / `puts` / `w`
6517 // from inside the command substitution → n2 = N2 = 5. Under
6518 // the ancestor-scoped mutant both surviving braces vanish:
6519 // n1 = 3, N1 = 3.
6520 //
6521 // #1354 widened the guard from the `{` alone to every direct
6522 // child of the braced word, which is why `x` and `v` are no
6523 // longer operands and the nested script body no longer is
6524 // either. It did not change the *scope*: the three operands
6525 // from inside the command substitution are grandchildren, and
6526 // an ancestor-scoped guard would drop them too.
6527 check_metrics::<TclParser>("set z {x [if {$q} {puts w}] v}\n", "foo.tcl", |metric| {
6528 assert_eq!(metric.halstead.unique_operators(), 4);
6529 assert_eq!(metric.halstead.total_operators(), 5);
6530 assert_eq!(metric.halstead.unique_operands(), 5);
6531 assert_eq!(metric.halstead.total_operands(), 5);
6532 });
6533 }
6534
6535 #[cfg(feature = "irules")]
6536 #[test]
6537 fn irules_braced_word_guard_is_parent_scoped_not_ancestor_scoped() {
6538 // The iRules twin of the test above — the two getters are
6539 // clones, so the mutant must fail in both.
6540 //
6541 // expected: operators `when`, `set`, `[]`, `if`, `{}` × 3 (the
6542 // handler body, the `if` condition and the `if` body) → n1 = 5,
6543 // N1 = 7. Operands `HTTP_REQUEST`, `z`, the whole braced word,
6544 // and `$q` / `log` / `w` from inside the command substitution →
6545 // n2 = N2 = 6 (10 before #1354, which also took the direct
6546 // children `x` / `v` and the two script bodies out of the
6547 // operand set without moving the operator columns this test
6548 // guards).
6549 check_metrics::<IrulesParser>(
6550 "when HTTP_REQUEST {\n set z {x [if {$q} {log w}] v}\n}\n",
6551 "foo.irule",
6552 |metric| {
6553 assert_eq!(metric.halstead.unique_operators(), 5);
6554 assert_eq!(metric.halstead.total_operators(), 7);
6555 assert_eq!(metric.halstead.unique_operands(), 6);
6556 assert_eq!(metric.halstead.total_operands(), 6);
6557 },
6558 );
6559 }
6560
6561 /// How one dialect of the Tcl family spells the braced-word
6562 /// construct. The two grammars are deliberate clones with different
6563 /// id blocks, so #1354's guard is derived once and instantiated
6564 /// twice — and a fix that landed in only one dialect fails the
6565 /// second instantiation rather than going unnoticed.
6566 struct BracedWordKinds {
6567 /// `braced_word_simple`, the literal *value* form the guard
6568 /// keys on.
6569 wrapper: u16,
6570 /// `braced_word`, the *script* form #1354 gated on holding a
6571 /// command: an operand only when it holds none.
6572 script_body: u16,
6573 /// `comment`, the one named child of a script that is not a
6574 /// command and that the gate must not mistake for one.
6575 comment: u16,
6576 /// Every named kind node-types.json admits directly inside
6577 /// `wrapper`. A child outside this set means the grammar moved
6578 /// and the parent-keyed arm has to be re-derived
6579 /// (grammar-dispatch §1).
6580 children: [u16; 6],
6581 /// The `{` / `}` the wrapper also holds. Suppressing the opener
6582 /// was the whole of #1314's narrower guard; the closer has
6583 /// never been classified, since `get_operator_id_as_str` folds
6584 /// the pair to one `{}` glyph.
6585 delimiters: [u16; 2],
6586 /// Of `children`, the three the operand arm classified
6587 /// unconditionally before the guard.
6588 operand_children: [u16; 3],
6589 /// `quoted_word`, which was an operand only when *inert* —
6590 /// `string_operand_type`'s own rule, replicated here so the
6591 /// shed count below is what `N2` actually shed.
6592 quoted_word: u16,
6593 /// The interpolation kinds that decide that.
6594 interpolation: [u16; 2],
6595 }
6596
6597 #[cfg(feature = "tcl")]
6598 const TCL_BRACED_WORD_KINDS: BracedWordKinds = BracedWordKinds {
6599 wrapper: Tcl::BracedWordSimple as u16,
6600 script_body: Tcl::BracedWord as u16,
6601 comment: Tcl::Comment as u16,
6602 children: [
6603 Tcl::SimpleWord as u16,
6604 Tcl::EscapedCharacter as u16,
6605 Tcl::QuotedWord as u16,
6606 Tcl::VariableSubstitution as u16,
6607 Tcl::CommandSubstitution as u16,
6608 Tcl::BracedWordSimple as u16,
6609 ],
6610 delimiters: [Tcl::LBRACE as u16, Tcl::RBRACE as u16],
6611 operand_children: [
6612 Tcl::SimpleWord as u16,
6613 Tcl::VariableSubstitution as u16,
6614 Tcl::BracedWordSimple as u16,
6615 ],
6616 quoted_word: Tcl::QuotedWord as u16,
6617 interpolation: [
6618 Tcl::VariableSubstitution as u16,
6619 Tcl::CommandSubstitution as u16,
6620 ],
6621 };
6622
6623 #[cfg(feature = "irules")]
6624 const IRULES_BRACED_WORD_KINDS: BracedWordKinds = BracedWordKinds {
6625 wrapper: Irules::BracedWordSimple as u16,
6626 script_body: Irules::BracedWord as u16,
6627 comment: Irules::Comment as u16,
6628 children: [
6629 Irules::SimpleWord as u16,
6630 Irules::EscapedCharacter as u16,
6631 Irules::QuotedWord as u16,
6632 Irules::VariableSubstitution as u16,
6633 Irules::CommandSubstitution as u16,
6634 Irules::BracedWordSimple as u16,
6635 ],
6636 delimiters: [Irules::LBRACE as u16, Irules::RBRACE as u16],
6637 operand_children: [
6638 Irules::SimpleWord as u16,
6639 Irules::VariableSubstitution as u16,
6640 Irules::BracedWordSimple as u16,
6641 ],
6642 quoted_word: Irules::QuotedWord as u16,
6643 interpolation: [
6644 Irules::VariableSubstitution as u16,
6645 Irules::CommandSubstitution as u16,
6646 ],
6647 };
6648
6649 /// The operand occurrences #1354 removed from `source`, as their
6650 /// source texts, plus the set of `braced_word_simple` child kinds
6651 /// the fixture witnessed.
6652 ///
6653 /// Walks with `for_each_node_with_chain`, which maintains the
6654 /// ancestor chain exactly as `spaces::compute` does, so "parent"
6655 /// here means what `Ancestors::parent` means inside the guard.
6656 /// Doubles as the grammar-dispatch §1 / §2 drift marker: a child of
6657 /// the wrapper outside `children` ∪ `delimiters` fails on the spot,
6658 /// which is what makes keying the arm on the parent alone — rather
6659 /// than on an enumerated child list — safe to rely on.
6660 #[cfg(any(feature = "irules", feature = "tcl"))]
6661 fn braced_word_shed<L: crate::LanguageInfo>(
6662 source: &str,
6663 kinds: &BracedWordKinds,
6664 ) -> (Vec<String>, HashSet<u16>) {
6665 let code = source.as_bytes();
6666 let mut shed = Vec::new();
6667 let mut witnessed = HashSet::new();
6668 for_each_node_with_chain::<L>(code, |node, chain| {
6669 let text = || {
6670 node.utf8_text(code)
6671 .expect("fixture is valid UTF-8")
6672 .to_owned()
6673 };
6674 // The script form was an operand of its own wherever it
6675 // appeared until #1354, which now gates it on holding a
6676 // command — a named child that is not a comment. This is not
6677 // a child of the wrapper, so it is counted before the parent
6678 // test below.
6679 if node.kind_id() == kinds.script_body
6680 && node
6681 .children()
6682 .any(|child| child.is_named() && child.kind_id() != kinds.comment)
6683 {
6684 shed.push(text());
6685 }
6686 if chain.last().is_none_or(|p| p.kind_id() != kinds.wrapper) {
6687 return;
6688 }
6689 assert!(
6690 kinds.children.contains(&node.kind_id())
6691 || kinds.delimiters.contains(&node.kind_id()),
6692 "`{source}`: a `{}` inside a braced word is a child this guard \
6693 was not derived against; re-read node-types.json before \
6694 trusting it",
6695 node.kind(),
6696 );
6697 if kinds.children.contains(&node.kind_id()) {
6698 witnessed.insert(node.kind_id());
6699 }
6700 let was_operand = kinds.operand_children.contains(&node.kind_id())
6701 || (node.kind_id() == kinds.quoted_word && !node.wraps_any(&kinds.interpolation));
6702 if was_operand {
6703 shed.push(text());
6704 }
6705 });
6706 (shed, witnessed)
6707 }
6708
6709 /// One row of the #1354 tables: a fixture, what it measures now,
6710 /// what it measured before, and the operand texts behind the counts.
6711 struct BracedWordCase {
6712 source: &'static str,
6713 /// `[n1, N1, n2, N2]` with the guard in place.
6714 counts: [u64; 4],
6715 /// `[n2, N2]` without it. Re-derived by the loop rather than
6716 /// trusted, so a stale row fails instead of misinforming.
6717 before: [u64; 2],
6718 operands: &'static [&'static str],
6719 }
6720
6721 /// Every row is measured in *both* dialects, so a fix applied to
6722 /// one getter and not its clone fails here. `braced_word_shed`'s
6723 /// drift assertion likewise runs against both grammars.
6724 #[cfg(any(feature = "irules", feature = "tcl"))]
6725 fn check_braced_word_cases<T: crate::MetricSuite, L: crate::LanguageInfo>(
6726 cases: &[BracedWordCase],
6727 file: &str,
6728 kinds: &BracedWordKinds,
6729 ) -> HashSet<u16> {
6730 let mut witnessed = HashSet::new();
6731 for case in cases {
6732 let (shed, seen) = braced_word_shed::<L>(case.source, kinds);
6733 witnessed.extend(seen);
6734
6735 // Phrased as an addition rather than a subtraction so a
6736 // future edit that inverts the two underflows nothing.
6737 assert_eq!(
6738 case.before[1],
6739 case.counts[3] + shed.len() as u64,
6740 "{file} `{}`: N2 must shed exactly one occurrence per \
6741 previously-billed part; recorded {}, parts {shed:?}",
6742 case.source,
6743 case.before[1],
6744 );
6745 let mut vocabulary: HashSet<&str> = case.operands.iter().copied().collect();
6746 vocabulary.extend(shed.iter().map(String::as_str));
6747 assert_eq!(
6748 vocabulary.len() as u64,
6749 case.before[0],
6750 "{file} `{}`: n2 before the fix is the post-fix vocabulary \
6751 plus those parts; got {vocabulary:?}",
6752 case.source,
6753 );
6754
6755 assert_halstead_counts::<T>(case.source, file, case.counts, case.source);
6756 assert_ops_operands::<T>(
6757 case.source,
6758 file,
6759 case.operands.len(),
6760 case.operands.to_vec(),
6761 );
6762 }
6763 witnessed
6764 }
6765
6766 /// The rows shared by both dialects: one per named child kind
6767 /// `braced_word_simple` admits, the childless spelling, the
6768 /// repeated-value row that separates `n2` from `N2` (#1294), and
6769 /// the braced/quoted parity pair #1317 asks for.
6770 #[cfg(any(feature = "irules", feature = "tcl"))]
6771 const BRACED_WORD_CASES: [BracedWordCase; 11] = [
6772 // simple_word, the reported fixture. Two words inside one
6773 // value scored two operands beside the value itself.
6774 BracedWordCase {
6775 source: "set x {literal here}\n",
6776 counts: [1, 1, 2, 2],
6777 before: [4, 4],
6778 operands: &["x", "{literal here}"],
6779 },
6780 // The childless spelling, and the reason the wrapper is kept
6781 // rather than dropped in favour of its contents
6782 // (grammar-dispatch §6): with nothing inside, the wrapper is
6783 // the empty string's only carrier. Nothing is shed here, so
6784 // this row asserts the two columns agree.
6785 BracedWordCase {
6786 source: "set y {}\n",
6787 counts: [1, 1, 2, 2],
6788 before: [2, 2],
6789 operands: &["y", "{}"],
6790 },
6791 // braced_word_simple inside braced_word_simple: the nesting
6792 // that makes the over-count unbounded in depth. One value
6793 // spelled six vocabulary entries.
6794 BracedWordCase {
6795 source: "set a {x {y z}}\n",
6796 counts: [1, 1, 2, 2],
6797 before: [6, 6],
6798 operands: &["a", "{x {y z}}"],
6799 },
6800 // quoted_word, inert — an operand in its own right elsewhere,
6801 // and shed here.
6802 BracedWordCase {
6803 source: "set a {x \"q w\" v}\n",
6804 counts: [1, 1, 2, 2],
6805 before: [5, 5],
6806 operands: &["a", "{x \"q w\" v}"],
6807 },
6808 // quoted_word carrying an interpolation, which was *not* an
6809 // operand before the guard either (`string_operand_type` had
6810 // already suppressed it) — so only `x` and `v` are shed. Its
6811 // `$q` is a grandchild of the braced word and still counts,
6812 // which is the parent-scoping this arm inherits from #1314.
6813 BracedWordCase {
6814 source: "set a {x \"$q\" v}\n",
6815 counts: [1, 1, 3, 3],
6816 before: [5, 5],
6817 operands: &["a", "$q", "{x \"$q\" v}"],
6818 },
6819 // escaped_character, never classified — so it sheds nothing
6820 // and the row measures the two `simple_word`s around it.
6821 BracedWordCase {
6822 source: "set a {x \\n y}\n",
6823 counts: [1, 1, 2, 2],
6824 before: [4, 4],
6825 operands: &["a", "{x \\n y}"],
6826 },
6827 // variable_substitution. Tcl substitutes nothing between
6828 // braces, so `{$x}` is the two-character string `$x` — the row
6829 // that makes "the wrapper is the value" more than a tie-break.
6830 BracedWordCase {
6831 source: "set a {$x}\n",
6832 counts: [1, 1, 2, 2],
6833 before: [3, 3],
6834 operands: &["a", "{$x}"],
6835 },
6836 // command_substitution, likewise never an operand itself. Its
6837 // interior is, and stays so: `$q`, `puts` and `w` are
6838 // grandchildren. The nested `{puts w}` is a *script* body and
6839 // sheds under the other half of #1354.
6840 BracedWordCase {
6841 source: "set z {x [if {$q} {puts w}] v}\n",
6842 counts: [4, 5, 5, 5],
6843 before: [8, 8],
6844 operands: &["z", "$q", "puts", "w", "{x [if {$q} {puts w}] v}"],
6845 },
6846 // The same value twice: n2 3 against N2 4, so a row that
6847 // asserted only the vocabulary could not tell the two axes
6848 // apart (#1294).
6849 BracedWordCase {
6850 source: "set a {b c}\nset d {b c}\n",
6851 counts: [1, 2, 3, 4],
6852 before: [5, 8],
6853 operands: &["a", "d", "{b c}"],
6854 },
6855 // The parity pair #1317 named: two spellings of one literal
6856 // value must score alike. They did not before — braced 4 / 4
6857 // against quoted 2 / 2 — which is the spelling sensitivity
6858 // #695, #1312 and #1314 each removed elsewhere.
6859 BracedWordCase {
6860 source: "set a {one two}\n",
6861 counts: [1, 1, 2, 2],
6862 before: [4, 4],
6863 operands: &["a", "{one two}"],
6864 },
6865 BracedWordCase {
6866 source: "set a \"one two\"\n",
6867 counts: [1, 1, 2, 2],
6868 before: [2, 2],
6869 operands: &["a", "\"one two\""],
6870 },
6871 ];
6872
6873 /// The script half of #1354, shared by both dialects: a
6874 /// `braced_word` holding commands is a block whose contents the
6875 /// walk already counts, so it is no longer also an operand
6876 /// spanning the whole block.
6877 ///
6878 /// The three childless rows are the gate, and the reason this is a
6879 /// gate and not a deletion (grammar-dispatch §6). `braced_word` is
6880 /// not only the script kind: it is the value slot of every command
6881 /// the grammar does not special-case, where `lappend l {}` is an
6882 /// empty list whose brace pair is its only carrier. Deleting the
6883 /// kind scored that zero while its `lappend l ""` synonym — the
6884 /// last row, the control — scored one. An empty `proc` body is
6885 /// spelled identically and so also keeps an operand; #1318 tells
6886 /// the two roles apart by the enclosing command, but it bills them
6887 /// alike, so this gate is still what keeps the operand.
6888 ///
6889 /// The rows' *operator* columns moved with #1318 wherever the
6890 /// enclosing command takes a value, which is why the `lappend`
6891 /// rows here read `0` where the `proc` and `if` rows read the
6892 /// block's `{}`. The operand columns are untouched by it — #1318
6893 /// revises the brace and nothing else — so every `before` here
6894 /// still describes #1354 alone.
6895 #[cfg(any(feature = "irules", feature = "tcl"))]
6896 const SCRIPT_BODY_CASES: [BracedWordCase; 9] = [
6897 BracedWordCase {
6898 source: "proc p {} { set b 1 }\n",
6899 counts: [3, 4, 3, 3],
6900 before: [4, 4],
6901 operands: &["p", "b", "1"],
6902 },
6903 BracedWordCase {
6904 source: "proc p {} {}\n",
6905 counts: [2, 3, 2, 2],
6906 before: [2, 2],
6907 operands: &["p", "{}"],
6908 },
6909 // A comment is a named child of the script but not a command,
6910 // and no arm bills it, so a comment-only body scores like the
6911 // empty one above rather than like nothing: the block's whole
6912 // text is its one operand. Gating on "any named child" billed
6913 // it zero — adding a comment to an empty block lowered N2.
6914 BracedWordCase {
6915 source: "proc p {} {\n # only a comment\n}\n",
6916 counts: [2, 3, 2, 2],
6917 before: [2, 2],
6918 operands: &["p", "{\n # only a comment\n}"],
6919 },
6920 BracedWordCase {
6921 source: "if {$q} {\n # noop\n}\n",
6922 counts: [2, 3, 2, 2],
6923 before: [2, 2],
6924 operands: &["$q", "{\n # noop\n}"],
6925 },
6926 // The control: a comment *beside* a command changes nothing, so a
6927 // gate keyed on "contains a comment" would fail this row.
6928 BracedWordCase {
6929 source: "proc p {} {\n # c\n set b 1\n}\n",
6930 counts: [3, 4, 3, 3],
6931 before: [4, 4],
6932 operands: &["p", "b", "1"],
6933 },
6934 // The value-role twin: inside a literal string Tcl performs no
6935 // substitution, so `# x` is not a comment at all, and the word
6936 // is its one operand exactly as `lappend l {}` below is. Its
6937 // operator columns are zero since #1318 — `lappend` takes a
6938 // value, so the brace pair is a quote and not a block.
6939 BracedWordCase {
6940 source: "lappend l {\n # x\n}\n",
6941 counts: [0, 0, 3, 3],
6942 before: [3, 3],
6943 operands: &["lappend", "l", "{\n # x\n}"],
6944 },
6945 // A `braced_word` in *value* position — the #1318 role, where
6946 // the same kind carries a literal list. Its interior words
6947 // counted before #1354 and still do; what #1354 removed is the
6948 // whole-block operand that was double-billing them, and what
6949 // #1318 removed is the `{}` operator (`lappend` takes a value,
6950 // so the brace pair quotes rather than opens).
6951 BracedWordCase {
6952 source: "lappend l {a b}\n",
6953 counts: [0, 0, 4, 4],
6954 before: [5, 5],
6955 operands: &["lappend", "l", "a", "b"],
6956 },
6957 // …and the childless spelling of that value, which sheds
6958 // nothing: the brace pair is the empty list's only carrier.
6959 BracedWordCase {
6960 source: "lappend l {}\nreturn {}\n",
6961 counts: [0, 0, 4, 5],
6962 before: [4, 5],
6963 operands: &["lappend", "l", "return", "{}"],
6964 },
6965 // The control the row above is measured against: the quoted
6966 // spelling of the same empty value, which never depended on
6967 // the arm and must keep scoring one operand. Since #1318 the
6968 // two rows agree on all four columns rather than only on the
6969 // operand pair — an empty list is an empty list however it is
6970 // spelled.
6971 BracedWordCase {
6972 source: "lappend l \"\"\nreturn \"\"\n",
6973 counts: [0, 0, 4, 5],
6974 before: [4, 5],
6975 operands: &["lappend", "l", "return", "\"\""],
6976 },
6977 ];
6978
6979 /// The table must exercise every named child kind the grammar
6980 /// admits inside a braced word, and no other — the other half of
6981 /// the drift marker in `braced_word_shed`, which can only police
6982 /// kinds a fixture actually produces.
6983 #[cfg(any(feature = "irules", feature = "tcl"))]
6984 fn assert_braced_word_children_witnessed(
6985 witnessed: &HashSet<u16>,
6986 kinds: &BracedWordKinds,
6987 dialect: &str,
6988 ) {
6989 let mut got: Vec<u16> = witnessed.iter().copied().collect();
6990 got.sort_unstable();
6991 let mut expected = kinds.children;
6992 expected.sort_unstable();
6993 assert_eq!(
6994 got.as_slice(),
6995 expected.as_slice(),
6996 "{dialect}: the fixtures must witness every child kind \
6997 node-types.json admits inside a braced word",
6998 );
6999 }
7000
7001 /// Regression for #1354 and #1317. `braced_word_simple` is a
7002 /// literal value and `braced_word` a script, and both were
7003 /// operands *beside* the content the walk counts anyway: a braced
7004 /// word was billed once per inner word plus once for itself, and a
7005 /// block once for every command in it plus once for its whole
7006 /// text. `set x {literal here}` scored n2 4 / N2 4 for one value.
7007 /// The name is the invariant either way — the value's content is
7008 /// the literal, the script's is its commands, and each is billed
7009 /// once.
7010 ///
7011 /// Each row's `before` column is re-derived by the loop from the
7012 /// current parse rather than trusted. That derivation is a replica
7013 /// of the pre-#1354 arms and could in principle drift from what
7014 /// they did, so every column was also measured directly against a
7015 /// build of the old getters — `lappend l {a b}` at n2 5 / N2 5,
7016 /// `lappend l {}` at 4 / 5, `proc p {} {}` at 2 / 2 — rather than
7017 /// derived only from the model here.
7018 ///
7019 /// The same walk doubles as the drift marker for the parent-keyed
7020 /// arm: it fails if the grammar ever puts a seventh kind directly
7021 /// inside a braced word, and the union assertion below fails if a
7022 /// bump stops emitting one of the six.
7023 #[cfg(feature = "tcl")]
7024 #[test]
7025 fn tcl_braced_word_bills_its_content_once_1354() {
7026 let mut witnessed = check_braced_word_cases::<TclParser, TclCode>(
7027 &BRACED_WORD_CASES,
7028 "foo.tcl",
7029 &TCL_BRACED_WORD_KINDS,
7030 );
7031 witnessed.extend(check_braced_word_cases::<TclParser, TclCode>(
7032 &SCRIPT_BODY_CASES,
7033 "foo.tcl",
7034 &TCL_BRACED_WORD_KINDS,
7035 ));
7036 assert_braced_word_children_witnessed(&witnessed, &TCL_BRACED_WORD_KINDS, "tcl");
7037 }
7038
7039 /// The iRules twin. The tables are shared, so a fix that reached
7040 /// only `big-code-analysis-ast/src/getter/tcl.rs` fails every row
7041 /// here — the two getters are deliberate clones and #1354 names
7042 /// both.
7043 #[cfg(feature = "irules")]
7044 #[test]
7045 fn irules_braced_word_bills_its_content_once_1354() {
7046 let mut witnessed = check_braced_word_cases::<IrulesParser, IrulesCode>(
7047 &BRACED_WORD_CASES,
7048 "foo.irule",
7049 &IRULES_BRACED_WORD_KINDS,
7050 );
7051 witnessed.extend(check_braced_word_cases::<IrulesParser, IrulesCode>(
7052 &SCRIPT_BODY_CASES,
7053 "foo.irule",
7054 &IRULES_BRACED_WORD_KINDS,
7055 ));
7056 // The `when` handler body is the iRules-only spelling of a
7057 // script body, and the largest instance of the defect: its
7058 // operand text was the entire event handler.
7059 let handlers: [BracedWordCase; 3] = [
7060 BracedWordCase {
7061 source: "when HTTP_REQUEST { set x 1 }\n",
7062 counts: [3, 3, 3, 3],
7063 before: [4, 4],
7064 operands: &["HTTP_REQUEST", "x", "1"],
7065 },
7066 BracedWordCase {
7067 source: "when HTTP_REQUEST {}\n",
7068 counts: [2, 2, 2, 2],
7069 before: [2, 2],
7070 operands: &["HTTP_REQUEST", "{}"],
7071 },
7072 // The comment-only twin of the empty handler above; see the
7073 // shared table for why it scores like it.
7074 BracedWordCase {
7075 source: "when HTTP_REQUEST {\n # only a comment\n}\n",
7076 counts: [2, 2, 2, 2],
7077 before: [2, 2],
7078 operands: &["HTTP_REQUEST", "{\n # only a comment\n}"],
7079 },
7080 ];
7081 witnessed.extend(check_braced_word_cases::<IrulesParser, IrulesCode>(
7082 &handlers,
7083 "foo.irule",
7084 &IRULES_BRACED_WORD_KINDS,
7085 ));
7086 assert_braced_word_children_witnessed(&witnessed, &IRULES_BRACED_WORD_KINDS, "irules");
7087 }
7088
7089 /// One row of the #1318 tables: a fixture and the four Halstead
7090 /// columns plus the operand vocabulary behind them.
7091 ///
7092 /// No `before` column, unlike [`BracedWordCase`]: #1318 is not a
7093 /// guard whose absence a walk can model, it is a *classification*.
7094 /// The pre-fix counts are recorded per row in prose instead, and
7095 /// each was measured against a build without the
7096 /// `get_op_type_with_code` overrides.
7097 struct BracedWordValueCase {
7098 source: &'static str,
7099 /// `[n1, N1, n2, N2]`.
7100 counts: [u64; 4],
7101 operands: &'static [&'static str],
7102 }
7103
7104 /// Runs one #1318 table against one dialect. Both dialects run
7105 /// every shared row, so a fix that reached one getter and not its
7106 /// clone fails here.
7107 #[cfg(any(feature = "irules", feature = "tcl"))]
7108 fn check_braced_word_value_cases<T: crate::MetricSuite>(
7109 cases: &[BracedWordValueCase],
7110 file: &str,
7111 ) {
7112 for case in cases {
7113 assert_halstead_counts::<T>(case.source, file, case.counts, case.source);
7114 assert_ops_operands::<T>(
7115 case.source,
7116 file,
7117 case.operands.len(),
7118 case.operands.to_vec(),
7119 );
7120 }
7121 }
7122
7123 /// The #1318 rows both dialects share. Every independent path
7124 /// through the role test gets a row of its own
7125 /// (`.claude/rules/testing.md`, "give one input to each independent
7126 /// path"): the value default, the script-command list, a modelled
7127 /// construct reached through a `word_list`, the two modelled slots
7128 /// that hold values, an unresolvable command name, and both
7129 /// nesting directions.
7130 ///
7131 /// **Every row asserts the operand columns too, and they are the
7132 /// half that must not move.** #1318 revises one operator and
7133 /// nothing else, so a mutant that suppressed a value word's
7134 /// *contents* — the tidier-looking rule, which collapses an
7135 /// `oo::class create C {…}` body into a single operand — fails
7136 /// here rather than passing as an improvement. #1382 keeps to the
7137 /// same discipline: it withdraws further `{}` operators and moves
7138 /// no operand column in this table.
7139 ///
7140 /// The trailing rows are #1382's. The first four are two *pairs*:
7141 /// a braced value slot beside the bare spelling of the same
7142 /// argument, which must score alike. Before #1382 they did not —
7143 /// the braced spelling billed one extra `{}`. The four after them
7144 /// carry no bare partner: each braces a value with a space in it
7145 /// (`proc {my proc}`, `{a b}`, `{code msg}`), which is the whole
7146 /// reason the braces are there. Their pre-#1382 counts are
7147 /// recorded per row instead.
7148 #[cfg(any(feature = "irules", feature = "tcl"))]
7149 const BRACED_WORD_VALUE_CASES: [BracedWordValueCase; 24] = [
7150 // The rule reaches the opener and *only* the opener. A `;`
7151 // separating two commands is a direct child of the
7152 // `braced_word` — `_terminator` is a hidden rule, so it is
7153 // inlined rather than wrapped — and both dialects classify
7154 // `SEMI` as an operator, so a revision keyed on the parent
7155 // kind alone swallowed it too: this row read n1 0 / N1 0 and
7156 // `halstead.effort` 0.0, against a line that is two commands.
7157 // The pairing with the `eval` row below is the point — same
7158 // brace, same separator, one command name apart.
7159 BracedWordValueCase {
7160 source: "lappend l {puts a ; puts b}\n",
7161 counts: [1, 1, 5, 6],
7162 operands: &["lappend", "l", "puts", "a", "b"],
7163 },
7164 BracedWordValueCase {
7165 source: "eval {puts a ; puts b}\n",
7166 counts: [2, 2, 4, 5],
7167 operands: &["eval", "puts", "a", "b"],
7168 },
7169 // The reported fixture. `lappend` takes a list, so `{a b}`
7170 // quotes rather than opens: no operator. Before #1318 this
7171 // read n1 1 / N1 1 — a `{}` block the line does not contain.
7172 BracedWordValueCase {
7173 source: "lappend l {a b}\n",
7174 counts: [0, 0, 4, 4],
7175 operands: &["lappend", "l", "a", "b"],
7176 },
7177 // The issue's second line, and the one showing the defect
7178 // needs no second argument to appear.
7179 BracedWordValueCase {
7180 source: "puts {c d}\n",
7181 counts: [0, 0, 3, 3],
7182 operands: &["puts", "c", "d"],
7183 },
7184 // The headline: one value, two commands, and until #1318 two
7185 // different operator counts — `set` reported none, `lappend` a
7186 // `{}`. `set` is an operator and `lappend` an operand because
7187 // the grammar models one command and not the other; that
7188 // asymmetry is the grammar's, not this rule's.
7189 BracedWordValueCase {
7190 source: "set x {a b}\nlappend y {a b}\n",
7191 counts: [1, 1, 6, 6],
7192 operands: &["lappend", "x", "y", "{a b}", "a", "b"],
7193 },
7194 // The script side of the same shape, and the reason `LBRACE`
7195 // is gated rather than deleted (grammar-dispatch §6): `eval`
7196 // evaluates its argument, so the block is real and keeps its
7197 // `{}`.
7198 BracedWordValueCase {
7199 source: "eval {puts hi}\n",
7200 counts: [1, 1, 3, 3],
7201 operands: &["eval", "puts", "hi"],
7202 },
7203 // The same command named through the global namespace, which
7204 // is the spelling a `namespace eval` body uses to guarantee the
7205 // core command over a local proc shadowing it. `::eval` *is*
7206 // `eval`, so the two rows must agree on the operator column;
7207 // before the leading `::` was stripped this one fell to the
7208 // value default and lost its block.
7209 BracedWordValueCase {
7210 source: "::eval {puts hi}\n",
7211 counts: [1, 1, 3, 3],
7212 operands: &["::eval", "puts", "hi"],
7213 },
7214 // The control for that strip: only the *leading* qualifier is
7215 // a namespace path to the core command. `ns::eval` is a
7216 // different proc living in `ns`, so it takes the value default
7217 // like any other unrecognised name — a strip of every `::`
7218 // segment would have promoted it to a script.
7219 BracedWordValueCase {
7220 source: "ns::eval {a b}\n",
7221 counts: [0, 0, 3, 3],
7222 operands: &["ns::eval", "a", "b"],
7223 },
7224 // The default for an unrecognised command — a user proc here,
7225 // but equally any package command. Nothing distinguishes it
7226 // from `lappend` structurally, so the row exists to pin which
7227 // way the coin lands. Note the words inside are still
7228 // operands: the rule withdraws the block, never the code.
7229 BracedWordValueCase {
7230 source: "myproc {g h}\n",
7231 counts: [0, 0, 3, 3],
7232 operands: &["myproc", "g", "h"],
7233 },
7234 // `namespace` is the one construct either grammar models that
7235 // still reaches its body through a `word_list`, so it is the
7236 // row that fails if the rule stops at the parent instead of
7237 // reading the grandparent. Its body must stay a script.
7238 BracedWordValueCase {
7239 source: "namespace eval ns {\n set a 1\n}\n",
7240 counts: [3, 3, 4, 4],
7241 operands: &["eval", "ns", "a", "1"],
7242 },
7243 // A literal nested in a literal: neither brace opens a block,
7244 // and both words survive.
7245 BracedWordValueCase {
7246 source: "puts {a {b c}}\n",
7247 counts: [0, 0, 4, 4],
7248 operands: &["puts", "a", "b", "c"],
7249 },
7250 // A `set` inside a literal. The `set` keyword and the `[]`
7251 // substitution are real operators of a command the walk
7252 // descends into, and they stay — the rule reaches exactly the
7253 // one brace whose parent it can classify, never a subtree.
7254 // `{x [foo] v}` is a `braced_word_simple`, so #1354 bills it
7255 // whole and its `[foo]` interior separately, unchanged.
7256 BracedWordValueCase {
7257 source: "lappend l {set a {x [foo] v}}\n",
7258 counts: [2, 2, 5, 5],
7259 operands: &["lappend", "l", "a", "{x [foo] v}", "foo"],
7260 },
7261 // A braced word in the command *name* position. Legal Tcl —
7262 // `{puts} hi` invokes `puts` — and the one generic position
7263 // that is not an argument, so it is answered before the
7264 // command lookup. It must also never reach the `switch`-arm
7265 // rescue, which is what the braced-pattern row in the Tcl
7266 // table checks.
7267 BracedWordValueCase {
7268 source: "{puts} hi\n",
7269 counts: [0, 0, 2, 2],
7270 operands: &["puts", "hi"],
7271 },
7272 // A defaulted `proc` parameter — the other modelled slot that
7273 // holds a value, decided by the parent kind rather than by a
7274 // command name. `{x y}` is data the interpreter assigns, never
7275 // a script it runs, and it billed a fourth `{}` before #1318.
7276 // The three that remain are the parameter list, the parameter
7277 // spec and the body: those two list delimiters group a
7278 // declaration rather than quote a value, the line #1354 drew
7279 // for `Arguments`.
7280 BracedWordValueCase {
7281 source: "proc p {a {b {x y}}} { puts $a }\n",
7282 counts: [2, 4, 7, 7],
7283 operands: &["p", "a", "b", "x", "y", "puts", "$a"],
7284 },
7285 // A literal nested in a script, the direction that must keep
7286 // working: the `proc` body is a block (`{}` twice with the
7287 // parameter list) and the list it appends is not.
7288 BracedWordValueCase {
7289 source: "proc p {} { lappend l {a b} }\n",
7290 counts: [2, 3, 5, 5],
7291 operands: &["p", "lappend", "l", "a", "b"],
7292 },
7293 // A computed command name resolves to nothing, so the value
7294 // default decides. `$c` is the reference's own operand.
7295 BracedWordValueCase {
7296 source: "set c puts\n$c {a b}\n",
7297 counts: [1, 1, 5, 5],
7298 operands: &["c", "puts", "$c", "a", "b"],
7299 },
7300 // #1382's headline, and the pair that names the defect: `after`
7301 // is a script-taking command, but only from its *second*
7302 // argument — the first is a millisecond count. Both spellings
7303 // of that count are one operand and no operator; before #1382
7304 // the braced one billed a `{}` for a block the line does not
7305 // contain, reading N1 2 against the bare form's 1. The `{}`
7306 // both rows do report is the `{puts hi}` script, which is real.
7307 BracedWordValueCase {
7308 source: "after {100} {puts hi}\n",
7309 counts: [1, 1, 4, 4],
7310 operands: &["after", "100", "puts", "hi"],
7311 },
7312 BracedWordValueCase {
7313 source: "after 100 {puts hi}\n",
7314 counts: [1, 1, 4, 4],
7315 operands: &["after", "100", "puts", "hi"],
7316 },
7317 // The same pair for `time script ?count?`, whose value slot is
7318 // the *last* rather than the first — so a rule that hard-coded
7319 // "argument 0 is the value" passes the `after` pair and fails
7320 // this one. The script argument keeps its `{}` in both rows,
7321 // which is what makes the operator column a 1 and not a 0.
7322 //
7323 // The *bare* row is the half that catches it, which is not the
7324 // half it looks like. Measured by giving `time` `after`'s slot
7325 // rule: `time {puts hi} 3` reads n1 0 / N1 0, its one block
7326 // demoted to a value — while the braced row below is unmoved
7327 // at [1, 1, 4, 4], because `{puts hi}` loses the `{}` that
7328 // `{3}` gains and the operand texts are the same either way.
7329 // Do not trim the bare spelling as redundant with the braced
7330 // one; here it is the discriminating row of the two.
7331 BracedWordValueCase {
7332 source: "time {puts hi} {3}\n",
7333 counts: [1, 1, 4, 4],
7334 operands: &["time", "puts", "hi", "3"],
7335 },
7336 BracedWordValueCase {
7337 source: "time {puts hi} 3\n",
7338 counts: [1, 1, 4, 4],
7339 operands: &["time", "puts", "hi", "3"],
7340 },
7341 // A braced `proc` *name*, the one value slot the grammar names
7342 // as a field rather than by position. `{my proc}` is an
7343 // identifier containing a space, not a body, and billed a
7344 // fourth `{}` before #1382. The three operators left are the
7345 // `proc` keyword and the two braces of the parameter list and
7346 // the body — and note the interior words survive, `proc` being
7347 // an operand here and an operator one column over.
7348 BracedWordValueCase {
7349 source: "proc {my proc} {} {}\n",
7350 counts: [2, 3, 3, 3],
7351 operands: &["my", "proc", "{}"],
7352 },
7353 // The two `namespace` subcommands that take values throughout,
7354 // both of them Tcl 8.6 standard-library spellings. Their braced
7355 // argument is a pattern list and a dictionary; neither is
7356 // evaluated, and each billed a `{}` before #1382 — n1 2 / N1 2
7357 // against the 1 / 1 here. The `namespace` keyword is the
7358 // operator that remains, which is what keeps these two rows
7359 // from reading `effort` 0.0.
7360 BracedWordValueCase {
7361 source: "namespace export {a b}\n",
7362 counts: [1, 1, 3, 3],
7363 operands: &["export", "a", "b"],
7364 },
7365 BracedWordValueCase {
7366 source: "namespace ensemble create -map {a b}\n",
7367 counts: [1, 1, 5, 5],
7368 operands: &["ensemble", "create", "-map", "a", "b"],
7369 },
7370 // A `trap` clause written outside a `try`, which is how both
7371 // grammars reach the generic-command path for it. This is the
7372 // `ScriptSlots::LastOf` row — the pattern and the variable list
7373 // are values and the handler is the script — and the only one
7374 // whose slot is counted from the *end* of the list, so a rule
7375 // that read `Only(2)` from the front would need the arity to
7376 // pass it. Before #1382 all three braces billed: N1 3.
7377 BracedWordValueCase {
7378 source: "trap {POSIX} {code msg} {puts hi}\n",
7379 counts: [1, 1, 6, 6],
7380 operands: &["trap", "POSIX", "code", "msg", "puts", "hi"],
7381 },
7382 ];
7383
7384 /// Regression for #1318. `braced_word` carries both a block and a
7385 /// plain literal, and the grammars spell the two identically, so
7386 /// #1314's kind-scoped guard reached only the value slots they
7387 /// special-case: `set x {a b}` reported no operator while
7388 /// `lappend x {a b}` reported a `{}` for a block the line does not
7389 /// contain. The role is recognised out-of-band, by the enclosing
7390 /// command's leading word (grammar-dispatch §9).
7391 ///
7392 /// The Tcl-only rows are the constructs this grammar models with
7393 /// no node of its own (#467, #1264) — `switch` and `for` — plus the
7394 /// braced `switch` *pattern* that must not be mistaken for an arm
7395 /// body, in both the one-arm-per-line and the one-line layouts.
7396 /// Their iRules counterparts are dedicated nodes and are covered by
7397 /// the sibling test.
7398 #[cfg(feature = "tcl")]
7399 #[test]
7400 fn tcl_braced_word_role_follows_the_enclosing_command_1318() {
7401 check_braced_word_value_cases::<TclParser>(&BRACED_WORD_VALUE_CASES, "foo.tcl");
7402 let tcl_only: [BracedWordValueCase; 8] = [
7403 // `switch` arm bodies. The grammar flattens `pat body pat
7404 // body` into a `command` named after the first pattern, so
7405 // the bodies read as arguments of a command called `a` —
7406 // the shape the value default would call a literal. The
7407 // arm list's own `switch` name is what rescues them, and
7408 // `puts` occurring in both bodies is what separates n2
7409 // from N2.
7410 BracedWordValueCase {
7411 source: "switch $v { a { puts A } b { puts B } }\n",
7412 counts: [1, 3, 7, 8],
7413 operands: &["switch", "$v", "a", "b", "puts", "A", "B"],
7414 },
7415 // …and the limit of that rescue. A braced *pattern* is the
7416 // arm command's `name`, not an argument, so it is a
7417 // literal — `switch -regexp $v { {^a.*b$} {…} }` is
7418 // idiomatic and its regex must not be wrapped in a block.
7419 // Rescuing the whole arm command would read N1 4 here.
7420 // The leading `-exact` / `--` options are `simple_word`
7421 // operands of the *`switch`* command and shift nothing:
7422 // the arm list is found as the sole `braced_word` argument,
7423 // not by index (grammar-dispatch §9).
7424 BracedWordValueCase {
7425 source: "switch -exact -- $v {\n {a b} { puts X }\n default { puts Y }\n}\n",
7426 counts: [1, 3, 10, 11],
7427 operands: &[
7428 "switch", "-exact", "--", "$v", "a", "b", "puts", "X", "default", "Y",
7429 ],
7430 },
7431 // The same arms on one line. Only the *first* pattern is
7432 // the arm command's name; `{^b}` is its third argument, and
7433 // the name test alone rescued it as a body — N1 4 here,
7434 // against N1 3 for the identical arms written one per line.
7435 // The arm list is `pattern body pattern body …` by Tcl's
7436 // own definition, so the arguments alternate and the
7437 // even-indexed ones are the bodies.
7438 BracedWordValueCase {
7439 source: "switch -regexp $v { {^a} { puts A } {^b} { puts B } }\n",
7440 counts: [1, 3, 8, 9],
7441 operands: &["switch", "-regexp", "$v", "^a", "puts", "A", "^b", "B"],
7442 },
7443 // The words that can interpose between a pattern and its
7444 // body take one slot each and leave the parity intact: a
7445 // `-` fall-through body and a `default` pattern are both
7446 // `simple_word`s. The two braced bodies are at indexes 2
7447 // and 4 of the run and both keep their `{}`.
7448 BracedWordValueCase {
7449 source: "switch $v { a - b { puts AB } default { puts D } }\n",
7450 counts: [1, 3, 9, 10],
7451 operands: &["switch", "$v", "a", "-", "b", "puts", "AB", "default", "D"],
7452 },
7453 // `for` has four braced arguments and all four are
7454 // evaluated, so all four keep their braces. `i` appears as
7455 // the `set` target and again inside `incr`, and `$i` in
7456 // the condition and the body, which is what separates
7457 // n2 = 8 from N2 = 10. `<` is an *operand* here rather
7458 // than a comparison operator: with no `for` rule there is
7459 // no `expr` slot, so the condition parses as a command and
7460 // its comparison never surfaces as a token — the same
7461 // grammar limitation #1264 recorded for ABC.
7462 BracedWordValueCase {
7463 source: "for {set i 0} {$i < 3} {incr i} { puts $i }\n",
7464 counts: [2, 5, 8, 10],
7465 operands: &["for", "i", "0", "$i", "<", "3", "incr", "puts"],
7466 },
7467 // #1382's guard rows: an arm *pattern* spelled like a
7468 // command whose value slots the rule now withdraws. An arm
7469 // list parses as commands, so the pattern becomes a real
7470 // command name and the arm *body* lands in one of that
7471 // command's value slots — where only `Getter::is_switch_arm`
7472 // keeps it a block. Both rows are `switch` bodies that must
7473 // keep their `{}`, and both were measured with the guard
7474 // short-circuited to `false`.
7475 //
7476 // This one is the positional half: `after`'s slot 0 is the
7477 // millisecond count, so without the guard `{ puts B }` reads
7478 // as that value and the row drops to N1 1.
7479 BracedWordValueCase {
7480 source: "switch $v { after { puts B } }\n",
7481 counts: [1, 2, 5, 5],
7482 operands: &["switch", "$v", "after", "puts", "B"],
7483 },
7484 // …and this is the field-keyed half. `proc` needs three
7485 // words, so the arm patterns must be written one per line
7486 // for the grammar to build a `procedure` at all — on one
7487 // line it recovers into an `ERROR` and no rule fires. Here
7488 // it swallows two arms, taking `{ puts A }` as its `name`
7489 // field and `{ puts B }` as its body; without the guard the
7490 // name's brace goes and the row reads N1 5. The `namespace`
7491 // arm is the third spelling of the same hazard and is not
7492 // discriminating: its subcommand slot holds the braced body
7493 // itself rather than a `simple_word`, so no layout resolves
7494 // and the construct-wide script answer stands either way.
7495 // `proc` and `namespace` are operators here and `after` an
7496 // operand, which is the three patterns' whole difference.
7497 BracedWordValueCase {
7498 source: "switch $v {\n proc { puts A }\n after { puts B }\n namespace { puts C }\n}\n",
7499 counts: [3, 6, 7, 9],
7500 operands: &["switch", "$v", "after", "puts", "A", "B", "C"],
7501 },
7502 // The arity guard on `ScriptSlots::LastOf`, Tcl-only
7503 // because the iRules grammar models `trap_handler` and so
7504 // never reaches the generic-command path (#1382 review).
7505 // Tcl models neither `on` nor `trap`, so the whole tail
7506 // after the `try` body parses as *one* `trap` command with
7507 // five arguments — `{p} {v} {puts b} finally {puts c}`.
7508 // Reading "the script is the last argument" off that names
7509 // the `finally` body and calls `{puts b}` a value, which
7510 // dropped the `{}` of a block the line does contain and
7511 // read N1 3. All five braces are blocks here, which is the
7512 // answer a list the signature cannot explain has to get.
7513 // The `trap` row in the shared table is the same clause at
7514 // its documented arity, where two of the three braces
7515 // really are values — so a guard stuck on "always script"
7516 // fails there and one stuck on "always last" fails here.
7517 BracedWordValueCase {
7518 source: "try {puts a} trap {p} {v} {puts b} finally {puts c}\n",
7519 counts: [2, 6, 8, 10],
7520 operands: &["a", "b", "c", "finally", "p", "puts", "trap", "v"],
7521 },
7522 ];
7523 check_braced_word_value_cases::<TclParser>(&tcl_only, "foo.tcl");
7524 }
7525
7526 /// The iRules twin of the test above. The shared table runs
7527 /// unchanged — the two getters are deliberate clones and #1318
7528 /// names both — and the dialect rows cover the handler bodies,
7529 /// which have no Tcl spelling.
7530 #[cfg(feature = "irules")]
7531 #[test]
7532 fn irules_braced_word_role_follows_the_enclosing_command_1318() {
7533 check_braced_word_value_cases::<IrulesParser>(&BRACED_WORD_VALUE_CASES, "foo.irule");
7534 let irules_only: [BracedWordValueCase; 4] = [
7535 // A `when` handler body is a modelled slot, so the literal
7536 // inside it is classified independently of the block that
7537 // holds it.
7538 BracedWordValueCase {
7539 source: "when HTTP_REQUEST {\n lappend b {x y}\n}\n",
7540 counts: [2, 2, 5, 5],
7541 operands: &["HTTP_REQUEST", "lappend", "b", "x", "y"],
7542 },
7543 // This grammar models `switch` with `switch_arm` children,
7544 // so the arm body fills a modelled slot and never reaches
7545 // the command-name list the Tcl rows need.
7546 BracedWordValueCase {
7547 source: "when HTTP_REQUEST {\n switch $v { a { pool p1 } }\n}\n",
7548 counts: [3, 5, 5, 5],
7549 operands: &["HTTP_REQUEST", "$v", "a", "pool", "p1"],
7550 },
7551 // `on` and `trap` are why the command list carries two
7552 // words no *Tcl* command spells. The grammar emits
7553 // `on_handler` / `trap_handler` only under `try` — pinned
7554 // by `irules_try_handler_kinds_appear_only_under_try` — so
7555 // a statement-level handler is a generic command, and
7556 // without the list entry its body would lose the `{}` that
7557 // the second occurrence here counts.
7558 BracedWordValueCase {
7559 source: "when RULE_INIT { set x 1 }\non error { log local0. oops }\n",
7560 counts: [3, 4, 8, 8],
7561 operands: &[
7562 "RULE_INIT",
7563 "x",
7564 "1",
7565 "on",
7566 "error",
7567 "log",
7568 "local0.",
7569 "oops",
7570 ],
7571 },
7572 // #1382 inside a modelled handler slot: the `when` body is
7573 // a block and keeps its `{}`, and the `after` delay nested
7574 // in it is a value and does not. Before #1382 this read
7575 // N1 4. The shared table's `after` pair already runs for
7576 // this dialect; what this row adds is that the rule reaches
7577 // a command nested inside a handler, where the `{}` billed
7578 // for the handler could otherwise mask a withdrawal.
7579 BracedWordValueCase {
7580 source: "when HTTP_REQUEST {\n after {100} {puts hi}\n}\n",
7581 counts: [2, 3, 5, 5],
7582 operands: &["HTTP_REQUEST", "after", "100", "puts", "hi"],
7583 },
7584 ];
7585 check_braced_word_value_cases::<IrulesParser>(&irules_only, "foo.irule");
7586 }
7587
7588 #[cfg(feature = "php")]
7589 #[test]
7590 fn php_operators_and_operands() {
7591 check_metrics::<PhpParser>(
7592 "<?php
7593 function avg(int $a, int $b, int $c): int {
7594 return ($a + $b + $c) / 3;
7595 }",
7596 "foo.php",
7597 |metric| {
7598 // After #695 only the opening delimiters count: `()` and
7599 // `{}` fold to one operator each per balanced pair, so the
7600 // former `)`/`}` closers no longer inflate n1/N1 (was
7601 // 11 unique / 15 total).
7602 //
7603 // Operands after #1293, tallied by `get_id` (source bytes):
7604 // `avg` × 1, `int` × 4 (the `primitive_type` wrapper at
7605 // all four type positions — its `int` keyword child is
7606 // suppressed under it), `$a` / `$b` / `$c` × 2 each,
7607 // `3` × 1 ⇒ n2 = 6, N2 = 12. Between #1259 and #1293 the
7608 // keyword leaf doubled the type count ⇒ 6 / 16; before
7609 // #1259 each `$v` also contributed its sigil-less `name`
7610 // leaf ⇒ 9 / 22.
7611 assert_eq!(metric.halstead.unique_operators(), 9);
7612 assert_eq!(metric.halstead.total_operators(), 12);
7613 assert_eq!(metric.halstead.unique_operands(), 6);
7614 assert_eq!(metric.halstead.total_operands(), 12);
7615 insta::assert_json_snapshot!(metric.halstead);
7616 },
7617 );
7618 }
7619
7620 #[cfg(feature = "php")]
7621 #[test]
7622 fn php_simple_function() {
7623 check_metrics::<PhpParser>(
7624 "<?php
7625 function inc(int $x): int { return $x + 1; }",
7626 "foo.php",
7627 |metric| {
7628 // After #695 only opening delimiters count: the `)`/`}`
7629 // closers no longer add operators (was 9 unique / 9 total).
7630 //
7631 // Operands after #1293: `inc` × 1, `int` × 2 (the
7632 // `primitive_type` wrapper at both type positions, its
7633 // `int` keyword child suppressed under it), `$x` × 2,
7634 // `1` × 1 ⇒ n2 = 4, N2 = 6. Between #1259 and #1293 the
7635 // keyword leaf doubled the type count ⇒ 4 / 8; before
7636 // #1259 `$x` also contributed its `x` leaf twice ⇒ 5 / 10.
7637 assert_eq!(metric.halstead.unique_operators(), 7);
7638 assert_eq!(metric.halstead.total_operators(), 7);
7639 assert_eq!(metric.halstead.unique_operands(), 4);
7640 assert_eq!(metric.halstead.total_operands(), 6);
7641 insta::assert_json_snapshot!(metric.halstead);
7642 },
7643 );
7644 }
7645
7646 #[cfg(feature = "php")]
7647 #[test]
7648 fn php_variable_reference_counts_once() {
7649 // Regression: issue #1259. `$x` parses as a `variable_name`
7650 // wrapping a `name` leaf, and both kinds were in the operand
7651 // arm — so every variable reference contributed twice to N2 and
7652 // planted a sigil-less twin (`x` beside `$x`) in the n2
7653 // vocabulary. Since `$var` is the most common token class in
7654 // PHP, that roughly doubled N2 for real files.
7655 //
7656 // Source: the issue's reproducer plus a re-reference of `$a` and
7657 // `$b`, so N2 exceeds n2 and the assertions can tell "counted
7658 // once per occurrence" from "deduplicated into the vocabulary".
7659 // <?php $a = null; $b = true; $c = NULL; $a = $b;
7660 //
7661 // Operands by text key: `$a` × 2, `$b` × 2, `$c`, `null`, `true`,
7662 // `NULL` ⇒ n2 = 6, N2 = 8. Before the fix the `a` / `b` / `c`
7663 // leaves added 3 unique and 5 occurrences ⇒ 9 / 13.
7664 check_metrics::<PhpParser>(
7665 "<?php\n$a = null;\n$b = true;\n$c = NULL;\n$a = $b;\n",
7666 "foo.php",
7667 |metric| {
7668 assert_eq!(metric.halstead.unique_operands(), 6);
7669 assert_eq!(metric.halstead.total_operands(), 8);
7670 },
7671 );
7672 }
7673
7674 #[cfg(feature = "php")]
7675 #[test]
7676 fn php_dynamic_variable_name_counts_once_at_any_depth() {
7677 // Regression: issue #1259. Variable-variable syntax nests the
7678 // wrappers, so the double count compounds: `$$a` is a
7679 // `dynamic_variable_name` → `variable_name` → `name` chain that
7680 // scored 3 for one reference, and `$$$b` scored 4. Only the
7681 // outermost wrapper may count.
7682 //
7683 // Source: <?php $$a = 1; $$$b = 2; ${$c} = 3; $$a = 4;
7684 // The trailing re-assignment repeats `$$a` so N2 exceeds n2 and
7685 // the assertions can tell "counted once per occurrence" from
7686 // "deduplicated into the vocabulary".
7687 //
7688 // Operands: `$$a` × 2, `$$$b`, `${$c}`, `1`, `2`, `3`, `4`
7689 // ⇒ n2 = 7, N2 = 8. Before the fix: 14 / 17 (measured), each
7690 // target contributing its whole nesting chain — `$$a` → `$a` →
7691 // `a` is 3 (twice over), `$$$b` → `$$b` → `$b` → `b` is 4, and
7692 // `${$c}` → `$c` → `c` is 3, plus the four integers.
7693 check_metrics::<PhpParser>(
7694 "<?php $$a = 1; $$$b = 2; ${$c} = 3; $$a = 4;",
7695 "foo.php",
7696 |metric| {
7697 assert_eq!(metric.halstead.unique_operands(), 7);
7698 assert_eq!(metric.halstead.total_operands(), 8);
7699 },
7700 );
7701 }
7702
7703 #[cfg(feature = "php")]
7704 #[test]
7705 fn php_dynamic_variable_name_guard_is_parent_scoped() {
7706 // Companion to the two tests above (#1259): the guards fire on
7707 // the *parent* kind, never on the kind alone, so the two
7708 // positions where a nested node is a reference in its own right
7709 // keep counting.
7710 //
7711 // Source: <?php $y = "brace ${z} end"; $s = ${$a . 'b'};
7712 //
7713 // `"${z}"` is a `dynamic_variable_name` whose `name` child is
7714 // suppressed (the wrapper `${z}` carries the reference), while
7715 // `${$a . 'b'}` reaches its `$a` through a `binary_expression`,
7716 // so that `variable_name`'s parent is not a
7717 // `dynamic_variable_name` and it counts normally.
7718 //
7719 // Operands: `$y`, `${z}`, `$s`, `${$a . 'b'}`, `$a`, `'b'` — one
7720 // each ⇒ n2 = 6, N2 = 6. A guard written as a blanket kind
7721 // exclusion instead of a parent check would drop `$a` ⇒ 5 / 5.
7722 check_metrics::<PhpParser>(
7723 "<?php $y = \"brace ${z} end\"; $s = ${$a . 'b'};",
7724 "foo.php",
7725 |metric| {
7726 assert_eq!(metric.halstead.unique_operands(), 6);
7727 assert_eq!(metric.halstead.total_operands(), 6);
7728 },
7729 );
7730 }
7731
7732 #[cfg(feature = "php")]
7733 #[test]
7734 fn php_type_wrappers_count_the_type_once() {
7735 // Regression: issue #1293. A parameter type nests wrapper nodes
7736 // whose text spans the node below them — `primitive_type` around
7737 // the `int` keyword token, `named_type` around a `name`,
7738 // `optional_type` around either — and every level was in the
7739 // operand arm, so `int` scored 2 and `?int` scored 3.
7740 //
7741 // Source: the issue's first reproducer.
7742 // <?php
7743 // function f(int $a, bool $b, float $c, string $d, array $e,
7744 // Foo $g): ?int { return 0; }
7745 //
7746 // Operands by text key: `f`, the five `primitive_type` parameter
7747 // types, `Foo` (the `name` under its `named_type`), `$a`..`$g`,
7748 // the return `int`, and `0`. `int` occurs twice (parameter and
7749 // return) ⇒ n2 = 14, N2 = 15. Before the fix: 15 / 23 — the
7750 // extra vocabulary entry being `?int`, which the `?` operator
7751 // already accounts for.
7752 check_metrics::<PhpParser>(
7753 "<?php\nfunction f(int $a, bool $b, float $c, string $d, \
7754 array $e, Foo $g): ?int { return 0; }\n",
7755 "foo.php",
7756 |metric| {
7757 assert_eq!(metric.halstead.unique_operands(), 14);
7758 assert_eq!(metric.halstead.total_operands(), 15);
7759 },
7760 );
7761 }
7762
7763 #[cfg(feature = "php")]
7764 #[test]
7765 fn php_qualified_name_counts_its_components_once() {
7766 // Regression: issue #1293. `Foo\Bar\Baz` parses as
7767 // `qualified_name` → `namespace_name` → `name` × N, and all
7768 // three kinds were operands, so one three-part path scored 5 and
7769 // planted `Foo`, `Foo\Bar` and `Foo\Bar\Baz` in the vocabulary.
7770 // The components carry the operand and `\` stays an operator,
7771 // matching how PHP's own `::` and `->` already read here.
7772 //
7773 // Source: the issue's second reproducer.
7774 // <?php
7775 // namespace App\Sub;
7776 // use Foo\Bar\Baz;
7777 // $o = new \Vendor\Pkg\Thing();
7778 //
7779 // Operands: `App`, `Sub`, `Foo`, `Bar`, `Baz`, `$o`, `Vendor`,
7780 // `Pkg`, `Thing` — one each ⇒ n2 = 9, N2 = 9. Before the fix:
7781 // 14 / 14.
7782 check_metrics::<PhpParser>(
7783 "<?php\nnamespace App\\Sub;\nuse Foo\\Bar\\Baz;\n\
7784 $o = new \\Vendor\\Pkg\\Thing();\n",
7785 "foo.php",
7786 |metric| {
7787 assert_eq!(metric.halstead.unique_operands(), 9);
7788 assert_eq!(metric.halstead.total_operands(), 9);
7789 },
7790 );
7791 }
7792
7793 #[cfg(feature = "php")]
7794 #[test]
7795 fn php_nested_type_wrappers_count_once_at_any_depth() {
7796 // Companion to the two tests above (#1293): the type and
7797 // qualified-name wrappers compose, so a single annotation can
7798 // stack five levels — `?A\B` is `optional_type` → `named_type` →
7799 // `qualified_name` → `namespace_name` → `name`, which scored 6
7800 // operands for two identifiers. `union_type` and
7801 // `intersection_type` stack the same way over their members;
7802 // their `|` and `&` are already operators.
7803 //
7804 // Source:
7805 // <?php function k(?A\B $p, int|string $q, C&D $r): ?A\B
7806 // { return 0; }
7807 // The return type repeats `?A\B` so N2 exceeds n2 and the
7808 // assertions can tell "counted once per occurrence" from
7809 // "deduplicated into the vocabulary".
7810 //
7811 // Operands: `k`, `A` × 2, `B` × 2, `$p`, `int`, `string`, `$q`,
7812 // `C`, `D`, `$r`, `0` ⇒ n2 = 11, N2 = 13.
7813 check_metrics::<PhpParser>(
7814 "<?php function k(?A\\B $p, int|string $q, C&D $r): ?A\\B { return 0; }",
7815 "foo.php",
7816 |metric| {
7817 assert_eq!(metric.halstead.unique_operands(), 11);
7818 assert_eq!(metric.halstead.total_operands(), 13);
7819 },
7820 );
7821 }
7822
7823 #[cfg(feature = "php")]
7824 #[test]
7825 fn php_childless_primitive_types_still_count() {
7826 // Guards the direction of the #1293 fix for `primitive_type`,
7827 // where — unlike the qualified-name wrappers — the *wrapper*
7828 // carries the operand and the keyword leaf is suppressed. The
7829 // grammar emits no token node under `primitive_type` for
7830 // `callable`, `iterable`, `mixed`, `void`, `false` or `true`
7831 // (verified with `bca dump`), so the other direction would score
7832 // those six types zero — grammar-dispatch §6.
7833 //
7834 // Source:
7835 // <?php function q(callable $a, iterable $b, mixed $c,
7836 // false $d, true $e): void { }
7837 //
7838 // Operands: `q`, `callable`, `iterable`, `mixed`, `false`,
7839 // `true`, `void`, `$a`..`$e` ⇒ n2 = 12, N2 = 12. Dropping
7840 // `PrimitiveType` from the operand arm instead of gating its
7841 // leaf gives 6 / 6.
7842 check_metrics::<PhpParser>(
7843 "<?php function q(callable $a, iterable $b, mixed $c, \
7844 false $d, true $e): void { }",
7845 "foo.php",
7846 |metric| {
7847 assert_eq!(metric.halstead.unique_operands(), 12);
7848 assert_eq!(metric.halstead.total_operands(), 12);
7849 },
7850 );
7851 }
7852
7853 #[cfg(feature = "php")]
7854 #[test]
7855 fn php_primitive_type_keyword_guard_is_parent_scoped() {
7856 // Companion to the test above (#1293): the keyword suppression
7857 // fires on the *parent* kind, never on the kind alone.
7858 // `array` is also the head token of an `array(…)` literal, where
7859 // it is the construct's only operand and must keep counting; a
7860 // `(int)` / `(string)` cast is a childless `cast_type` that
7861 // never reaches the guard at all.
7862 //
7863 // Source: <?php $x = array(1, 2); $y = (int) $x; $z = (string) $x;
7864 //
7865 // Operands: `$x` × 3, `array`, `1`, `2`, `$y`, `int`, `$z`,
7866 // `string` ⇒ n2 = 8, N2 = 10. A blanket kind exclusion instead
7867 // of a parent check would drop the `array` head ⇒ 7 / 9.
7868 check_metrics::<PhpParser>(
7869 "<?php $x = array(1, 2); $y = (int) $x; $z = (string) $x;",
7870 "foo.php",
7871 |metric| {
7872 assert_eq!(metric.halstead.unique_operands(), 8);
7873 assert_eq!(metric.halstead.total_operands(), 10);
7874 },
7875 );
7876 }
7877
7878 #[cfg(feature = "php")]
7879 #[test]
7880 fn php_encapsed_string_interpolation_no_double_count() {
7881 // Regression: issue #184. A PHP `"Hello $name!"` used to be
7882 // classified as a Halstead operand (the wrapping
7883 // `encapsed_string`) AND have its inner `variable_name`
7884 // (`$name`) plus the inner `name` token classified as
7885 // operands too. With the fix, the wrapping literal drops to
7886 // `Unknown` when it carries any `$var` / `${name}` / `{$expr}`
7887 // child, so `$name` is counted exactly once at each text
7888 // occurrence.
7889 //
7890 // Source:
7891 // <?php $name = "world"; echo "Hello $name!";
7892 //
7893 // Inert operand: `"world"` (no interpolation, still operand).
7894 // Operands by text key (`get_id` keys by source bytes):
7895 // `$name` × 2 (assignment LHS and `$name` inside the
7896 // interpolated string), `"world"` × 1.
7897 // u_operands = 2, N2 = 3.
7898 // Without the #184 fix the wrapping `"Hello $name!"` would also
7899 // count → 3 / 4. This test additionally pinned the *inner* `name`
7900 // leaf of each `variable_name` (a further 2 occurrences, 1 unique
7901 // ⇒ the historical 3 / 5) until #1259 recognised that as the same
7902 // double count one level down.
7903 check_metrics::<PhpParser>(
7904 "<?php $name = \"world\"; echo \"Hello $name!\";",
7905 "foo.php",
7906 |metric| {
7907 assert_eq!(metric.halstead.unique_operands(), 2);
7908 assert_eq!(metric.halstead.total_operands(), 3);
7909 },
7910 );
7911 }
7912
7913 #[cfg(feature = "php")]
7914 #[test]
7915 fn php_encapsed_string_no_interpolation_still_operand() {
7916 // The fix for #184 only drops `EncapsedString`/`Heredoc` from
7917 // the operand arm when interpolation is present. An inert
7918 // double-quoted string must still count as exactly one
7919 // operand, identical to the single-quoted equivalent.
7920 //
7921 // Source: `<?php echo "Hello world!";`
7922 // Operands: `"Hello world!"` × 1 → u_operands = 1, N2 = 1.
7923 check_metrics::<PhpParser>("<?php echo \"Hello world!\";", "foo.php", |metric| {
7924 assert_eq!(metric.halstead.unique_operands(), 1);
7925 assert_eq!(metric.halstead.total_operands(), 1);
7926 });
7927 }
7928
7929 #[cfg(feature = "php")]
7930 #[test]
7931 fn php_heredoc_interpolation_no_double_count() {
7932 // Regression: issue #184. A PHP heredoc whose body
7933 // interpolates `$name` previously counted both the wrapping
7934 // `heredoc` node and the inner `$name` as operands; the fix
7935 // drops the wrapper when its `heredoc_body` carries any
7936 // interpolation child.
7937 //
7938 // Source:
7939 // <?php $name = "x"; echo <<<EOT
7940 // hi $name
7941 // EOT;
7942 //
7943 // Operands by text key: `$name` × 2, `"x"` × 1 (inert encapsed
7944 // string, still an operand). With the fix u_operands = 2,
7945 // N2 = 3. Without it the wrapping heredoc text would add one
7946 // more unique operand. The sigil-less `name` leaf inside each
7947 // `variable_name` was counted too until #1259.
7948 check_metrics::<PhpParser>(
7949 "<?php $name = \"x\"; echo <<<EOT\nhi $name\nEOT;\n",
7950 "foo.php",
7951 |metric| {
7952 assert_eq!(metric.halstead.unique_operands(), 2);
7953 assert_eq!(metric.halstead.total_operands(), 3);
7954 },
7955 );
7956 }
7957
7958 #[cfg(feature = "php")]
7959 #[test]
7960 fn php_nowdoc_unaffected() {
7961 // `Nowdoc` (single-quoted heredoc) never interpolates and is
7962 // never matched by `php_string_has_interpolation`. It must
7963 // continue counting as exactly one operand regardless of the
7964 // text inside, mirroring single-quoted `String`.
7965 //
7966 // Source:
7967 // <?php echo <<<'EOT'
7968 // plain $name not interpolated
7969 // EOT;
7970 //
7971 // Operands: the nowdoc literal × 1 → u_operands = 1, N2 = 1.
7972 check_metrics::<PhpParser>(
7973 "<?php echo <<<'EOT'\nplain $name not interpolated\nEOT;\n",
7974 "foo.php",
7975 |metric| {
7976 assert_eq!(metric.halstead.unique_operands(), 1);
7977 assert_eq!(metric.halstead.total_operands(), 1);
7978 },
7979 );
7980 }
7981
7982 #[cfg(feature = "php")]
7983 #[test]
7984 fn php_encapsed_string_bare_member_access_no_double_count() {
7985 // Regression: issue #184 follow-up. The PHP grammar allows
7986 // bare `$obj->prop` interpolation inside `"…"` without
7987 // surrounding `{ … }`; tree-sitter-php emits this as a
7988 // direct `member_access_expression` child of
7989 // `encapsed_string` (kind_id 329 in the current grammar).
7990 // The wrapper must drop to `Unknown` for that form too —
7991 // otherwise the inner `$obj` and `prop` `name` tokens are
7992 // walked as operands while the wrapper also counts,
7993 // double-counting `N2`.
7994 //
7995 // Source:
7996 // <?php $obj = new stdClass; $obj->prop = "x"; echo "Hi $obj->prop!";
7997 //
7998 // Operands tallied by `get_id` (keyed on source bytes):
7999 // `$obj` × 3 (LHS assignment, member-access target,
8000 // inside the interpolated string)
8001 // `prop` (name) × 2 (member-access RHS twice — a bare `name`
8002 // outside any `variable_name`, so #1259's
8003 // guard leaves it an operand)
8004 // `stdClass` × 1
8005 // `"x"` × 1
8006 // ⇒ u_operands = 4, N2 = 7.
8007 // With the bug the wrapping `"Hi $obj->prop!"` text adds one
8008 // more unique operand and one more occurrence ⇒ 5 / 8.
8009 check_metrics::<PhpParser>(
8010 "<?php $obj = new stdClass; $obj->prop = \"x\"; echo \"Hi $obj->prop!\";",
8011 "foo.php",
8012 |metric| {
8013 assert_eq!(metric.halstead.unique_operands(), 4);
8014 assert_eq!(metric.halstead.total_operands(), 7);
8015 },
8016 );
8017 }
8018
8019 #[cfg(feature = "php")]
8020 #[test]
8021 fn php_encapsed_string_bare_subscript_no_double_count() {
8022 // Regression: issue #184 follow-up. Bare `$arr[0]` inside
8023 // `"…"` produces a `subscript_expression` child of
8024 // `encapsed_string` (kind_id 351). The wrapper must drop to
8025 // `Unknown` for that form.
8026 //
8027 // Source:
8028 // <?php $arr = [1]; echo "Hi $arr[0]!";
8029 //
8030 // Operands tallied by `get_id`:
8031 // `$arr` × 2, `1` × 1, `0` × 1.
8032 // ⇒ u_operands = 3, N2 = 4.
8033 // With the bug the wrapping `"Hi $arr[0]!"` text adds 1 / 1.
8034 // The inner `arr` leaf of each `variable_name` added a further
8035 // 1 / 2 until #1259.
8036 check_metrics::<PhpParser>(
8037 "<?php $arr = [1]; echo \"Hi $arr[0]!\";",
8038 "foo.php",
8039 |metric| {
8040 assert_eq!(metric.halstead.unique_operands(), 3);
8041 assert_eq!(metric.halstead.total_operands(), 4);
8042 },
8043 );
8044 }
8045
8046 #[cfg(feature = "php")]
8047 #[test]
8048 fn php_shell_command_expression_inert_is_operand() {
8049 // Regression: issue #288. Backtick command literals (PHP's
8050 // `shell_command_expression`) were filtered as strings by
8051 // `Checker::is_string` and `Alterator::alterate`, but never
8052 // classified as Halstead operands — so they contributed
8053 // nothing to N2 / eta2. An inert backtick literal must now
8054 // count as exactly one operand, matching `EncapsedString`
8055 // and `Heredoc`.
8056 //
8057 // Source: `<?php $out = ` + backtick `ls` + backtick + `;`
8058 // Operands tallied by `get_id`:
8059 // `$out` × 1, backtick literal × 1.
8060 // ⇒ u_operands = 2, N2 = 2.
8061 // Before the fix the backtick literal vanished from the count
8062 // ⇒ u_operands = 1, N2 = 1. (The inner `out` leaf of the
8063 // `variable_name` added another 1 / 1 until #1259.)
8064 check_metrics::<PhpParser>("<?php $out = `ls`;", "foo.php", |metric| {
8065 assert_eq!(metric.halstead.unique_operands(), 2);
8066 assert_eq!(metric.halstead.total_operands(), 2);
8067 });
8068 }
8069
8070 #[cfg(feature = "php")]
8071 #[test]
8072 fn php_shell_command_expression_interpolation_no_double_count() {
8073 // Regression: issue #288. PHP backtick literals DO support
8074 // `$var` interpolation (see tree-sitter-php node-types.json:
8075 // `shell_command_expression` children include `variable_name`,
8076 // `dynamic_variable_name`, `member_access_expression`,
8077 // `subscript_expression`). With the fix the wrapper drops to
8078 // `Unknown` when it carries any interpolation child, exactly
8079 // as `EncapsedString` does.
8080 //
8081 // Source: `<?php $dir = "/tmp"; $out = ` + backtick `ls $dir` +
8082 // backtick + `;`
8083 //
8084 // Operands tallied by `get_id`:
8085 // `$dir` × 2 (assignment LHS, inside backticks),
8086 // `$out` × 1, `"/tmp"` × 1.
8087 // ⇒ u_operands = 3, N2 = 4.
8088 // Without the interpolation guard the wrapping backtick literal
8089 // would also count ⇒ u_operands = 4, N2 = 5. The sigil-less
8090 // `dir` / `out` leaves added a further 2 / 3 until #1259.
8091 check_metrics::<PhpParser>(
8092 "<?php $dir = \"/tmp\"; $out = `ls $dir`;",
8093 "foo.php",
8094 |metric| {
8095 assert_eq!(metric.halstead.unique_operands(), 3);
8096 assert_eq!(metric.halstead.total_operands(), 4);
8097 },
8098 );
8099 }
8100
8101 #[cfg(feature = "php")]
8102 #[test]
8103 fn php_interpolation_opener_is_not_an_operator() {
8104 // Regression: issue #1314. `Php::LBRACE` is *both* the
8105 // compound-statement brace and the complex-interpolation
8106 // opener, so `"dq {$y} end"` reported a `{}` operator — and
8107 // reported it against the same vocabulary entry a real block
8108 // uses, which no other language does.
8109 //
8110 // expected: operators `=` × 2, `;` × 2 → n1 = 2, N1 = 4.
8111 // Operands `$s`, `$t`, `$y` × 2 → n2 = 3, N2 = 4. Before the
8112 // guard the two openers added `{}` → n1 = 3, N1 = 6.
8113 check_metrics::<PhpParser>(
8114 "<?php\n$s = \"dq {$y} end\";\n$t = \"dq {$y} end\";\n",
8115 "foo.php",
8116 |metric| {
8117 assert_eq!(metric.halstead.unique_operators(), 2);
8118 assert_eq!(metric.halstead.total_operators(), 4);
8119 assert_eq!(metric.halstead.unique_operands(), 3);
8120 assert_eq!(metric.halstead.total_operands(), 4);
8121 },
8122 );
8123 }
8124
8125 #[cfg(feature = "php")]
8126 #[test]
8127 fn php_interpolation_opener_guard_covers_every_wrapper() {
8128 // The opener is a direct child of four distinct parents, and
8129 // each is an independent leg of the guard (grammar-dispatch
8130 // section 11) — a fixture covering only `encapsed_string`
8131 // leaves the other three dead.
8132 //
8133 // One row per parent, so a failure names the leg that broke.
8134 // The heredoc's brace hangs off `heredoc_body` rather than
8135 // `heredoc`; the backtick form is `shell_command_expression`;
8136 // and the bare `${$y}` variable-variable is a
8137 // `dynamic_variable_name`, the one position that is not inside
8138 // a string at all.
8139 //
8140 // expected per row: operators `=` × 2, `;` × 2 → n1 = 2,
8141 // N1 = 4; three distinct operands with one repeated → n2 = 3,
8142 // N2 = 4.
8143 for (label, source) in [
8144 (
8145 "encapsed_string",
8146 "<?php\n$s = \"dq {$y} end\";\n$t = \"dq {$y} end\";\n",
8147 ),
8148 (
8149 "heredoc_body",
8150 "<?php\n$h = <<<EOT\na {$y} b\nEOT;\n$i = <<<EOT\na {$y} b\nEOT;\n",
8151 ),
8152 (
8153 "shell_command_expression",
8154 "<?php\n$b = `ls {$y}`;\n$c = `ls {$y}`;\n",
8155 ),
8156 ("dynamic_variable_name", "<?php\n$q = ${$y};\n$r = ${$y};\n"),
8157 ] {
8158 assert_halstead_counts::<PhpParser>(source, "foo.php", [2, 4, 3, 4], label);
8159 }
8160 }
8161
8162 #[cfg(feature = "php")]
8163 #[test]
8164 fn php_every_interpolation_spelling_scores_alike() {
8165 // The policy stated as a test (#1314). PHP writes one
8166 // interpolation three ways; the choice is spelling, so all
8167 // three must score identically. Before the guard the two
8168 // braced forms reported a `{}` the bare `$y` form did not.
8169 //
8170 // `"${y}"` is deprecated as of PHP 8.2 and removed in 9.0, but
8171 // the pinned grammar still parses it and it is still in the
8172 // wild, so it stays a row here.
8173 //
8174 // The fixture deliberately omits a `$y = …` declaration: with
8175 // one, the bare and `{$y}` forms key their operand as `$y` and
8176 // collapse into the declaration's entry while `${y}` keys as
8177 // `${y}` and does not, so n2 would differ for a reason that has
8178 // nothing to do with this guard.
8179 //
8180 // expected per spelling: operators `=` × 2, `;` × 2 → n1 = 2,
8181 // N1 = 4; operands `$s`, `$t`, the interpolated reference × 2
8182 // → n2 = 3, N2 = 4.
8183 for literal in ["\"a $y b\"", "\"a {$y} b\"", "\"a ${y} b\""] {
8184 assert_halstead_counts::<PhpParser>(
8185 &format!("<?php\n$s = {literal};\n$t = {literal};\n"),
8186 "foo.php",
8187 [2, 4, 3, 4],
8188 &format!("interpolation {literal}"),
8189 );
8190 }
8191 }
8192
8193 #[cfg(feature = "php")]
8194 #[test]
8195 fn php_compound_statement_brace_still_counts() {
8196 // Control for #1314: the guard is scoped to the four
8197 // interpolating wrappers, so a real block keeps its `{}`. A
8198 // guard widened to every `LBRACE` would take `{}` out of the
8199 // operator set entirely and fail here.
8200 //
8201 // expected: operators `function`, `()`, `{}` × 2, `if`,
8202 // `return`, `;` → n1 = 6, N1 = 8. Operands `f`, `1`, `2` →
8203 // n2 = N2 = 3.
8204 check_metrics::<PhpParser>(
8205 "<?php\nfunction f() { if (1) { return 2; } }\n",
8206 "foo.php",
8207 |metric| {
8208 assert_eq!(metric.halstead.unique_operators(), 6);
8209 assert_eq!(metric.halstead.total_operators(), 8);
8210 assert_eq!(metric.halstead.unique_operands(), 3);
8211 assert_eq!(metric.halstead.total_operands(), 3);
8212 },
8213 );
8214 }
8215
8216 #[cfg(feature = "php")]
8217 #[test]
8218 fn php_interpolation_guard_is_parent_scoped_not_ancestor_scoped() {
8219 // The input that separates the parent-scoped guard from the
8220 // ancestor-scanning mutant — the mutant #1256's post-mortem
8221 // says survives every ordinary fixture. PHP is one of only two
8222 // languages in #1314 where such an input exists: a closure
8223 // inside a complex interpolation puts a *compound-statement*
8224 // brace under an `encapsed_string` ancestor while its parent is
8225 // the `compound_statement`. An ancestor scan swallows it.
8226 //
8227 // expected: operators `=`, `;` × 2, `->`, `()` × 2, `function`,
8228 // `{}`, `return` → n1 = 7, N1 = 9. Operands `$s`, `$o`, `m`,
8229 // `1` → n2 = N2 = 4. Under the ancestor-scoped mutant the
8230 // closure's brace vanishes: n1 = 6, N1 = 8.
8231 check_metrics::<PhpParser>(
8232 "<?php\n$s = \"{$o->m(function() { return 1; })}\";\n",
8233 "foo.php",
8234 |metric| {
8235 assert_eq!(metric.halstead.unique_operators(), 7);
8236 assert_eq!(metric.halstead.total_operators(), 9);
8237 assert_eq!(metric.halstead.unique_operands(), 4);
8238 assert_eq!(metric.halstead.total_operands(), 4);
8239 },
8240 );
8241 }
8242
8243 #[cfg(feature = "elixir")]
8244 #[test]
8245 fn elixir_operators_and_operands() {
8246 // Exercises every Halstead family classified in Elixir's
8247 // `get_op_type`: control-flow keywords (`do`, `end`, `fn`),
8248 // structural punctuation — only the *opening* delimiters `(`,
8249 // `[` count after #695 (the `)`/`]` closers were dropped), plus
8250 // `,`, `.`, `@`,
8251 // arithmetic (`+`, `-`, `*`, `/`), comparison (`==`, `>`),
8252 // logical (`&&`, `||`, `and`, `or`, `!`), pipe (`|>`), capture
8253 // (`&`), assignment/match (`=`), and the stab arrow (`->`).
8254 // The body mixes identifiers, integers, atoms, and a string.
8255 check_metrics::<ElixirParser>(
8256 "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",
8257 "foo.ex",
8258 |metric| {
8259 // Positive headline assertions on integer counts. After
8260 // #695 only opening delimiters count: the `)`/`]` closers
8261 // no longer add operators (was 15 unique / 23 total).
8262 assert_eq!(metric.halstead.unique_operators(), 13);
8263 assert_eq!(metric.halstead.total_operators(), 21);
8264 assert_eq!(metric.halstead.unique_operands(), 16);
8265 assert_eq!(metric.halstead.total_operands(), 27);
8266 insta::assert_json_snapshot!(
8267 metric.halstead,
8268 @r#"
8269 {
8270 "unique_operators": 13,
8271 "total_operators": 21,
8272 "unique_operands": 16,
8273 "total_operands": 27,
8274 "length": 48,
8275 "estimated_program_length": 112.10571633583419,
8276 "purity_ratio": 2.3355357569965456,
8277 "vocabulary": 29,
8278 "volume": 233.18308776612344,
8279 "difficulty": 10.96875,
8280 "level": 0.09116809116809117,
8281 "effort": 2557.7269939346666,
8282 "time": 142.09594410748147,
8283 "bugs": 0.062342115670886794
8284 }
8285 "#
8286 );
8287 },
8288 );
8289 }
8290
8291 #[cfg(feature = "ruby")]
8292 #[test]
8293 fn ruby_operators_and_operands() {
8294 // A small Ruby method exercising operators (def/if/end keyword
8295 // tokens, `+`, `==`, `<=`, structural punctuation) and operands
8296 // (`n`, `1`, `factorial`). Anchors the unique/total counts on
8297 // both sides and snapshots the full Halstead derivation.
8298 //
8299 // Lesson 4 invariants: u_operators / u_operands here equal the
8300 // dedupe lengths the `--ops` accessor would emit on the same
8301 // source. Any future grammar bump that adds an aliased kind_id
8302 // to either side will trip this without snapshot drift.
8303 check_metrics::<RubyParser>(
8304 "def factorial(n)\n return 1 if n <= 1\n n * factorial(n - 1)\nend\n",
8305 "foo.rb",
8306 |metric| {
8307 // After #695 only the `(` opener counts (folded `()`); the
8308 // `)` closer — which appeared twice across the two calls —
8309 // no longer adds an operator (was 9 unique / 11 total).
8310 assert_eq!(metric.halstead.unique_operators(), 8);
8311 assert_eq!(metric.halstead.total_operators(), 9);
8312 assert_eq!(metric.halstead.unique_operands(), 3);
8313 assert_eq!(metric.halstead.total_operands(), 9);
8314 insta::assert_json_snapshot!(metric.halstead);
8315 },
8316 );
8317 }
8318
8319 #[cfg(feature = "ruby")]
8320 #[test]
8321 fn ruby_halstead_plain_string_operand() {
8322 // A bare string literal contributes exactly one operand. The
8323 // counterpart to `ruby_halstead_interpolated_string_no_double_count`
8324 // — verifies the "no interpolation" branch of the same arm
8325 // (see `get_op_type`'s `R::String | …` case in
8326 // `big-code-analysis-ast/src/getter.rs`).
8327 // expected: operators = {def, end} = 2; operands = {f, "hello"} = 2.
8328 check_metrics::<RubyParser>("def f\n \"hello\"\nend\n", "foo.rb", |metric| {
8329 assert_eq!(metric.halstead.unique_operators(), 2);
8330 assert_eq!(metric.halstead.total_operators(), 2);
8331 assert_eq!(metric.halstead.unique_operands(), 2);
8332 assert_eq!(metric.halstead.total_operands(), 2);
8333 });
8334 }
8335
8336 #[cfg(feature = "ruby")]
8337 #[test]
8338 fn ruby_halstead_interpolated_string_no_double_count() {
8339 // Regression mirror for #180 (Bash) / #183 (C#): when a Ruby
8340 // string literal carries an `Interpolation` child, the
8341 // wrapping `String` node is intentionally classified as
8342 // `Unknown` so the inner expression's identifiers are not
8343 // double-counted as operands.
8344 //
8345 // expected: for `def f(name)\n "Hi #{name}"\nend\n` —
8346 // operators: def, (, ), #{, }, end → u_operators = 6.
8347 // operands: f, name (param), name (inside `#{name}`). The
8348 // wrapping `"…#{name}"` literal is skipped by the
8349 // `is_child(R::Interpolation)` guard; the operand store
8350 // keys by token text so the two `name` occurrences dedupe
8351 // into one distinct entry → u_operands = 2, operands = 3
8352 // (`f` once, `name` twice).
8353 // Without the guard, the wrapping literal would also count,
8354 // inflating u_operands to 3 and operands to 4.
8355 check_metrics::<RubyParser>("def f(name)\n \"Hi #{name}\"\nend\n", "foo.rb", |metric| {
8356 assert_eq!(metric.halstead.unique_operands(), 2);
8357 assert_eq!(metric.halstead.total_operands(), 3);
8358 });
8359 }
8360
8361 #[cfg(feature = "ruby")]
8362 #[test]
8363 fn ruby_halstead_symbol_literal_operand() {
8364 // `:foo` is a `SimpleSymbol` leaf — counts as a single
8365 // operand, no interpolation guard needed (only
8366 // `DelimitedSymbol` (`:"…#{x}…"`) can interpolate).
8367 // expected: operators = {def, end} = 2; operands = {f, :ok} = 2.
8368 check_metrics::<RubyParser>("def f\n :ok\nend\n", "foo.rb", |metric| {
8369 assert_eq!(metric.halstead.unique_operators(), 2);
8370 assert_eq!(metric.halstead.unique_operands(), 2);
8371 });
8372 }
8373
8374 #[cfg(feature = "ruby")]
8375 #[test]
8376 fn ruby_halstead_regex_operand() {
8377 // `/foo/` parses as a `Regex` node — one operand. Its two
8378 // `SLASH` delimiters used to fall through to the shared
8379 // arithmetic arm and add a `/` operator that is nowhere in the
8380 // source; #1312 parent-guards them to `Unknown`.
8381 // expected: u_operators = {def, (, =~, end} = 4, N1 = 4 (only
8382 // the `(` opener counts after #695 — the `)` closer was
8383 // dropped; was 5 with the fabricated `/`); u_operands =
8384 // {f, s, /foo/} = 3, N2 = 4 (`s` twice: parameter and use).
8385 check_metrics::<RubyParser>("def f(s)\n s =~ /foo/\nend\n", "foo.rb", |metric| {
8386 assert_eq!(metric.halstead.unique_operators(), 4);
8387 assert_eq!(metric.halstead.total_operators(), 4);
8388 assert_eq!(metric.halstead.unique_operands(), 3);
8389 assert_eq!(metric.halstead.total_operands(), 4);
8390 });
8391 }
8392
8393 #[cfg(feature = "ruby")]
8394 #[test]
8395 fn ruby_regex_delimiters_are_not_operators() {
8396 // Regression: issue #1312, the Ruby sibling of Elixir #1256.
8397 // Both of a `Regex` literal's delimiter tokens are `SLASH` —
8398 // the same kind id real division uses — so `x = /abc/`
8399 // reported a `/` operator with no division in the source.
8400 //
8401 // expected: operators `=` → n1 = N1 = 1. Operands `x` and the
8402 // `/abc/` literal → n2 = N2 = 2. Before the guard the two
8403 // delimiters added `/` → n1 = 2, N1 = 3.
8404 check_metrics::<RubyParser>("x = /abc/\n", "foo.rb", |metric| {
8405 assert_eq!(metric.halstead.unique_operators(), 1);
8406 assert_eq!(metric.halstead.total_operators(), 1);
8407 assert_eq!(metric.halstead.unique_operands(), 2);
8408 assert_eq!(metric.halstead.total_operands(), 2);
8409 });
8410 }
8411
8412 #[cfg(feature = "ruby")]
8413 #[test]
8414 fn ruby_regex_delimiter_choice_is_invariant() {
8415 // Companion to the test above (#1312): `%r`-form regexes are
8416 // the same literal spelled differently, so every delimiter
8417 // choice must produce identical counts. tree-sitter-ruby
8418 // aliases all of them to `SLASH` — verified with `bca dump`,
8419 // which shows `%r{`/`}`, `%r(`/`)`, `%r[`/`]`, `%r<`/`>`,
8420 // `%r|`/`|` and `%r!`/`!` every one emitting kind `SLASH` —
8421 // so each row here genuinely exercises the guard rather than
8422 // reaching a different, already-clean path.
8423 //
8424 // expected per variant: operator `=` → n1 = N1 = 1; operands
8425 // `x` and the literal → n2 = N2 = 2.
8426 for literal in [
8427 "/abc/", "%r{abc}", "%r(abc)", "%r[abc]", "%r<abc>", "%r|abc|", "%r!abc!",
8428 ] {
8429 assert_halstead_counts::<RubyParser>(
8430 &format!("x = {literal}\n"),
8431 "foo.rb",
8432 [1, 1, 2, 2],
8433 &format!("regex literal {literal}"),
8434 );
8435 }
8436 }
8437
8438 #[cfg(feature = "ruby")]
8439 #[test]
8440 fn ruby_division_survives_the_regex_guard() {
8441 // Control for #1312: the guard is scoped to a `Regex` parent,
8442 // so real division still counts. Two divisions, so a mutant
8443 // that collapsed repeated hits would move `N1` even though
8444 // `n1` held (the #1294 count-only-anchor lesson).
8445 //
8446 // expected: operators `=` and `/` × 2 → n1 = 2, N1 = 3.
8447 // Operands `z`, `a`, `b`, `c` → n2 = N2 = 4.
8448 check_metrics::<RubyParser>("z = a / b / c\n", "foo.rb", |metric| {
8449 assert_eq!(metric.halstead.unique_operators(), 2);
8450 assert_eq!(metric.halstead.total_operators(), 3);
8451 assert_eq!(metric.halstead.unique_operands(), 4);
8452 assert_eq!(metric.halstead.total_operands(), 4);
8453 });
8454 }
8455
8456 #[cfg(feature = "ruby")]
8457 #[test]
8458 fn ruby_regex_guard_is_parent_scoped_not_ancestor_scoped() {
8459 // The one input that separates the correct parent-scoped guard
8460 // from the ancestor-scoped mutant of it (#1312, mirroring
8461 // #1256's Elixir case): a division *inside* a regex's `#{…}`
8462 // interpolation. Its `/` has `Binary` as its parent but the
8463 // `Regex` as a further ancestor, so an ancestor scan would
8464 // swallow it. Every other fixture in this file passes under
8465 // both spellings. Two interpolations, so the mutant moves both
8466 // n1 (2 → 1) and N1 (3 → 1).
8467 //
8468 // expected: operators `=`, `/` × 2 → n1 = 2, N1 = 3. Operands
8469 // `w`, `p`, `q`, `r`, `t` → n2 = N2 = 5; the wrapping `Regex`
8470 // is skipped because it carries an `Interpolation` child (the
8471 // #180 double-count guard).
8472 //
8473 // Was n1 = 3, N1 = 5 until #1314 dropped `#{` from the operator
8474 // arm. The mutant still moves both axes, so this fixture is as
8475 // discriminating as it was — it just no longer counts the two
8476 // interpolation openers alongside the two divisions.
8477 check_metrics::<RubyParser>("w = /a#{p / q}c#{r / t}b/\n", "foo.rb", |metric| {
8478 assert_eq!(metric.halstead.unique_operators(), 2);
8479 assert_eq!(metric.halstead.total_operators(), 3);
8480 assert_eq!(metric.halstead.unique_operands(), 5);
8481 assert_eq!(metric.halstead.total_operands(), 5);
8482 });
8483 }
8484
8485 #[cfg(feature = "ruby")]
8486 #[test]
8487 fn ruby_regex_start_alias_never_reaches_kind_id() {
8488 // Drift marker for the `R::SLASH2` half of #1312's guard.
8489 // `SLASH2` is the aliased regex-start token: it sits in the
8490 // enum beside the other literal-start aliases (`DQUOTE`,
8491 // `COLONDQUOTE`, `BQUOTE2`, `PERCENTwLPAREN`) and the runtime
8492 // `public_symbol_map` collapses it to `SLASH` before
8493 // `kind_id()`, exactly like `LPAREN2` in #768. It is listed in
8494 // the guard rather than the arithmetic arm because a regex
8495 // delimiter is the only thing it could ever be; this pins that
8496 // it is currently unreachable, so a grammar bump that starts
8497 // emitting it fails here instead of silently changing a metric.
8498 let path = PathBuf::from("foo.rb");
8499 for source in ["x = /abc/\n", "x = %r{abc}\n"] {
8500 let parser = RubyParser::new(source.as_bytes().to_vec(), &path, None);
8501 assert!(
8502 !ast_has_kind_id(&parser, Ruby::SLASH2 as u16),
8503 "Ruby::SLASH2 must stay collapsed to Ruby::SLASH for `{source}`"
8504 );
8505 // Positive control: the id the guard actually fires on is
8506 // present, so the assertion above cannot pass merely
8507 // because no delimiter was parsed at all.
8508 assert!(
8509 ast_has_kind_id(&parser, Ruby::SLASH as u16),
8510 "Ruby::SLASH must be the delimiter kind for `{source}`"
8511 );
8512 }
8513 }
8514
8515 #[cfg(feature = "ruby")]
8516 #[test]
8517 fn ruby_subshell_delimiters_are_not_operators() {
8518 // Regression: issue #1360, the second delimiter family of the
8519 // fabrication #1312 removed for regexes. tree-sitter-ruby
8520 // aliases both ends of a subshell literal to `BQUOTE` — the
8521 // same kind id the `` def ` `` method-name marker uses — so
8522 // `` x = `echo hi` `` reported a `` ` `` operator with no
8523 // backtick method call in the source.
8524 //
8525 // expected: operator `=` → n1 = N1 = 1. Operands `x` and the
8526 // `` `echo hi` `` literal → n2 = N2 = 2. Before the guard the
8527 // two delimiters added `` ` `` → n1 = 2, N1 = 3.
8528 assert_halstead_counts::<RubyParser>(
8529 "x = `echo hi`\n",
8530 "foo.rb",
8531 [1, 1, 2, 2],
8532 "backtick subshell",
8533 );
8534 }
8535
8536 #[cfg(feature = "ruby")]
8537 #[test]
8538 fn ruby_subshell_delimiter_choice_is_invariant() {
8539 // Companion to the test above (#1360): `%x`-form subshells are
8540 // the same literal spelled differently, so every delimiter
8541 // choice must produce identical counts. tree-sitter-ruby
8542 // aliases all of them to `BQUOTE` — verified with `bca dump`,
8543 // which shows `` ` ``/`` ` ``, `%x{`/`}`, `%x(`/`)`, `%x[`/`]`,
8544 // `%x<`/`>`, `%x|`/`|` and `%x!`/`!` every one emitting kind
8545 // `BQUOTE` — so each row here genuinely exercises the guard
8546 // rather than reaching a different, already-clean path. Note
8547 // the `%x!…!` and `%x|…|` closers in particular: they are
8548 // `BQUOTE`, not the `BANG` / `PIPE` operators they look like.
8549 //
8550 // The empty row records that `` `` `` parses to a `Subshell`
8551 // holding nothing but its two delimiters, and still bills that
8552 // wrapper as one operand once both are suppressed. It is
8553 // documentation, not a discriminator: the guard deletes nothing
8554 // from the operand arm, so grammar-dispatch §6's
8555 // childless-variant-regresses-to-zero hazard cannot arise here
8556 // and this row moves with the first row under every mutant.
8557 //
8558 // expected per variant: operator `=` → n1 = N1 = 1; operands
8559 // `x` and the literal → n2 = N2 = 2.
8560 for literal in [
8561 "`echo`", "%x{echo}", "%x(echo)", "%x[echo]", "%x<echo>", "%x|echo|", "%x!echo!", "``",
8562 ] {
8563 assert_halstead_counts::<RubyParser>(
8564 &format!("x = {literal}\n"),
8565 "foo.rb",
8566 [1, 1, 2, 2],
8567 &format!("subshell literal {literal}"),
8568 );
8569 }
8570 }
8571
8572 #[cfg(feature = "ruby")]
8573 #[test]
8574 fn ruby_backtick_method_name_survives_the_subshell_guard() {
8575 // Control for #1360, and the reason the kind is gated rather
8576 // than dropped from the operator arm (grammar-dispatch §6):
8577 // `` ` `` is a legal Ruby method name. `bca dump` at the pinned
8578 // grammar shows `` def `(cmd) `` emitting a `BQUOTE` wrapped in
8579 // a named `operator` node, so its parent is `Operator` and the
8580 // parent-scoped guard leaves it alone. Deleting `BQUOTE` from
8581 // the operator arm would have scored this construct zero.
8582 //
8583 // expected: operators `def`, `` ` ``, `(` and `end` →
8584 // n1 = N1 = 4 (only the `(` opener counts after #695).
8585 // Operand `cmd` twice, parameter and use → n2 = 1, N2 = 2.
8586 assert_halstead_counts::<RubyParser>(
8587 "def `(cmd)\n cmd\nend\n",
8588 "foo.rb",
8589 [4, 4, 1, 2],
8590 "backtick method name",
8591 );
8592 }
8593
8594 #[cfg(feature = "ruby")]
8595 #[test]
8596 fn ruby_subshell_guard_is_parent_scoped_not_ancestor_scoped() {
8597 // The one input that separates the correct parent-scoped guard
8598 // from the ancestor-scoped mutant of it (#1360, mirroring
8599 // #1312's regex case): backtick *methods* used inside a
8600 // subshell's `#{…}` interpolation. Their `BQUOTE` has the named
8601 // `operator` node as its parent but the `Subshell` as a further
8602 // ancestor, so an ancestor scan would swallow both. Every other
8603 // fixture in this file passes under either spelling. Two
8604 // markers — a definition and a call — so the mutant moves both
8605 // n1 (7 → 6) and N1 (10 → 8), per the #1294 count-only-anchor
8606 // lesson.
8607 //
8608 // expected: operators `=` (1), `def` (1), `` ` `` (2), `(` (2),
8609 // `;` (2), `end` (1), `.` (1) → n1 = 7, N1 = 10. Operands `n`,
8610 // `c` × 2, `s`, `"x"` → n2 = 4, N2 = 5; the wrapping `Subshell`
8611 // is skipped because it carries `Interpolation` children (the
8612 // #180 double-count guard), and its `a` / `b` string content is
8613 // classified by no arm.
8614 check_metrics::<RubyParser>(
8615 "n = `a#{def `(c); c; end}b#{s.`(\"x\")}`\n",
8616 "foo.rb",
8617 |metric| {
8618 assert_eq!(metric.halstead.unique_operators(), 7);
8619 assert_eq!(metric.halstead.total_operators(), 10);
8620 assert_eq!(metric.halstead.unique_operands(), 4);
8621 assert_eq!(metric.halstead.total_operands(), 5);
8622 },
8623 );
8624 }
8625
8626 #[cfg(feature = "ruby")]
8627 #[test]
8628 fn ruby_subshell_start_alias_never_reaches_kind_id() {
8629 // Drift marker for #1360's guard, the `BQUOTE2` sibling of
8630 // `ruby_regex_start_alias_never_reaches_kind_id`. `BQUOTE2`
8631 // sits in the enum's literal-start alias block beside `DQUOTE`,
8632 // `COLONDQUOTE` and `SLASH2`, and the runtime
8633 // `public_symbol_map` collapses it to `BQUOTE` before
8634 // `kind_id()`, exactly like `LPAREN2` in #768.
8635 //
8636 // `BQUOTE2` is therefore in no dispatch arm at all — not the
8637 // guard and not the operator arm — which is the asymmetry with
8638 // `SLASH2`, a kind #1312 had to *move* out of the arithmetic
8639 // arm. Adding it to the guard would change nothing, since the
8640 // wildcard already answers `Unknown` for it. What that leaves
8641 // unguarded is the reverse direction: a bump that started
8642 // emitting `BQUOTE2` would drop a subshell opener silently
8643 // (already the wanted answer) but would also stop counting a
8644 // `` def ` `` marker spelled that way. This pins the
8645 // reachability so such a bump fails here rather than moving a
8646 // metric unobserved.
8647 let path = PathBuf::from("foo.rb");
8648 for source in ["x = `echo`\n", "x = %x{echo}\n"] {
8649 let parser = RubyParser::new(source.as_bytes().to_vec(), &path, None);
8650 assert!(
8651 !ast_has_kind_id(&parser, Ruby::BQUOTE2 as u16),
8652 "Ruby::BQUOTE2 must stay collapsed to Ruby::BQUOTE for `{source}`"
8653 );
8654 // Positive control: the id the guard actually fires on is
8655 // present, so the assertion above cannot pass merely
8656 // because no delimiter was parsed at all.
8657 assert!(
8658 ast_has_kind_id(&parser, Ruby::BQUOTE as u16),
8659 "Ruby::BQUOTE must be the delimiter kind for `{source}`"
8660 );
8661 }
8662 }
8663
8664 #[cfg(feature = "ruby")]
8665 #[test]
8666 fn ruby_interpolation_opener_is_not_an_operator() {
8667 // Behaviour change, not a fabrication fix: #1314 drops
8668 // `HASHLBRACE` from Ruby's operator arm. `#{` is a token of its
8669 // own here — unlike PHP's `{`, which aliases the
8670 // compound-statement brace — so nothing was being miscounted;
8671 // the question was whether an interpolation opener is an
8672 // operation at all, and across the five interpolating languages
8673 // three already said no. Ruby Halstead operator counts drop for
8674 // interpolated literals as a result.
8675 //
8676 // Asserted as an invariance: the interpolated and plain
8677 // spellings of one string must now score identically, which is
8678 // the policy rather than a magic number. Before the change the
8679 // interpolated row was n1 = 2, N1 = 3.
8680 //
8681 // expected per row: operator `=` × 2 → n1 = 1, N1 = 2; operands
8682 // `s`, `t`, and the literal's content contribution × 2 →
8683 // n2 = 3, N2 = 4.
8684 for literal in ["\"a #{y} b\"", "\"a b\""] {
8685 assert_halstead_counts::<RubyParser>(
8686 &format!("s = {literal}\nt = {literal}\n"),
8687 "foo.rb",
8688 [1, 2, 3, 4],
8689 &format!("literal {literal}"),
8690 );
8691 }
8692 }
8693
8694 #[cfg(feature = "ruby")]
8695 #[test]
8696 fn ruby_interpolation_opener_drop_covers_every_literal() {
8697 // `HASHLBRACE` is one arm, but it fires under every Ruby
8698 // literal that interpolates, so the drop is not specific to
8699 // double-quoted strings. A symbol and a regex — two literals
8700 // whose `#{…}` reaches the same token — must contribute no
8701 // operator for the opener either.
8702 //
8703 // expected: operator `=` × 3 → n1 = 1, N1 = 3. Operands `y`,
8704 // `1`, `a`, `b`, plus the two interpolated `y` references →
8705 // n2 = 4, N2 = 6. Before the change each `#{` added one →
8706 // n1 = 2, N1 = 5.
8707 check_metrics::<RubyParser>("y = 1\na = :\"s#{y}\"\nb = /r#{y}/\n", "foo.rb", |metric| {
8708 assert_eq!(metric.halstead.unique_operators(), 1);
8709 assert_eq!(metric.halstead.total_operators(), 3);
8710 assert_eq!(metric.halstead.unique_operands(), 4);
8711 assert_eq!(metric.halstead.total_operands(), 6);
8712 });
8713 }
8714
8715 #[cfg(feature = "ruby")]
8716 #[test]
8717 fn ruby_element_containers_count_elements_not_the_composite_1353() {
8718 // #1353. `chained_string`, `string_array` and `symbol_array`
8719 // hold *classified operand* children instead of the raw
8720 // `string_content` every other string-like literal wraps, so
8721 // the arm's shared `Interpolation` guard never fired and the
8722 // wrapper was billed alongside each element. Every row below
8723 // scored n2 4 / N2 4 for three operands, the extra entry being
8724 // the wrapper's whole-span text — which made the vocabulary
8725 // depend on how the author grouped the literals.
8726 //
8727 // `assert_ops_operands` pins the operand *text*, not just the
8728 // count: a wrapper coming back would show up as `"one" "two"` /
8729 // `%w[x y]` / `%i[p q]` rather than as an off-by-one number.
8730 //
8731 // The kind assertions are the grammar-dispatch §1 / §2 drift
8732 // marker — a bump that renumbers a wrapper or an element would
8733 // otherwise leave these counts passing while measuring a
8734 // construct the arm no longer names.
8735 //
8736 // Two of the three rows also pin the arm's *membership*:
8737 // dropping `StringArray` or `SymbolArray` from it fails
8738 // `ruby_empty_word_and_symbol_arrays_still_bill_one_operand_1353`.
8739 // `ChainedString`'s membership is unobservable and no test can
8740 // pin it — `repeat1($.string)` guarantees the guard always
8741 // fires, so `Unknown` and "not in the arm at all" are the same
8742 // answer for every input. It is listed for symmetry with its
8743 // siblings, and correct-by-construction is the only defence
8744 // available there.
8745 for (source, wrapper, element, operands) in [
8746 (
8747 "a = \"one\" \"two\"\n",
8748 Ruby::ChainedString,
8749 Ruby::String,
8750 vec!["a", "\"one\"", "\"two\""],
8751 ),
8752 (
8753 "b = %w[x y]\n",
8754 Ruby::StringArray,
8755 Ruby::BareString,
8756 vec!["b", "x", "y"],
8757 ),
8758 (
8759 "c = %i[p q]\n",
8760 Ruby::SymbolArray,
8761 Ruby::BareSymbol,
8762 vec!["c", "p", "q"],
8763 ),
8764 ] {
8765 let parser =
8766 RubyParser::new(source.as_bytes().to_vec(), &PathBuf::from("foo.rb"), None);
8767 assert!(
8768 ast_has_kind_id(&parser, wrapper as u16),
8769 "the container kind this arm gates on is unreachable for `{source}`"
8770 );
8771 assert!(
8772 ast_has_kind_id(&parser, element as u16),
8773 "the element kind this arm gates on is unreachable for `{source}`"
8774 );
8775
8776 // expected: operator `=` → n1 = N1 = 1; operands are the
8777 // assignment target and the two elements → n2 = N2 = 3.
8778 assert_halstead_counts::<RubyParser>(source, "foo.rb", [1, 1, 3, 3], source);
8779 assert_ops_operands::<RubyParser>(source, "foo.rb", 3, operands);
8780 }
8781 }
8782
8783 #[cfg(feature = "ruby")]
8784 #[test]
8785 fn ruby_empty_word_and_symbol_arrays_still_bill_one_operand_1353() {
8786 // The childless spelling, and the whole reason #1353 gates the
8787 // three container kinds instead of dropping them from the arm
8788 // (grammar-dispatch §6). `%w[]` / `%i[]` parse to a wrapper
8789 // holding nothing but its two delimiter tokens, so the tempting
8790 // "just delete the wrapper" fix — the one #1351 was right to
8791 // take for Bash's `command_name` — would score an empty literal
8792 // zero operands where the source plainly has a literal. This is
8793 // the assertion to watch fail against that alternative.
8794 //
8795 // expected: operator `=` → n1 = N1 = 1; operands the assignment
8796 // target and the empty literal itself → n2 = N2 = 2.
8797 for (source, operands) in [
8798 ("d = %w[]\n", vec!["d", "%w[]"]),
8799 ("e = %i[]\n", vec!["e", "%i[]"]),
8800 ] {
8801 assert_halstead_counts::<RubyParser>(source, "foo.rb", [1, 1, 2, 2], source);
8802 assert_ops_operands::<RubyParser>(source, "foo.rb", 2, operands);
8803 }
8804 }
8805
8806 #[cfg(feature = "ruby")]
8807 #[test]
8808 fn ruby_interpolated_array_elements_are_not_double_counted_1353() {
8809 // `bare_string` and `bare_symbol` are two aliases of a single
8810 // grammar production (`_literal_contents`), one per array form,
8811 // so `%W[…]` and `%I[…]` must score identically. Until #1353
8812 // only `bare_string` carried the interpolation guard and
8813 // `bare_symbol` sat in the plain operand arm, so `%I[a#{n}b c]`
8814 // billed the element `a#{n}b` *and* the `n` inside it (n2 4)
8815 // where `%W[a#{n}b c]` billed 3.
8816 //
8817 // Asserted as an invariance over the two spellings, the way
8818 // `ruby_interpolation_opener_is_not_an_operator` is — the
8819 // policy is the claim, not the magic number. The third row is
8820 // the composed case: a `chained_string` whose own guard defers
8821 // to an element that is itself interpolated.
8822 //
8823 // expected per row: operator `=` → n1 = N1 = 1; operands the
8824 // assignment target, the interpolated expression `n`, and the
8825 // one inert element → n2 = N2 = 3. The operand *text* is pinned
8826 // too, because 3 is also the count an implementation that
8827 // suppressed the inert element instead of the wrapper would
8828 // report.
8829 for (source, operands) in [
8830 ("w = %W[a#{n}b c]\n", vec!["w", "n", "c"]),
8831 ("w = %I[a#{n}b c]\n", vec!["w", "n", "c"]),
8832 ("a = \"x#{n}\" \"y\"\n", vec!["a", "n", "\"y\""]),
8833 ] {
8834 assert_halstead_counts::<RubyParser>(source, "foo.rb", [1, 1, 3, 3], source);
8835 assert_ops_operands::<RubyParser>(source, "foo.rb", 3, operands);
8836 }
8837 }
8838
8839 #[cfg(feature = "ruby")]
8840 #[test]
8841 fn ruby_suffixed_numeric_literals_bill_one_operand_1359() {
8842 // #1359, the wrapper/leaf double count one arm below #1353's.
8843 // `rational` and `complex` wrap the numeral they suffix, and
8844 // `get_op_type` billed every level: `a = 1r` scored n2 3 / N2 3
8845 // for two operands, and `c = 3ri` — a `complex` over a
8846 // `rational` over an `integer` — scored n2 3 / N2 4 for the
8847 // same two.
8848 //
8849 // The rows are ordered by nesting depth on purpose, because the
8850 // half-fix is a plausible one to write. Guard the two leaf
8851 // kinds and not the intermediate `rational` — `R::Integer |
8852 // R::Float` where the arm says `R::Integer | R::Float |
8853 // R::Rational` — and the `1r` and `2i` rows still pass; the
8854 // `ri` rows, whose middle `rational` must be suppressed as well
8855 // as the leaf beneath it, are the two that fail (measured:
8856 // `c = 3ri` reports n2 3 / N2 3).
8857 //
8858 // The trailing rows are the ones that must *not* move. `g = 4`
8859 // has no wrapper at all and pins that the guard is scoped to
8860 // the numerals inside one (grammar-dispatch §6 — nothing
8861 // regresses to zero). `"#{4r}"` and `-3r` put the wrapper
8862 // somewhere other than an assignment right-hand side, under a
8863 // node that guards its own children (`Interpolation`, whose
8864 // enclosing `String` is suppressed by the arm above) and under
8865 // one that does not (`unary`); both must still bill the
8866 // wrapper once. They do *not* pin parent-versus-ancestor
8867 // scoping and nothing can: `complex` and `rational` admit
8868 // numeral children only, so an ancestor-scoped guard would
8869 // classify every Ruby input identically. And `1r + 1r` is the
8870 // row where N2 and n2 come apart, so the `N2` column is
8871 // load-bearing on its own rather than tracking `n2`.
8872 //
8873 // The kind assertions are the grammar-dispatch §1 / §2 drift
8874 // marker: a bump that renumbered `complex` or `rational`, or
8875 // stopped nesting them, would leave the counts passing while
8876 // measuring a construct the guard no longer names. `IntOrFloat`
8877 // is the §2 half — the hidden `_int_or_float` the two wrappers
8878 // are written over, which a bump could promote into the
8879 // numeral's parent and silently stop the guard firing. Not
8880 // every kind a row mentions is one the guard names
8881 // (`Interpolation` is context), and `unary` and `binary` are
8882 // deliberately absent: they carry five and three aliased
8883 // variants here, so naming one would pin the alias rather than
8884 // the shape.
8885 for (source, kinds, counts, operands) in [
8886 (
8887 "a = 1r\n",
8888 &[Ruby::Rational as u16, Ruby::Integer as u16][..],
8889 [1, 1, 2, 2],
8890 vec!["a", "1r"],
8891 ),
8892 (
8893 "b = 2i\n",
8894 &[Ruby::Complex as u16, Ruby::Integer as u16][..],
8895 [1, 1, 2, 2],
8896 vec!["b", "2i"],
8897 ),
8898 (
8899 "c = 3ri\n",
8900 &[
8901 Ruby::Complex as u16,
8902 Ruby::Rational as u16,
8903 Ruby::Integer as u16,
8904 ][..],
8905 [1, 1, 2, 2],
8906 vec!["c", "3ri"],
8907 ),
8908 (
8909 "d = 1.5r\n",
8910 &[Ruby::Rational as u16, Ruby::Float as u16][..],
8911 [1, 1, 2, 2],
8912 vec!["d", "1.5r"],
8913 ),
8914 (
8915 "e = 2.5i\n",
8916 &[Ruby::Complex as u16, Ruby::Float as u16][..],
8917 [1, 1, 2, 2],
8918 vec!["e", "2.5i"],
8919 ),
8920 (
8921 "f = 1.5ri\n",
8922 &[
8923 Ruby::Complex as u16,
8924 Ruby::Rational as u16,
8925 Ruby::Float as u16,
8926 ][..],
8927 [1, 1, 2, 2],
8928 vec!["f", "1.5ri"],
8929 ),
8930 (
8931 "g = 4\n",
8932 &[Ruby::Integer as u16][..],
8933 [1, 1, 2, 2],
8934 vec!["g", "4"],
8935 ),
8936 (
8937 "h = \"#{4r}\"\n",
8938 &[Ruby::Interpolation as u16, Ruby::Rational as u16][..],
8939 [1, 1, 2, 2],
8940 vec!["h", "4r"],
8941 ),
8942 (
8943 "i = -3r\n",
8944 &[Ruby::Rational as u16, Ruby::Integer as u16][..],
8945 [2, 2, 2, 2],
8946 vec!["i", "3r"],
8947 ),
8948 (
8949 "j = 1r + 1r\n",
8950 &[Ruby::Rational as u16, Ruby::Integer as u16][..],
8951 [2, 2, 2, 3],
8952 vec!["j", "1r"],
8953 ),
8954 ] {
8955 let parser =
8956 RubyParser::new(source.as_bytes().to_vec(), &PathBuf::from("foo.rb"), None);
8957 for kind in kinds {
8958 assert!(
8959 ast_has_kind_id(&parser, *kind),
8960 "the kind this row measures is unreachable for `{source}`"
8961 );
8962 }
8963 assert!(
8964 !ast_has_kind_id(&parser, Ruby::IntOrFloat as u16),
8965 "`_int_or_float` is hidden; a grammar that emits it would \
8966 sit between the wrapper and the numeral for `{source}`"
8967 );
8968
8969 assert_halstead_counts::<RubyParser>(source, "foo.rb", counts, source);
8970 assert_ops_operands::<RubyParser>(source, "foo.rb", operands.len(), operands);
8971 }
8972 }
8973
8974 #[cfg(feature = "ruby")]
8975 #[test]
8976 fn ruby_numeric_suffixes_stay_distinct_operands_1359() {
8977 // The reason #1359 keeps the *wrapper* and gates the leaf
8978 // rather than dropping the two wrapper kinds from the arm, as
8979 // #1351 and #1358 did for their Bash wrappers. `1`, `1r`, `1i`
8980 // and `1ri` are four distinct Ruby constants — Integer,
8981 // Rational, Complex and Complex-of-Rational — and only the
8982 // wrapper's span carries the suffix that says which.
8983 //
8984 // This is the assertion to watch fail against the leaf-keeping
8985 // alternative: billing the `integer` would file all four under
8986 // the operand text `1`, reporting n2 5 / N2 8 where the source
8987 // plainly has eight distinct operands. The count alone does not
8988 // say that, so the vocabulary is pinned by text.
8989 //
8990 // expected: operator `=` → n1 1, N1 4; four targets and four
8991 // constants, none repeated → n2 = N2 = 8.
8992 let source = "w = 1\nx = 1r\ny = 1i\nz = 1ri\n";
8993 assert_halstead_counts::<RubyParser>(source, "foo.rb", [1, 4, 8, 8], source);
8994 assert_ops_operands::<RubyParser>(
8995 source,
8996 "foo.rb",
8997 8,
8998 vec!["w", "x", "y", "z", "1", "1r", "1i", "1ri"],
8999 );
9000 }
9001
9002 /// Comprehensive iRules Halstead test exercising every operator family
9003 /// classified in `get_op_type`: declaration/control keywords (`proc`,
9004 /// `set`, `if`, `return`), structural punctuation (`{}` `[]` `()`),
9005 /// arithmetic (`+`), comparison (`>`), the word-form string comparator
9006 /// (`eq`), and short-circuit logical (`&&`). Anchored on the integer
9007 /// `n1`/`N1`/`n2`/`N2` headline values; the float fields are derived and
9008 /// bit-brittle, so they are not pinned.
9009 ///
9010 /// The second half pins the lesson-4 invariant: the independent
9011 /// text-keyed `operands_and_operators` store must dedupe to the same
9012 /// `n1`/`n2`. A classification change that moved one store without the
9013 /// other (e.g. a kind landing in both the operator and operand arms)
9014 /// would break this even though the snapshot stayed green.
9015 #[cfg(feature = "irules")]
9016 #[test]
9017 fn irules_operators_and_operands() {
9018 let source = "proc f { a b } {
9019 set x [expr { $a + $b }]
9020 if { $x > 0 && $a eq \"go\" } {
9021 return $x
9022 }
9023 return 0
9024}
9025";
9026 check_metrics::<IrulesParser>(source, "foo.irule", |metric| {
9027 // After #695 only opening delimiters count: the `}`/`]`
9028 // closers no longer add operators (was 12 unique / 20 total).
9029 assert_eq!(metric.halstead.unique_operators(), 10);
9030 assert_eq!(metric.halstead.total_operators(), 14);
9031 // Operands fell 12 / 16 → 10 / 14 with #1354: the proc body
9032 // and the `if` body are `BracedWord` script kinds and are no
9033 // longer operands beside the commands they contain.
9034 assert_eq!(metric.halstead.unique_operands(), 10);
9035 assert_eq!(metric.halstead.total_operands(), 14);
9036 });
9037
9038 let path = PathBuf::from("foo.irule");
9039 let parser = IrulesParser::new(source.as_bytes().to_vec(), &path, None);
9040 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
9041 let unique_operators: HashSet<&str> = ops.operators.iter().map(String::as_str).collect();
9042 let unique_operands: HashSet<&str> = ops.operands.iter().map(String::as_str).collect();
9043 assert_eq!(
9044 unique_operators.len(),
9045 10,
9046 "dedupe(ops.operators) must equal n1; operators were {:?}",
9047 ops.operators
9048 );
9049 assert_eq!(
9050 unique_operands.len(),
9051 10,
9052 "dedupe(ops.operands) must equal n2; operands were {:?}",
9053 ops.operands
9054 );
9055 }
9056
9057 /// An inert `"hello world"` double-quoted string (no `$var` / `[cmd]`
9058 /// interpolation child) contributes exactly **one** operand — the
9059 /// wrapping `QuotedWord`. Operands are `f`, `s` and `"hello world"` —
9060 /// n2 = 3, the same as Tcl since #1294 restored its `set` target to
9061 /// the operand count and #1354 dropped the proc-body `braced_word`
9062 /// from both. Mirrors `tcl_inert_quoted_word_counts_as_operand`
9063 /// (#277).
9064 #[cfg(feature = "irules")]
9065 #[test]
9066 fn irules_inert_quoted_word_counts_as_operand() {
9067 let source = "proc f {} {\n set s \"hello world\"\n}\n";
9068 check_metrics::<IrulesParser>(source, "foo.irule", |metric| {
9069 // After #695 only the `{` opener counts; the `}` closer no
9070 // longer adds an operator (was 4 unique / 6 total).
9071 assert_eq!(metric.halstead.unique_operators(), 3);
9072 assert_eq!(metric.halstead.total_operators(), 4);
9073 assert_eq!(metric.halstead.unique_operands(), 3);
9074 assert_eq!(metric.halstead.total_operands(), 3);
9075 });
9076
9077 let path = PathBuf::from("foo.irule");
9078 let parser = IrulesParser::new(source.as_bytes().to_vec(), &path, None);
9079 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
9080 // The inert quoted word is present as exactly one operand (not
9081 // dropped, not split): dropping it would mean the inert branch was
9082 // over-guarded.
9083 let quoted = ops
9084 .operands
9085 .iter()
9086 .filter(|o| o.as_str() == "\"hello world\"")
9087 .count();
9088 assert_eq!(quoted, 1, "inert quoted word must be one operand");
9089 let unique_operands: HashSet<&str> = ops.operands.iter().map(String::as_str).collect();
9090 assert_eq!(unique_operands.len(), 3, "operands were {:?}", ops.operands);
9091 }
9092
9093 /// Regression for the `QuotedWord` interpolation guard (the #277 /
9094 /// Bash-#180 / C#-#183 / PHP-#184 pattern). An interpolated
9095 /// `"$x is $y"` must contribute **zero** operands for the wrapping
9096 /// `QuotedWord`; the inner `$x` / `$y` `variable_substitution` nodes are
9097 /// walked separately and count on their own. Operands are `f`, `x`, `y`,
9098 /// `s`, `$x`, `$y` = 6 (7 before #1354 dropped the proc-body
9099 /// `braced_word`). If the guard regressed (wrapper classified
9100 /// `Operand`), the wrapper string would add a 7th operand. This is the
9101 /// branch that had no test before.
9102 #[cfg(feature = "irules")]
9103 #[test]
9104 fn irules_interpolated_quoted_word_no_double_count() {
9105 let source = "proc f {x y} {\n set s \"$x is $y\"\n}\n";
9106 check_metrics::<IrulesParser>(source, "foo.irule", |metric| {
9107 // After #695 only the `{` opener counts; the `}` closer no
9108 // longer adds an operator (was 4 unique / 6 total).
9109 assert_eq!(metric.halstead.unique_operators(), 3);
9110 assert_eq!(metric.halstead.total_operators(), 4);
9111 assert_eq!(metric.halstead.unique_operands(), 6);
9112 assert_eq!(metric.halstead.total_operands(), 6);
9113 });
9114
9115 let path = PathBuf::from("foo.irule");
9116 let parser = IrulesParser::new(source.as_bytes().to_vec(), &path, None);
9117 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
9118 // The wrapping interpolated string must NOT appear as an operand;
9119 // its inner substitutions must. The wrapper, if wrongly counted,
9120 // would surface as the quoted literal `"$x is $y"` (with quotes,
9121 // like the inert `"hello world"` operand). Match that exact token —
9122 // a substring check would false-match the proc-body `braced_word`
9123 // operand, which legitimately contains the source text.
9124 assert!(
9125 !ops.operands.iter().any(|o| o.as_str() == "\"$x is $y\""),
9126 "interpolated wrapper must not be an operand; operands were {:?}",
9127 ops.operands
9128 );
9129 assert!(
9130 ops.operands.iter().any(|o| o.as_str() == "$x")
9131 && ops.operands.iter().any(|o| o.as_str() == "$y"),
9132 "inner $x / $y substitutions must each be operands; operands were {:?}",
9133 ops.operands
9134 );
9135 let unique_operands: HashSet<&str> = ops.operands.iter().map(String::as_str).collect();
9136 assert_eq!(unique_operands.len(), 6, "operands were {:?}", ops.operands);
9137 }
9138
9139 /// Exercises the operator families not covered by
9140 /// `irules_operators_and_operands`: bitwise (`& | ^ ~ << >>`), ternary
9141 /// (`? :`), the keyword string comparators (`starts_with`, `ends_with`,
9142 /// `contains`, `matches`, `eq`, `ne`), and the keyword logical operator
9143 /// (`and`). Pins every operator-family arm in `get_op_type` plus the
9144 /// lesson-4 dedupe invariant.
9145 #[cfg(feature = "irules")]
9146 #[test]
9147 fn irules_bitwise_ternary_string_ops() {
9148 let source = "proc f { a b } {
9149 set bits [expr { $a & $b | $a ^ ~$b }]
9150 set sh [expr { $a << 2 | $b >> 1 }]
9151 set t [expr { $a > 0 ? $a : $b }]
9152 if { $a starts_with \"x\" && $b ends_with \"y\" } { return 1 }
9153 if { $a contains \"z\" || $b matches \"q\" } { return 2 }
9154 if { $a eq \"m\" and $b ne \"n\" } { return 3 }
9155 return $b
9156}
9157";
9158 check_metrics::<IrulesParser>(source, "foo.irule", |metric| {
9159 // After #695 only opening delimiters count: the `}`/`]`
9160 // closers no longer add operators (was 26 unique / 57 total).
9161 assert_eq!(metric.halstead.unique_operators(), 24);
9162 assert_eq!(metric.halstead.total_operators(), 43);
9163 // Operands fell 23 / 42 → 19 / 38 with #1354: the proc body
9164 // and the three single-statement `if` bodies are
9165 // `BracedWord` script kinds and no longer count as operands.
9166 assert_eq!(metric.halstead.unique_operands(), 19);
9167 assert_eq!(metric.halstead.total_operands(), 38);
9168 });
9169
9170 let path = PathBuf::from("foo.irule");
9171 let parser = IrulesParser::new(source.as_bytes().to_vec(), &path, None);
9172 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
9173 let unique_operators: HashSet<&str> = ops.operators.iter().map(String::as_str).collect();
9174 let unique_operands: HashSet<&str> = ops.operands.iter().map(String::as_str).collect();
9175 assert_eq!(
9176 unique_operators.len(),
9177 24,
9178 "dedupe(ops.operators) must equal n1; operators were {:?}",
9179 ops.operators
9180 );
9181 assert_eq!(
9182 unique_operands.len(),
9183 19,
9184 "dedupe(ops.operands) must equal n2; operands were {:?}",
9185 ops.operands
9186 );
9187 }
9188
9189 /// A bare `$x` produces one `variable_substitution` operand. Its inner
9190 /// `id` leaf (the *named* `Id` node — not the anonymous `Id2` token Tcl
9191 /// has there) must NOT be counted separately, or every variable
9192 /// reference double-counts. `get_op_type` excludes `Id` whose parent is
9193 /// a `VariableSubstitution`. Operands: `f`, the proc arg `x`, `return`
9194 /// and `$x` — four, with no duplicate (`total_operands()` == 4; the
9195 /// proc-body `braced_word` was a fifth before #1354). If the guard
9196 /// regressed, the inner `id` "x" would add a fifth operand occurrence
9197 /// (it text-collides with the proc arg `x`, so `u_operands` would stay
9198 /// 4 but `total_operands()` would rise to 5 — hence the total, not just
9199 /// the unique count, is asserted).
9200 #[cfg(feature = "irules")]
9201 #[test]
9202 fn irules_array_reference_bills_the_reference_and_the_index() {
9203 // The iRules twin of
9204 // `tcl_array_reference_bills_the_reference_and_the_index`. Until
9205 // `ArrayIndex` left the operand arm this fixture billed `(k)` and
9206 // `($i)` beside the six operands below — the grammar-dispatch §5
9207 // wrapper-plus-leaf count, and a divergence from Tcl.
9208 assert_ops_operands::<IrulesParser>(
9209 "set arr(k) 1\nset z \"$arr($i)\"\n",
9210 "foo.irule",
9211 6,
9212 vec!["arr", "k", "1", "z", "$arr($i)", "$i"],
9213 );
9214 }
9215
9216 #[cfg(feature = "irules")]
9217 #[test]
9218 fn irules_bare_variable_operand() {
9219 let source = "proc f {x} {\n return $x\n}\n";
9220 check_metrics::<IrulesParser>(source, "foo.irule", |metric| {
9221 // After #695 only the `{` opener counts (folded `{}`); the
9222 // `}` closer no longer adds an operator (was 3 unique / 5 total).
9223 assert_eq!(metric.halstead.unique_operators(), 2);
9224 assert_eq!(metric.halstead.total_operators(), 3);
9225 assert_eq!(metric.halstead.unique_operands(), 4);
9226 assert_eq!(metric.halstead.total_operands(), 4);
9227 });
9228
9229 let path = PathBuf::from("foo.irule");
9230 let parser = IrulesParser::new(source.as_bytes().to_vec(), &path, None);
9231 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
9232 let bare_var = ops.operands.iter().filter(|o| o.as_str() == "$x").count();
9233 assert_eq!(
9234 bare_var, 1,
9235 "bare $x must be exactly one operand (inner id leaf not double-counted); operands were {:?}",
9236 ops.operands
9237 );
9238 }
9239
9240 #[cfg(feature = "irules")]
9241 #[test]
9242 fn irules_braced_word_delimiter_is_not_an_operator() {
9243 // Regression: issue #1314. The iRules twin of
9244 // `tcl_braced_word_delimiter_is_not_an_operator` — the two
9245 // getters are deliberate clones, so the guard lands in both and
9246 // is asserted in both.
9247 //
9248 // One fixture covers the guard and its control: `{braced word}`
9249 // and `{y}` are values whose openers must not count, while the
9250 // handler body, the `if` condition and the `if` body are
9251 // `BracedWord` / `Expr` and must keep theirs. A guard keyed on
9252 // the brace alone would take `{}` out of the operator set
9253 // entirely and fail here.
9254 //
9255 // expected: operators `when`, `set` × 2, `if`, `contains`,
9256 // `[]`, `{}` × 3 → n1 = 6, N1 = 9. Every operand is distinct →
9257 // n2 = N2 = 7: `HTTP_REQUEST`, `a`, `b`, `HTTP::uri`, `"x"` and
9258 // the two braced words `{braced word}` and `{y}`. Before the
9259 // guard the two value openers added two more `{}` occurrences →
9260 // N1 = 11; before #1354 the handler and `if` bodies and the
9261 // inner `braced` / `word` / `y` were operands too → n2 = 12.
9262 check_metrics::<IrulesParser>(
9263 "when HTTP_REQUEST {\n set a {braced word}\n if {[HTTP::uri] contains \"x\"} { set b {y} }\n}\n",
9264 "foo.irule",
9265 |metric| {
9266 assert_eq!(metric.halstead.unique_operators(), 6);
9267 assert_eq!(metric.halstead.total_operators(), 9);
9268 assert_eq!(metric.halstead.unique_operands(), 7);
9269 assert_eq!(metric.halstead.total_operands(), 7);
9270 },
9271 );
9272 }
9273
9274 /// Regression for #563: the two Halstead `Display` labels must use the
9275 /// underscore key that matches the JSON/CSV field name, so a user can grep
9276 /// the same token across `Display` and JSON. The space-separated forms
9277 /// (`estimated program length` / `purity ratio`) were the only outliers,
9278 /// mirroring the `dump` fix in #562.
9279 #[cfg(feature = "cpp")]
9280 #[test]
9281 fn display_halstead_labels_use_underscore_keys() {
9282 check_metrics::<CppParser>("int a = 42;", "foo.cpp", |metric| {
9283 let out = metric.halstead.to_string();
9284 assert!(
9285 out.contains("estimated_program_length: "),
9286 "Display must use the underscore key `estimated_program_length`:\n{out}"
9287 );
9288 assert!(
9289 out.contains("purity_ratio: "),
9290 "Display must use the underscore key `purity_ratio`:\n{out}"
9291 );
9292 assert!(
9293 !out.contains("estimated program length"),
9294 "Display must not emit the space-separated `estimated program length`:\n{out}"
9295 );
9296 assert!(
9297 !out.contains("purity ratio"),
9298 "Display must not emit the space-separated `purity ratio`:\n{out}"
9299 );
9300 });
9301 }
9302
9303 /// `@"…"` is one `string_literal` holding its `@` as a child, so the
9304 /// literal is the operand, keyed by its whole text, and the marker is
9305 /// not an operator on top — it was, which planted a phantom `@` in
9306 /// n1 for a file whose only `@` was in NSString literals and billed
9307 /// the same byte in both streams. Boxing (`@42`) keeps its `@`: there
9308 /// the token is a child of the `at_expression`, not of the literal.
9309 #[cfg(feature = "objc")]
9310 #[test]
9311 fn objc_nsstring_literal_is_one_operand() {
9312 // expected: [n1, N1, n2, N2]. Before the guard the first two rows
9313 // read [8, 8, 5, 5] and [8, 9, 6, 6] — one `@` operator per
9314 // literal; the boxing control is unchanged.
9315 let cases = [
9316 ("NSString *s = @\"str\";", [7, 7, 5, 5]),
9317 ("NSString *t = @\"x\" @\"y\";", [7, 7, 6, 6]),
9318 ("NSNumber *n = @42;", [8, 8, 5, 5]),
9319 ];
9320 for (body, counts) in cases {
9321 let source = format!("@implementation Foo\n- (void)m {{\n {body}\n}}\n@end\n");
9322 assert_halstead_counts::<ObjcParser>(&source, "foo.m", counts, body);
9323 }
9324 assert_ops_operands::<ObjcParser>(
9325 "@implementation Foo\n- (void)m {\n NSString *s = @\"str\";\n}\n@end\n",
9326 "foo.m",
9327 5,
9328 vec!["Foo", "m", "NSString", "s", "@\"str\""],
9329 );
9330 }
9331
9332 /// Comprehensive Objective-C Halstead fixture exercising a message
9333 /// send (`[self log:@"hi"]`), an ObjC string literal (`@"hi"`), an
9334 /// `if`, a short-circuit `&&`, arithmetic (`+`), comparisons, and
9335 /// assignment. Pins every field and enforces the lesson-4 invariants
9336 /// `unique_operators == n1` / `unique_operands == n2` via the
9337 /// independent `--ops` store.
9338 #[cfg(feature = "objc")]
9339 #[test]
9340 fn objc_operators_and_operands() {
9341 let source = "@implementation Foo
9342- (int)bar:(int)x {
9343 int y = x + 1;
9344 if (x > 0 && y < 10) {
9345 [self log:@\"hi\"];
9346 }
9347 return y;
9348}
9349@end
9350";
9351 check_metrics::<ObjcParser>(source, "foo.m", |metric| {
9352 // n1 = 14 unique operators:
9353 // `&&`, `()`, `+`, `-`, `:`, `;`, `<`, `=`, `>`,
9354 // `[]` (message send), `if`, `int`, `return`, `{}`.
9355 // The `@` of `@"hi"` is part of the literal's operand
9356 // key, not an operator (see `objc_nsstring_literal_is_one_operand`).
9357 // n2 = 10 unique operands:
9358 // `Foo`, `bar`, `log`, `self`, `x`, `y`, `0`, `1`, `10`,
9359 // `@"hi"` (the ObjC string literal).
9360 assert_eq!(metric.halstead.unique_operators(), 14);
9361 assert_eq!(metric.halstead.unique_operands(), 10);
9362 insta::assert_json_snapshot!(metric.halstead, @r#"
9363 {
9364 "unique_operators": 14,
9365 "total_operators": 22,
9366 "unique_operands": 10,
9367 "total_operands": 14,
9368 "length": 36,
9369 "estimated_program_length": 86.52224985768008,
9370 "purity_ratio": 2.403395829380002,
9371 "vocabulary": 24,
9372 "volume": 165.0586500259616,
9373 "difficulty": 9.8,
9374 "level": 0.1020408163265306,
9375 "effort": 1617.5747702544238,
9376 "time": 89.86526501413465,
9377 "bugs": 0.04593266617952463
9378 }
9379 "#);
9380 });
9381 // Lesson-4 invariant: dedupe(ops.operands) == n2 (10), via the
9382 // independent text-keyed `--ops` store.
9383 assert_ops_operands::<ObjcParser>(
9384 source,
9385 "foo.m",
9386 10,
9387 vec![
9388 "Foo", "bar", "log", "self", "x", "y", "0", "1", "10", "@\"hi\"",
9389 ],
9390 );
9391 }
9392
9393 /// #1316 fixture separating the vocabulary and occurrence axes
9394 /// (#1294): `'x'` appears twice, so a correct fix adds four `n2`
9395 /// entries and five `N2` hits. The `'ab'` multi-character constant
9396 /// is also the grammar-dispatch section 5 pin — that literal carries
9397 /// *two* `character` children and must still bill one operand.
9398 ///
9399 /// Both #1316 fixtures are plain C, which every C-family grammar
9400 /// parses to the same shape, so one source proves the same thing
9401 /// about each of the four clones.
9402 #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
9403 const C_FAMILY_CHAR_REPEATS: &str =
9404 "char a = 'x';\nchar b = 'x';\nchar c = 'y';\nchar d = '\\n';\nint e = 'ab';\n";
9405
9406 /// #1316 fixture holding all five spellings the grammars admit. Each
9407 /// opens on a distinct delimiter kind (`'`, `L'`, `u'`, `U'`, `u8'`),
9408 /// and operands key on source text, so the five are five vocabulary
9409 /// entries rather than one.
9410 #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
9411 const C_FAMILY_CHAR_PREFIXES: &str =
9412 "char a = 'x';\nchar b = L'x';\nchar c = u'x';\nchar d = U'x';\nchar e = u8'x';\n";
9413
9414 /// Asserts both #1316 fixtures for one C-family language, through
9415 /// the metrics store *and* the text-keyed `--ops` store (the
9416 /// lesson-4 invariant `n2 == len(dedupe(ops.operands))`).
9417 ///
9418 /// The `--ops` half pins *which text* each literal is billed under,
9419 /// which the counts cannot see: billing a literal's `character`
9420 /// payload instead of the whole literal keeps `n2` at 9 while the
9421 /// vocabulary silently becomes `x` rather than `'x'`. (Billing
9422 /// *both* is caught earlier, by the counts.) It is a second *walk*
9423 /// rather than a second classification — `ops_inner` reads the keys
9424 /// of the same `HalsteadMaps` — which is worth knowing before
9425 /// reading it as independent corroboration of the count.
9426 #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
9427 #[track_caller]
9428 fn assert_char_literal_operands<T: crate::MetricSuite>(file: &str, label: &str) {
9429 // `char` x4 and `int` are text-keyed primitive operators, so
9430 // n1 = 4 (`;`, `=`, `char`, `int`) and N1 = 5 + 5 + 4 + 1.
9431 // Operands: `a`..`e`, plus `'x'` (twice), `'y'`, `'\n'`, `'ab'`.
9432 let repeats = format!("{label}: repeated / escaped / multi-char literals");
9433 assert_halstead_counts::<T>(C_FAMILY_CHAR_REPEATS, file, [4, 15, 9, 10], &repeats);
9434 assert_ops_operands::<T>(
9435 C_FAMILY_CHAR_REPEATS,
9436 file,
9437 9,
9438 vec!["a", "b", "c", "d", "e", "'x'", "'y'", "'\\n'", "'ab'"],
9439 );
9440
9441 // Every declaration is `char` here, so the `int` primitive
9442 // operator of the other fixture is gone and n1 drops to 3. Ten
9443 // distinct operands, each seen once.
9444 let prefixes = format!("{label}: L / u / U / u8 prefixed literals");
9445 assert_halstead_counts::<T>(C_FAMILY_CHAR_PREFIXES, file, [3, 15, 10, 10], &prefixes);
9446 assert_ops_operands::<T>(
9447 C_FAMILY_CHAR_PREFIXES,
9448 file,
9449 10,
9450 vec![
9451 "a", "b", "c", "d", "e", "'x'", "L'x'", "u'x'", "U'x'", "u8'x'",
9452 ],
9453 );
9454 }
9455
9456 /// Regression for #1316: a C-family character literal is a Halstead
9457 /// operand.
9458 ///
9459 /// `char_literal` was in no arm of `CCode` / `CppCode` /
9460 /// `MozcppCode` / `ObjcCode`'s `get_op_type`, so a character literal
9461 /// contributed *nothing* — not an operator (correct) and not an
9462 /// operand (wrong) — while Rust, Java, Kotlin, C#, Go and Elixir all
9463 /// counted theirs. Both fixtures measured `n2` 5, `N2` 5 before the
9464 /// fix: the five declared identifiers and not one literal.
9465 ///
9466 /// Each language asserts separately over the same source rather than
9467 /// sharing one call, so reverting one clone's arm fails that row
9468 /// alone (grammar-dispatch section 11) — verified by perturbing each
9469 /// of the four arms in turn. A single shared assertion would be
9470 /// satisfied by whichever clone still had the arm.
9471 ///
9472 /// Mozcpp owns no file extension, so no integration snapshot ever
9473 /// reaches its clone; its row is the whole coverage that arm has.
9474 #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
9475 #[test]
9476 fn c_family_char_literals_are_operands() {
9477 assert_char_literal_operands::<CParser>("chars.c", "c");
9478 assert_char_literal_operands::<CppParser>("chars.cpp", "cpp");
9479 assert_char_literal_operands::<MozcppParser>("chars.cpp", "mozcpp");
9480 assert_char_literal_operands::<ObjcParser>("chars.m", "objc");
9481 }
9482
9483 /// ObjC boxes a character literal as `@'y'` — an `at_expression`
9484 /// wrapping the same `char_literal`, with the `@` counted as its own
9485 /// operator. The wrapper is in no operand arm, so the boxed form
9486 /// bills exactly the literal it wraps and stays distinct from a bare
9487 /// one (#1316).
9488 #[cfg(feature = "objc")]
9489 #[test]
9490 fn objc_boxed_char_literal_is_one_operand() {
9491 let source = "char a = 'x';\nid b = @'y';\n";
9492 // n1: `char`, `=`, `;`, `@`. N1: 1 + 2 + 2 + 1.
9493 // n2 / N2: `a`, `b`, `'x'`, `'y'` — `id` is a `typedefed_specifier`
9494 // and is classified by neither arm. Before #1316 this was n2 2,
9495 // N2 2.
9496 assert_halstead_counts::<ObjcParser>(source, "boxed.m", [4, 6, 4, 4], "objc @'y'");
9497 assert_ops_operands::<ObjcParser>(source, "boxed.m", 4, vec!["a", "b", "'x'", "'y'"]);
9498 }
9499
9500 /// Walks both #1316 fixtures under all four C-family `Getter`s and
9501 /// pins the two grammar facts the new operand arm rests on.
9502 ///
9503 /// * **Grammar-dispatch section 1.** In the positions these
9504 /// fixtures exercise, every node the grammar spells `char_literal`
9505 /// carries the one `kind_id` the arm lists. That is the weaker
9506 /// half of the alias evidence — an alias arises in a *different*
9507 /// syntactic position, which no fixture can enumerate. The strong
9508 /// half is that these generated enums do carry numeric-suffix
9509 /// aliases in quantity (`language_c.rs` alone has ninety) and none
9510 /// of the four spells a `CharLiteral2`, so its absence is a
9511 /// measurement rather than a silence. This loop is what notices if
9512 /// a grammar bump changes that under an existing fixture.
9513 /// * **Grammar-dispatch section 5.** No child of a `char_literal` is
9514 /// classified. That is what makes listing the wrapper safe rather
9515 /// than a wrapper/leaf double count: the opening delimiter, the
9516 /// closing `'`, and the `character` / `escape_sequence` payload
9517 /// must all stay `Unknown`, or every literal would bill two
9518 /// operands and a prefixed one three.
9519 ///
9520 /// Both loops are non-vacuous by assertion, since a fixture that
9521 /// stopped containing a character literal would otherwise make this
9522 /// test pass having checked nothing.
9523 #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
9524 #[test]
9525 fn c_family_char_literal_internals_stay_unclassified() {
9526 #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
9527 fn check<L: LanguageInfo + Getter>(char_literal: u16, label: &str) {
9528 let mut literals = 0_usize;
9529 let mut children = 0_usize;
9530 for source in [C_FAMILY_CHAR_REPEATS, C_FAMILY_CHAR_PREFIXES] {
9531 for_each_node_with_chain::<L>(source.as_bytes(), |node, chain| {
9532 if node.kind() == "char_literal" {
9533 assert_eq!(
9534 node.kind_id(),
9535 char_literal,
9536 "{label}: a `char_literal` carries kind_id {} rather than the \
9537 {char_literal} the operand arm lists — an alias the arm cannot see",
9538 node.kind_id()
9539 );
9540 literals += 1;
9541 }
9542 if chain
9543 .last()
9544 .is_none_or(|parent| parent.kind_id() != char_literal)
9545 {
9546 return;
9547 }
9548 children += 1;
9549 // `_with_code` is the spelling `compute_halstead`
9550 // calls. The default forwards to the byte-less form,
9551 // so today the two agree for every C-family
9552 // language — which is exactly why asking the wrong
9553 // one would read as correct right up until one of
9554 // these four grew an override (grammar-dispatch
9555 // section 7).
9556 assert!(
9557 matches!(
9558 L::get_op_type_with_code(
9559 node,
9560 source.as_bytes(),
9561 Ancestors::known(chain)
9562 ),
9563 TokenRole::Unknown
9564 ),
9565 "{label}: `{}` inside a character literal is classified, so the \
9566 literal now double-counts against its wrapper",
9567 node.kind()
9568 );
9569 });
9570 }
9571 // Ten literals across the two fixtures; each holds two
9572 // delimiters plus at least one payload leaf, and `'ab'` two.
9573 assert_eq!(literals, 10, "{label}: fixtures lost a character literal");
9574 assert_eq!(children, 31, "{label}: fixtures lost a literal's internals");
9575 }
9576
9577 check::<CCode>(C::CharLiteral as u16, "c");
9578 check::<CppCode>(Cpp::CharLiteral as u16, "cpp");
9579 check::<MozcppCode>(Mozcpp::CharLiteral as u16, "mozcpp");
9580 check::<ObjcCode>(Objc::CharLiteral as u16, "objc");
9581 }
9582
9583 /// #1361 fixture pairing the two spellings of a member access whose
9584 /// receiver is the enclosing object: `this->x` and `p->x`.
9585 ///
9586 /// The pairing is the point, not decoration. `field_expression` is
9587 /// `<receiver> -> <field>`, so the two lines are the same AST shape
9588 /// and must bill the same *kinds* of thing — a receiver operand, the
9589 /// `->` operator, a field operand. Before the fix `this` was in
9590 /// neither arm, so the two shapes scored differently and `m1` billed
9591 /// a `->` with only one operand around it.
9592 ///
9593 /// Plain C++ that the mozcpp fork parses identically, so one source
9594 /// proves the same thing about both clones.
9595 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
9596 const CPP_THIS_RECEIVER_PARITY: &str = "struct S {
9597 int x;
9598 int m1() { return this->x; }
9599 int m2(S* p) { return p->x; }
9600 };";
9601
9602 /// #1361 fixture holding one representative position per kind of
9603 /// container the pinned `tree-sitter-cpp` can put a `this` in: the
9604 /// `->` receiver, a dereference inside a parenthesised expression, a
9605 /// bare return value, both lambda-capture spellings (`[this]` and
9606 /// `[=, this]`), a `decltype` trailing return type, a constructor
9607 /// body following a member-initialiser list, and a call argument.
9608 ///
9609 /// Representative, not exhaustive — `delete this;`, `throw this;`,
9610 /// `[&, this]`, a default member initialiser and a `this->template
9611 /// f<int>()` all parse cleanly too. Every one of them is the same
9612 /// childless `{this:215}` leaf under an unclassified wrapper, which
9613 /// is precisely why the arm needs no guard and why enumerating them
9614 /// would add rows without adding a distinct path (grammar-dispatch
9615 /// section 11).
9616 ///
9617 /// C++23's explicit object parameter (`int g(this S&& self)`) is
9618 /// deliberately absent: the pinned grammar cannot parse it and emits
9619 /// `type_identifier "this"` plus an `ERROR`, which
9620 /// `for_each_node_with_chain` rejects outright. That spelling is
9621 /// already an operand through `TypeIdentifier`, so the construct is
9622 /// unaffected by this arm either way.
9623 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
9624 const CPP_THIS_POSITIONS: &str = "struct S {
9625 int x;
9626 void g(S*);
9627 int arrow() { return this->x; }
9628 int deref() { return (*this).x; }
9629 S* ret() { return this; }
9630 void cap() { auto l = [this]() { return x; }; }
9631 void cap2() { auto l = [=, this]() { return x; }; }
9632 auto trail() -> decltype(this->x) { return x; }
9633 S(int v) : x(v) { this->x = v; }
9634 void call() { g(this); }
9635 };";
9636
9637 /// Asserts `CPP_THIS_RECEIVER_PARITY` for one C++-family language,
9638 /// through the metrics store *and* the text-keyed `--ops` store
9639 /// (the lesson-4 invariant `n2 == len(dedupe(ops.operands))`).
9640 ///
9641 /// The `--ops` half pins *which text* the new operand is billed
9642 /// under, which the counts alone cannot see: billing the enclosing
9643 /// `field_expression` instead of the `this` leaf would hold `n2` at
9644 /// 6 while the vocabulary silently became `this->x`.
9645 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
9646 #[track_caller]
9647 fn assert_this_receiver_parity<T: crate::MetricSuite>(file: &str, label: &str) {
9648 // Operators, keyed by kind_id except the text-keyed primitives:
9649 // `{` x3 (class body, `m1`, `m2`), `int` x3, `;` x4 (the field,
9650 // the two returns, the struct terminator), `(` x2, `return` x2,
9651 // `->` x2, and the one `*` of `S* p`. `struct` is in no arm, and
9652 // #695 dropped every closing delimiter. n1 = 7, N1 = 17.
9653 //
9654 // Operands, keyed by source text: `S` x2 (the name and the
9655 // parameter type), `x` x3, `m1`, `m2`, `p` x2, and — the fix —
9656 // `this` x1. n2 = 6, N2 = 10. Before #1361: n2 5, N2 9, with
9657 // every operator count identical, which is the whole claim.
9658 let counts = format!("{label}: this->x and p->x are the same shape");
9659 assert_halstead_counts::<T>(CPP_THIS_RECEIVER_PARITY, file, [7, 17, 6, 10], &counts);
9660 assert_ops_operands::<T>(
9661 CPP_THIS_RECEIVER_PARITY,
9662 file,
9663 6,
9664 vec!["S", "x", "m1", "m2", "p", "this"],
9665 );
9666 }
9667
9668 /// Regression for #1361: a C++ `this` is a Halstead operand.
9669 ///
9670 /// `Cpp::This` / `Mozcpp::This` were in neither arm of
9671 /// `CppCode::get_op_type` / `MozcppCode::get_op_type`, so a `this`
9672 /// contributed *nothing* — not an operator and not an operand —
9673 /// while eleven of the fourteen languages here bill their
9674 /// self-reference as an operand. Same shape as #1316's character
9675 /// literals, and the inverse of the #1351-#1355 over-counts.
9676 ///
9677 /// Each language asserts separately over the same source rather than
9678 /// sharing one call, so reverting one clone's arm fails that row
9679 /// alone (grammar-dispatch section 11). Mozcpp owns no file
9680 /// extension, so no integration snapshot reaches its clone; this row
9681 /// and `cpp_and_mozcpp_agree_on_this` in `tests/parity/` are the
9682 /// whole coverage that arm has.
9683 #[cfg(all(feature = "cpp", feature = "mozcpp"))]
9684 #[test]
9685 fn cpp_this_is_an_operand() {
9686 assert_this_receiver_parity::<CppParser>("this.cpp", "cpp");
9687 assert_this_receiver_parity::<MozcppParser>("this.cpp", "mozcpp");
9688 }
9689
9690 /// The `This` arm is unconditional: every `this` in
9691 /// `CPP_THIS_POSITIONS` classifies as an operand whatever encloses
9692 /// it (#1361).
9693 ///
9694 /// The counts in `cpp_this_is_an_operand` are measured on one
9695 /// position only. An arm reached through a parent-scoped guard —
9696 /// the shape the neighbouring `RawStringLiteral` `LPAREN` arm uses,
9697 /// and the shape a reviewer might reasonably add here — would pass
9698 /// that test and still drop `this` in a lambda capture or a
9699 /// `decltype`. This walk is what makes the arm's unconditional
9700 /// reach a measurement rather than an assumption.
9701 ///
9702 /// Named for the *property* rather than for the fixture: the fixture
9703 /// samples one position per container kind and is deliberately not
9704 /// exhaustive (see `CPP_THIS_POSITIONS`), so a name promising "every
9705 /// position" would claim more than it checks.
9706 #[cfg(all(feature = "cpp", feature = "mozcpp"))]
9707 #[test]
9708 fn cpp_this_is_an_operand_regardless_of_position() {
9709 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
9710 fn check<L: LanguageInfo + Getter>(label: &str) {
9711 let mut seen = 0_usize;
9712 for_each_node_with_chain::<L>(CPP_THIS_POSITIONS.as_bytes(), |node, chain| {
9713 if node.kind() != "this" {
9714 return;
9715 }
9716 seen += 1;
9717 // `_with_code` is the spelling `compute_halstead` calls.
9718 // The default forwards to the byte-less form, so the two
9719 // agree for both languages today — which is exactly why
9720 // asking the wrong one would read as correct right up
9721 // until one of them grew an override
9722 // (grammar-dispatch section 7).
9723 assert!(
9724 matches!(
9725 L::get_op_type_with_code(
9726 node,
9727 CPP_THIS_POSITIONS.as_bytes(),
9728 Ancestors::known(chain)
9729 ),
9730 TokenRole::Operand
9731 ),
9732 "{label}: a `this` under `{}` is not an operand",
9733 chain.last().map_or("<root>", Node::kind)
9734 );
9735 });
9736 // One per position: `->` receiver, `(*this)`, bare return,
9737 // `[this]`, `[=, this]`, `decltype(this->x)`, the
9738 // constructor body, and the call argument.
9739 assert_eq!(seen, 8, "{label}: fixture lost a `this` position");
9740 }
9741
9742 check::<CppCode>("cpp");
9743 check::<MozcppCode>("mozcpp");
9744 }
9745
9746 /// Pins the two grammar facts the #1361 operand arm rests on, across
9747 /// both fixtures and both languages.
9748 ///
9749 /// * **Grammar-dispatch section 1.** Every node the grammar spells
9750 /// `this` carries the one `kind_id` the arm lists. That is the
9751 /// weaker half of the alias evidence — an alias arises in a
9752 /// *different* syntactic position, which no fixture can enumerate.
9753 /// The strong half is that these generated enums carry
9754 /// numeric-suffix aliases in quantity (`FunctionDefinition2..4`,
9755 /// `QualifiedIdentifier2..4`, `LPAREN2`, `Try2`, `GT2` in this one
9756 /// file) and neither spells a `This2` — `This = 215` is the sole
9757 /// variant rendering `"this"` in `language_cpp.rs` and
9758 /// `language_mozcpp.rs`. This loop is what notices if a grammar
9759 /// bump changes that under an existing fixture.
9760 /// * **Grammar-dispatch section 5.** A `this` is a childless leaf and
9761 /// no node containing it is classified. That is both halves of the
9762 /// double-count question: nothing below it can be billed a second
9763 /// time, and no `field_expression` / `pointer_expression` /
9764 /// `lambda_capture_specifier` / `argument_list` wrapper bills the
9765 /// same source text from above.
9766 #[cfg(all(feature = "cpp", feature = "mozcpp"))]
9767 #[test]
9768 fn cpp_this_is_a_childless_unaliased_leaf() {
9769 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
9770 fn check<L: LanguageInfo + Getter>(this: u16, label: &str) {
9771 let mut seen = 0_usize;
9772 for source in [CPP_THIS_RECEIVER_PARITY, CPP_THIS_POSITIONS] {
9773 for_each_node_with_chain::<L>(source.as_bytes(), |node, chain| {
9774 if node.kind() != "this" {
9775 return;
9776 }
9777 seen += 1;
9778 assert_eq!(
9779 node.kind_id(),
9780 this,
9781 "{label}: a `this` carries kind_id {} rather than the {this} the \
9782 operand arm lists — an alias the arm cannot see",
9783 node.kind_id()
9784 );
9785 assert_eq!(
9786 node.child_count(),
9787 0,
9788 "{label}: `this` grew children, which the operand arm would now \
9789 double-count against"
9790 );
9791 assert!(!chain.is_empty(), "{label}: a `this` cannot be the root");
9792 // Every ancestor, not just the parent. The claim is
9793 // that *no node containing* a `this` is classified,
9794 // and `(*this)` alone puts two wrappers between the
9795 // leaf and the nearest classified node — checking
9796 // only `chain.last()` would assert something
9797 // narrower than the comment on the arm promises.
9798 // `chain` is the *node's* ancestry, so the ancestor
9799 // at `i` has `chain[..i]` for its own.
9800 for (i, ancestor) in chain.iter().enumerate() {
9801 assert!(
9802 matches!(
9803 L::get_op_type_with_code(
9804 ancestor,
9805 source.as_bytes(),
9806 Ancestors::known(&chain[..i])
9807 ),
9808 TokenRole::Unknown
9809 ),
9810 "{label}: `{}` contains a `this` and is itself classified, so the \
9811 reference now counts twice",
9812 ancestor.kind()
9813 );
9814 }
9815 });
9816 }
9817 // One `this` in the parity fixture, eight in the positions
9818 // fixture. Asserted so a fixture that stopped containing one
9819 // cannot make this test pass having checked nothing.
9820 assert_eq!(seen, 9, "{label}: fixtures lost a `this`");
9821 }
9822
9823 check::<CppCode>(Cpp::This as u16, "cpp");
9824 check::<MozcppCode>(Mozcpp::This as u16, "mozcpp");
9825 }
9826
9827 /// Builds a `HalsteadMaps` from explicit occurrence counts.
9828 ///
9829 /// The per-language tests above reach these maps only through a
9830 /// parse, which cannot produce a *chosen* overlap between a child
9831 /// and its parent — the cases `merge` exists to get right.
9832 fn halstead_maps_of<'a>(
9833 operators: &[(u16, u64)],
9834 primitive_operators: &[(&'a [u8], u64)],
9835 operands: &[(&'a [u8], u64)],
9836 ) -> HalsteadMaps<'a> {
9837 HalsteadMaps {
9838 operators: operators.iter().copied().collect(),
9839 primitive_operators: primitive_operators.iter().copied().collect(),
9840 operands: operands.iter().copied().collect(),
9841 }
9842 }
9843
9844 /// `HalsteadMaps::operators` must stay on the crate's integer hasher.
9845 ///
9846 /// Swapping a hasher moves no metric value, so every other test in
9847 /// this file passes just as well with #1108 reverted. Both halves
9848 /// here are needed: the typed binding stops compiling if the field
9849 /// goes back to a default-hasher `HashMap`, and the `type_name`
9850 /// comparison still fails at runtime if `IntKeyHashMap` itself is
9851 /// ever redefined to wrap `RandomState`.
9852 ///
9853 /// The two text-keyed maps are pinned to SipHash in the same test,
9854 /// because moving *them* would be a regression rather than an
9855 /// optimisation. `crate::int_hash`'s module doc is the single place
9856 /// that argues why analysed source text does not qualify.
9857 #[test]
9858 fn halstead_operator_map_uses_the_int_key_hasher() {
9859 use std::any::{type_name, type_name_of_val};
9860 use std::hash::BuildHasherDefault;
9861
9862 use crate::int_hash::IntKeyHasher;
9863
9864 let maps = HalsteadMaps::new();
9865
9866 let operators: &IntKeyHashMap<u16, u64> = &maps.operators;
9867 assert_eq!(
9868 type_name_of_val(operators.hasher()),
9869 type_name::<BuildHasherDefault<IntKeyHasher>>(),
9870 "the kind_id-keyed operator map must use the int_hash hasher"
9871 );
9872
9873 let siphash = type_name::<std::collections::hash_map::RandomState>();
9874 assert_eq!(
9875 type_name_of_val(maps.operands.hasher()),
9876 siphash,
9877 "operand keys come from the analysed source, so the keyed hash \
9878 is what stops a crafted file from flooding this map"
9879 );
9880 assert_eq!(
9881 type_name_of_val(maps.primitive_operators.hasher()),
9882 siphash,
9883 "primitive-operator keys come from the analysed source, so the \
9884 keyed hash is what stops a crafted file from flooding this map"
9885 );
9886 }
9887
9888 /// `merge` sums overlapping keys and adopts disjoint ones, in all
9889 /// three maps, and `finalize` reads the union back as n1/N1/n2/N2.
9890 ///
9891 /// Every count differs from every other and none is zero, so a
9892 /// dropped key, an overwrite where an addition belongs, or a map
9893 /// crossed with its neighbour all change the totals.
9894 #[test]
9895 fn halstead_maps_merge_sums_overlaps_and_adopts_disjoint_keys() {
9896 let mut parent = halstead_maps_of(
9897 &[(1, 2), (2, 3)],
9898 &[(b"int", 1)],
9899 &[(b"alpha", 4), (b"beta", 7)],
9900 );
9901 let child = halstead_maps_of(
9902 &[(2, 5), (7, 11)],
9903 &[(b"double", 13)],
9904 &[(b"alpha", 17), (b"gamma", 19)],
9905 );
9906
9907 parent.merge(&child);
9908
9909 // expected: operators {1: 2, 2: 3+5, 7: 11}; primitives
9910 // {int: 1, double: 13}; operands {alpha: 4+17, beta: 7,
9911 // gamma: 19}.
9912 assert_eq!(
9913 parent,
9914 halstead_maps_of(
9915 &[(1, 2), (2, 8), (7, 11)],
9916 &[(b"int", 1), (b"double", 13)],
9917 &[(b"alpha", 21), (b"beta", 7), (b"gamma", 19)],
9918 )
9919 );
9920
9921 let mut stats = Stats::default();
9922 parent.finalize(&mut stats);
9923 // expected: n1 = 3 kind ids + 2 primitives; N1 = (2+8+11) +
9924 // (1+13); n2 = 3 texts; N2 = 21+7+19.
9925 assert_eq!(stats.unique_operators(), 5);
9926 assert_eq!(stats.total_operators(), 35);
9927 assert_eq!(stats.unique_operands(), 3);
9928 assert_eq!(stats.total_operands(), 47);
9929 }
9930
9931 /// Merging an empty child leaves the parent untouched.
9932 ///
9933 /// A space with no operators or operands is the common case for a
9934 /// leaf getter or an empty function body, and `finalize` runs on
9935 /// the parent afterwards either way.
9936 #[test]
9937 fn halstead_maps_merge_of_empty_child_is_a_no_op() {
9938 let mut parent = halstead_maps_of(&[(3, 5)], &[(b"char", 2)], &[(b"delta", 9)]);
9939 let before = parent.clone();
9940
9941 parent.merge(&HalsteadMaps::new());
9942
9943 assert_eq!(parent, before);
9944
9945 let mut stats = Stats::default();
9946 parent.finalize(&mut stats);
9947 // expected: n1 = 1 kind id + 1 primitive; N1 = 5 + 2; n2 = 1;
9948 // N2 = 9.
9949 assert_eq!(stats.unique_operators(), 2);
9950 assert_eq!(stats.total_operators(), 7);
9951 assert_eq!(stats.unique_operands(), 1);
9952 assert_eq!(stats.total_operands(), 9);
9953 }
9954
9955 /// Folding a chain of nested spaces bottom-up must reach the union
9956 /// of every level, re-merging already-merged maps on the way up.
9957 ///
9958 /// This is what `spaces.rs` and `ops.rs` actually do: each space is
9959 /// merged into its parent as the walk pops it, so by the time the
9960 /// root sees a grandchild's counts they have already passed through
9961 /// one `merge`. The literal expectation below is what discriminates
9962 /// — the `nested == flat` cross-check on its own does not, because
9963 /// any entry-wise fold over the same levels agrees with itself
9964 /// however it is associated, including a broken one.
9965 #[test]
9966 fn halstead_maps_merge_folds_a_nested_chain() {
9967 let levels = [
9968 halstead_maps_of(&[(1, 1)], &[(b"int", 1)], &[(b"a", 1)]),
9969 halstead_maps_of(&[(1, 2), (2, 3)], &[], &[(b"a", 2), (b"b", 4)]),
9970 halstead_maps_of(&[(2, 5)], &[(b"long", 6)], &[(b"b", 7)]),
9971 halstead_maps_of(&[(3, 8)], &[(b"int", 9)], &[(b"c", 10)]),
9972 ];
9973
9974 // Bottom-up: the deepest level folds into its parent, that
9975 // result into *its* parent, and so on up to the root.
9976 let mut nested = levels[levels.len() - 1].clone();
9977 for level in levels.iter().rev().skip(1) {
9978 let mut outer = level.clone();
9979 outer.merge(&nested);
9980 nested = outer;
9981 }
9982
9983 // Flat: every level merged directly into the root.
9984 let mut flat = levels[0].clone();
9985 for level in &levels[1..] {
9986 flat.merge(level);
9987 }
9988
9989 // expected: every key summed across the four levels — operators
9990 // {1: 1+2, 2: 3+5, 3: 8}, primitives {int: 1+9, long: 6},
9991 // operands {a: 1+2, b: 4+7, c: 10}.
9992 assert_eq!(
9993 nested,
9994 halstead_maps_of(
9995 &[(1, 3), (2, 8), (3, 8)],
9996 &[(b"int", 10), (b"long", 6)],
9997 &[(b"a", 3), (b"b", 11), (b"c", 10)],
9998 )
9999 );
10000 assert_eq!(nested, flat);
10001
10002 let mut stats = Stats::default();
10003 nested.finalize(&mut stats);
10004 // expected: n1 = 3 kind ids + 2 primitives; N1 = (3+8+8) +
10005 // (10+6); n2 = 3 texts; N2 = 3+11+10.
10006 assert_eq!(stats.unique_operators(), 5);
10007 assert_eq!(stats.total_operators(), 35);
10008 assert_eq!(stats.unique_operands(), 3);
10009 assert_eq!(stats.total_operands(), 24);
10010 }
10011
10012 /// A `kind_id` at the top of the `u16` range must behave like any
10013 /// other key.
10014 ///
10015 /// The largest grammar in the workspace (`mozcpp`) tops out around
10016 /// 640 symbols, so nothing near `u16::MAX` occurs today — but the
10017 /// map is keyed by the raw id, and a dense-array representation
10018 /// (the shape #1108 considered and rejected) is exactly what such a
10019 /// key would break. Pinning it keeps that trade-off honest if the
10020 /// representation is ever revisited.
10021 #[test]
10022 fn halstead_maps_handle_the_full_kind_id_range() {
10023 let mut parent = halstead_maps_of(&[(0, 3), (u16::MAX, 5)], &[], &[]);
10024 parent.merge(&halstead_maps_of(&[(u16::MAX, 7)], &[], &[]));
10025
10026 let mut stats = Stats::default();
10027 parent.finalize(&mut stats);
10028 // expected: two distinct kind ids, occurrences 3 and 5+7.
10029 assert_eq!(stats.unique_operators(), 2);
10030 assert_eq!(stats.total_operators(), 15);
10031 }
10032}