big_code_analysis/metrics/halstead.rs
1// Per-language metric and AST modules deliberately consume the macro-
2// generated tree-sitter token enums via `use crate::*` and `use Foo::*`
3// inside match expressions — explicit imports would list dozens of
4// variants per arm and obscure the per-language token sets that are the
5// point of these files. Allowed at the module level rather than per
6// function so the per-language impl blocks stay readable.
7#![allow(
8 clippy::doc_markdown,
9 clippy::enum_glob_use,
10 clippy::match_wildcard_for_single_variants,
11 clippy::similar_names,
12 clippy::unused_self,
13 clippy::wildcard_imports
14)]
15// Metric counts (token, function, branch, argument, etc.) are stored as
16// `usize` and crossed with `f64` averages, ratios, and Halstead scores
17// across the cyclomatic / MI / Halstead computations. The `usize as f64`
18// and `f64 as usize` casts are intentional and snapshot-anchored — every
19// site is bounded by the count it came from. Allowing the lints at the
20// module level keeps the metric arithmetic legible.
21#![allow(
22 clippy::cast_precision_loss,
23 clippy::cast_possible_truncation,
24 clippy::cast_sign_loss
25)]
26
27use std::collections::HashMap;
28
29use std::fmt;
30
31// The operator / operand classification is syntactic and lives beside
32// the per-language `Getter` tables that produce it; re-exported here
33// because this metric is what consumes it.
34pub use crate::token_role::TokenRole;
35
36/// The former name of [`TokenRole`], kept so the
37/// `metrics::halstead::HalsteadType` path still resolves.
38///
39/// The classification never was Halstead-specific — it answers whether
40/// a node acts as an operator or an operand, which the grammar decides
41/// and other consumers can use (#1376). Renaming it outright would be a
42/// SemVer break, so the old spelling stays until the next major.
43#[deprecated(
44 since = "2.3.0",
45 note = "renamed to `TokenRole`: the classification is syntactic, not Halstead-specific"
46)]
47pub type HalsteadType = TokenRole;
48
49use crate::checker::Checker;
50use crate::getter::Getter;
51use crate::int_hash::IntKeyHashMap;
52use crate::macros::implement_metric_trait;
53
54use crate::*;
55
56/// The `Halstead` metric suite.
57#[derive(Default, Clone, Debug, PartialEq)]
58#[non_exhaustive]
59pub struct Stats {
60 u_operators: u64,
61 operators: u64,
62 u_operands: u64,
63 operands: u64,
64}
65
66/// Per-space operator / operand occurrence maps used to compute the
67/// Halstead `Stats` struct. One map per distinct operator (`kind_id`)
68/// and one per distinct operand (`text`); merged across nested spaces.
69#[derive(Debug, Default, Clone, PartialEq)]
70pub struct HalsteadMaps<'a> {
71 /// Keyed by `kind_id`, so it is hashed with [`crate::int_hash`]'s
72 /// integer hasher rather than SipHash: the key is a grammar symbol
73 /// this crate generated, drawn from an alphabet of at most a few
74 /// hundred values, so there is nothing for a keyed hash to defend.
75 pub(crate) operators: IntKeyHashMap<u16, u64>,
76 /// Primitive-type operators stored by text so each distinct primitive
77 /// (e.g. `int` vs `double`) counts as a separate distinct operator,
78 /// even when the grammar maps them all to a single kind_id. Also
79 /// holds the operators a getter keys by a fixed spelling
80 /// ([`Getter::get_operator_spelling`]), such as the generic `<>`
81 /// pair (#1559).
82 ///
83 /// Text-keyed, so it keeps SipHash — see the module doc on
84 /// [`crate::int_hash`] for why analysed source text does not qualify
85 /// for the fast hasher.
86 pub(crate) primitive_operators: HashMap<&'a [u8], u64>,
87 /// Text-keyed, and on SipHash for the same reason as
88 /// `primitive_operators`.
89 pub(crate) operands: HashMap<&'a [u8], u64>,
90}
91
92impl<'a> HalsteadMaps<'a> {
93 pub(crate) fn new() -> Self {
94 Self::default()
95 }
96
97 pub(crate) fn merge(&mut self, other: &HalsteadMaps<'a>) {
98 for (k, v) in &other.operators {
99 *self.operators.entry(*k).or_insert(0) += v;
100 }
101 for (k, v) in &other.primitive_operators {
102 *self.primitive_operators.entry(*k).or_insert(0) += v;
103 }
104 for (k, v) in &other.operands {
105 *self.operands.entry(*k).or_insert(0) += v;
106 }
107 }
108
109 pub(crate) fn finalize(&self, stats: &mut Stats) {
110 stats.u_operators = (self.operators.len() + self.primitive_operators.len()) as u64;
111 stats.operators =
112 self.operators.values().sum::<u64>() + self.primitive_operators.values().sum::<u64>();
113 stats.u_operands = self.operands.len() as u64;
114 stats.operands = self.operands.values().sum::<u64>();
115 }
116}
117
118impl fmt::Display for Stats {
119 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
120 write!(
121 f,
122 "unique_operators: {}, \
123 total_operators: {}, \
124 unique_operands: {}, \
125 total_operands: {}, \
126 length: {}, \
127 estimated_program_length: {}, \
128 purity_ratio: {}, \
129 size: {}, \
130 volume: {}, \
131 difficulty: {}, \
132 level: {}, \
133 effort: {}, \
134 time: {}, \
135 bugs: {}",
136 self.unique_operators(),
137 self.total_operators(),
138 self.unique_operands(),
139 self.total_operands(),
140 self.length(),
141 self.estimated_program_length(),
142 self.purity_ratio(),
143 self.vocabulary(),
144 self.volume(),
145 self.difficulty(),
146 self.level(),
147 self.effort(),
148 self.time(),
149 self.bugs(),
150 )
151 }
152}
153
154impl Stats {
155 // Intentionally a no-op. Halstead distinct-counts (`u_operators` /
156 // `u_operands`) cannot be summed across sibling spaces without
157 // double-counting operators/operands they share. Cross-space
158 // aggregation is instead done by unioning the occurrence maps
159 // (`HalsteadMaps::merge`) and re-running `finalize` on the parent
160 // (see `spaces/compute.rs`). Summing the finalized fields here —
161 // mirroring the sibling metrics' `merge` — would silently inflate
162 // every parent space's n1/n2/N1/N2.
163 pub(crate) fn merge(&mut self, _other: &Stats) {}
164
165 /// Returns `η1`, the number of distinct operators
166 #[inline]
167 #[must_use]
168 pub fn unique_operators(&self) -> u64 {
169 self.u_operators
170 }
171
172 /// Returns `N1`, the number of total operators
173 #[inline]
174 #[must_use]
175 pub fn total_operators(&self) -> u64 {
176 self.operators
177 }
178
179 /// Returns `η2`, the number of distinct operands
180 #[inline]
181 #[must_use]
182 pub fn unique_operands(&self) -> u64 {
183 self.u_operands
184 }
185
186 /// Returns `N2`, the number of total operands
187 #[inline]
188 #[must_use]
189 pub fn total_operands(&self) -> u64 {
190 self.operands
191 }
192
193 /// Returns the program length
194 ///
195 /// Computed as `N = N1 + N2`, the sum of [`Self::total_operators`] and
196 /// [`Self::total_operands`].
197 #[inline]
198 #[must_use]
199 pub fn length(&self) -> u64 {
200 self.total_operands() + self.total_operators()
201 }
202
203 /// Returns the calculated estimated program length
204 ///
205 /// Computed as `N^ = n1 * log2(n1) + n2 * log2(n2)`, where `n1` is
206 /// [`Self::unique_operators`] and `n2` is [`Self::unique_operands`]. Each term is
207 /// treated as `0` when its unique count is `0`.
208 #[inline]
209 #[must_use]
210 pub fn estimated_program_length(&self) -> f64 {
211 let uo = self.unique_operators() as f64;
212 let ud = self.unique_operands() as f64;
213 let uo_term = if uo == 0.0 { 0.0 } else { uo * uo.log2() };
214 let ud_term = if ud == 0.0 { 0.0 } else { ud * ud.log2() };
215 uo_term + ud_term
216 }
217
218 /// Returns the purity ratio
219 ///
220 /// Computed as `PR = N^ / N`, the ratio of
221 /// [`Self::estimated_program_length`] to [`Self::length`].
222 #[inline]
223 #[must_use]
224 pub fn purity_ratio(&self) -> f64 {
225 let len = self.length() as f64;
226 if len == 0.0 {
227 0.0
228 } else {
229 self.estimated_program_length() / len
230 }
231 }
232
233 /// Returns the program vocabulary
234 ///
235 /// Computed as `n = n1 + n2`, the sum of [`Self::unique_operators`] and
236 /// [`Self::unique_operands`].
237 #[inline]
238 #[must_use]
239 pub fn vocabulary(&self) -> u64 {
240 self.unique_operands() + self.unique_operators()
241 }
242
243 /// Returns the program volume.
244 ///
245 /// Computed as `V = N * log2(n)`, where `N` is [`Self::length`] and `n`
246 /// is [`Self::vocabulary`]. Returns `0` when the vocabulary is `<= 1`,
247 /// since `log2` would be non-positive.
248 ///
249 /// Unit of measurement: bits
250 #[inline]
251 #[must_use]
252 pub fn volume(&self) -> f64 {
253 // Assumes a uniform binary encoding for the vocabulary is used.
254 let vocab = self.vocabulary() as f64;
255 if vocab <= 1.0 {
256 0.0
257 } else {
258 self.length() as f64 * vocab.log2()
259 }
260 }
261
262 /// Returns the estimated difficulty required to program
263 ///
264 /// Computed as `D = (n1 / 2) * (N2 / n2)`, where `n1` is
265 /// [`Self::unique_operators`], `N2` is [`Self::total_operands`], and `n2` is
266 /// [`Self::unique_operands`].
267 #[inline]
268 #[must_use]
269 pub fn difficulty(&self) -> f64 {
270 let ud = self.unique_operands() as f64;
271 if ud == 0.0 {
272 0.0
273 } else {
274 self.unique_operators() as f64 / 2. * self.total_operands() as f64 / ud
275 }
276 }
277
278 /// Returns the estimated level of difficulty required to program
279 ///
280 /// Computed as `L = 1 / D`, the reciprocal of [`Self::difficulty`].
281 #[inline]
282 #[must_use]
283 pub fn level(&self) -> f64 {
284 let d = self.difficulty();
285 if d == 0.0 { 0.0 } else { 1. / d }
286 }
287
288 /// Returns the estimated effort required to program
289 ///
290 /// Computed as `E = D * V`, the product of [`Self::difficulty`] and
291 /// [`Self::volume`].
292 #[inline]
293 #[must_use]
294 pub fn effort(&self) -> f64 {
295 self.difficulty() * self.volume()
296 }
297
298 /// Returns the estimated time required to program.
299 ///
300 /// Computed as `T = E / 18`, where `E` is [`Self::effort`] and `18` is
301 /// the Stroud number (see the divisor rationale below).
302 ///
303 /// Unit of measurement: seconds
304 #[inline]
305 #[must_use]
306 pub fn time(&self) -> f64 {
307 // The floating point `18.` aims to describe the processing rate of the
308 // human brain. It is called Stoud number, S, and its
309 // unit of measurement is moments/seconds.
310 // A moment is the time required by the human brain to carry out the
311 // most elementary decision.
312 // 5 <= S <= 20. Halstead uses 18.
313 // The value of S has been empirically developed from psychological
314 // reasoning, and its recommended value for
315 // programming applications is 18.
316 //
317 // Source: https://www.geeksforgeeks.org/software-engineering-halsteads-software-metrics/
318 self.effort() / 18.
319 }
320
321 /// Returns the estimated number of delivered bugs.
322 ///
323 /// This metric represents the average amount of work a programmer can do
324 /// without introducing an error.
325 ///
326 /// Computed as `B = E^(2/3) / 3000`, where `E` is [`Self::effort`]. This
327 /// is the effort-based variant of Halstead's delivered-bugs estimate
328 /// rather than the more commonly cited volume-based form `B = V / 3000`;
329 /// it matches the formula used by upstream `rust-code-analysis`.
330 #[inline]
331 #[must_use]
332 pub fn bugs(&self) -> f64 {
333 // The floating point `3000.` represents the number of elementary
334 // mental discriminations.
335 // A mental discrimination, in psychology, is the ability to perceive
336 // and respond to differences among stimuli.
337 //
338 // The value above is obtained starting from a constant that
339 // is different for every language and assumes that natural language is
340 // the language of the brain.
341 // For programming languages, the English language constant
342 // has been considered.
343 //
344 // After every 3000 mental discriminations a result is produced.
345 // This result, whether correct or incorrect, is more than likely
346 // either used as an input for the next operation or is output to the
347 // environment.
348 // If incorrect the error should become apparent.
349 // Thus, an opportunity for error occurs every 3000
350 // mental discriminations.
351 //
352 // Source: https://docs.lib.purdue.edu/cgi/viewcontent.cgi?article=1145&context=cstech
353 self.effort().powf(2. / 3.) / 3000.
354 }
355}
356
357#[doc(hidden)]
358/// Per-language extraction of Halstead operator/operand maps.
359pub(crate) trait Halstead
360where
361 Self: Checker + Getter,
362{
363 /// Walk `node` and update `stats` with this metric for the language
364 /// implementing the trait.
365 ///
366 /// `ancestors` is the chain the walker descended through; it is
367 /// handed to [`Getter::get_op_type`], six of whose impls classify a
368 /// token by what encloses it (#1096).
369 fn compute<'a>(
370 node: &Node<'a>,
371 code: &'a [u8],
372 ancestors: Ancestors<'a, '_>,
373 halstead_maps: &mut HalsteadMaps<'a>,
374 );
375}
376
377#[inline]
378fn get_id<'a>(node: &Node<'a>, code: &'a [u8]) -> &'a [u8] {
379 &code[node.start_byte()..node.end_byte()]
380}
381
382#[inline]
383fn compute_halstead<'a, T: Getter + Checker>(
384 node: &Node<'a>,
385 code: &'a [u8],
386 ancestors: Ancestors<'a, '_>,
387 halstead_maps: &mut HalsteadMaps<'a>,
388) {
389 // Halstead counts the tokens written, and a zero-width node spells
390 // none, so it bills neither half (#1546). Error recovery is where
391 // they come from: every MISSING node tree-sitter inserts for a token
392 // the source lacks (a `;`, a `)`, an identifier) is zero-width, and
393 // so is the identifier C#'s and Elixir's recovery builds without
394 // flagging it MISSING. Checked here rather than per getter because
395 // every getter that classifies the kind would otherwise bill it, an
396 // identifier as the empty-string operand.
397 if node.start_byte() == node.end_byte() {
398 return;
399 }
400 match T::get_op_type_with_code(node, code, ancestors) {
401 TokenRole::Operator => {
402 if let Some(spelling) = T::get_operator_spelling(node, ancestors) {
403 *halstead_maps
404 .primitive_operators
405 .entry(spelling.as_bytes())
406 .or_insert(0) += 1;
407 } else if T::is_primitive(node) {
408 // Store primitive-type operators by text so distinct
409 // primitives (e.g. `int` vs `double`) that share a
410 // single kind_id are counted separately in n1/N1.
411 *halstead_maps
412 .primitive_operators
413 .entry(get_id(node, code))
414 .or_insert(0) += 1;
415 } else {
416 *halstead_maps.operators.entry(node.kind_id()).or_insert(0) += 1;
417 }
418 }
419 TokenRole::Operand => {
420 *halstead_maps
421 .operands
422 .entry(T::get_operand_id(node, code, ancestors))
423 .or_insert(0) += 1;
424 }
425 _ => {}
426 }
427}
428
429// Every language's `Halstead::compute` is the same forward to
430// `compute_halstead`, which classifies each node through the language's
431// own `Getter` / `Checker`. Nothing per-language lives here — it lives
432// in `big-code-analysis-ast/src/getter/<lang>.rs` — so writing the
433// impls out was 23 copies of one signature. (This is the only metric
434// whose per-language impls are all identical; every other trait has
435// real per-language bodies.)
436macro_rules! impl_halstead_forwarding {
437 ($($code:ty),+ $(,)?) => {
438 $(
439 impl Halstead for $code {
440 fn compute<'a>(
441 node: &Node<'a>,
442 code: &'a [u8],
443 ancestors: Ancestors<'a, '_>,
444 halstead_maps: &mut HalsteadMaps<'a>,
445 ) {
446 compute_halstead::<Self>(node, code, ancestors, halstead_maps);
447 }
448 }
449 )+
450 };
451}
452
453impl_halstead_forwarding!(
454 PythonCode,
455 MozjsCode,
456 JavascriptCode,
457 TypescriptCode,
458 TsxCode,
459 RustCode,
460 CppCode,
461 CCode,
462 ObjcCode,
463 MozcppCode,
464 JavaCode,
465 GroovyCode,
466 CsharpCode,
467 GoCode,
468 PerlCode,
469 KotlinCode,
470 LuaCode,
471 PhpCode,
472 RubyCode,
473 ElixirCode,
474 BashCode,
475 TclCode,
476 IrulesCode,
477);
478
479// Real defaults — no operators / operands to count. Audited in #188.
480implement_metric_trait!(Halstead, PreprocCode, CcommentCode);
481
482#[cfg(test)]
483#[allow(
484 clippy::float_cmp,
485 clippy::cast_precision_loss,
486 clippy::cast_possible_truncation,
487 clippy::cast_sign_loss,
488 clippy::similar_names,
489 clippy::doc_markdown,
490 clippy::needless_raw_string_hashes,
491 clippy::too_many_lines
492)]
493mod tests {
494 use std::collections::{BTreeSet, HashSet};
495 use std::path::PathBuf;
496
497 use crate::test_support::{ast_has_kind_id, check_metrics_only_shim, for_each_node_with_chain};
498
499 use super::*;
500
501 check_metrics_only_shim!(check_metrics, Halstead);
502
503 // `HalsteadType` is the pre-#1376 name for `TokenRole`, kept as a
504 // deprecated alias because removing it before `3.0` would be a
505 // SemVer break (STABILITY.md). Nothing in this repository uses the
506 // old spelling any more, so only this guard would notice it being
507 // dropped — and dropping it silently is exactly what the promise
508 // rules out.
509 //
510 // The `matches!` arms are the assertion: a value typed through the
511 // alias can only be matched against `TokenRole`'s variants if the
512 // alias still names that enum, so this fails to *compile* if the
513 // alias is removed or re-pointed, rather than failing at runtime.
514 // `TokenRole` derives nothing, so there is no `assert_eq!` to reach
515 // for here.
516 #[test]
517 #[allow(deprecated)]
518 fn halstead_type_alias_still_names_token_role() {
519 let via_alias: HalsteadType = HalsteadType::Operand;
520 assert!(matches!(via_alias, TokenRole::Operand));
521 assert!(!matches!(via_alias, TokenRole::Operator));
522 }
523
524 /// Runs the `--ops` walk over `source` and returns the root space's
525 /// merged vocabulary, with every nested space still reachable
526 /// through [`crate::ops::Ops::spaces`].
527 #[cfg(any(
528 feature = "c",
529 feature = "cpp",
530 feature = "csharp",
531 feature = "groovy",
532 feature = "irules",
533 feature = "java",
534 feature = "javascript",
535 feature = "kotlin",
536 feature = "mozcpp",
537 feature = "mozjs",
538 feature = "objc",
539 feature = "perl",
540 feature = "ruby",
541 feature = "tcl",
542 feature = "typescript",
543 ))]
544 fn ops_of<T: crate::MetricSuite>(source: &str, file: &str) -> crate::ops::Ops {
545 let path = PathBuf::from(file);
546 let parser = T::new(source.as_bytes().to_vec(), &path, None);
547 crate::ops::ops_inner(&parser, None).expect("ops walk succeeds")
548 }
549
550 // Pins the lesson-4 invariant `n2 == len(dedupe(ops.operands))` by
551 // running `operands_and_operators` (the text-keyed `--ops` store)
552 // on the same source and comparing its deduplicated operand count
553 // to the expected `n2`. The metrics store and the ops store are
554 // independent (lesson 4); this catches a classification change that
555 // moves one without the other.
556 // `#[track_caller]` so a failure reports the *caller's* line rather
557 // than this helper's. Callers that wrap it in a per-language helper
558 // (`assert_char_literal_operands`, #1316) are tracked too, so the
559 // reported location names the language row instead of a shared line
560 // no assertion message distinguishes.
561 #[cfg(any(
562 feature = "c",
563 feature = "cpp",
564 feature = "groovy",
565 feature = "irules",
566 feature = "kotlin",
567 feature = "mozcpp",
568 feature = "objc",
569 feature = "perl",
570 feature = "ruby",
571 feature = "tcl",
572 ))]
573 #[track_caller]
574 fn assert_ops_operands<T: crate::MetricSuite>(
575 source: &str,
576 file: &str,
577 expected_n2: usize,
578 mut expected_operands: Vec<&str>,
579 ) {
580 let ops = ops_of::<T>(source, file);
581
582 let unique: HashSet<&str> = ops.operands.iter().map(String::as_str).collect();
583 assert_eq!(
584 unique.len(),
585 expected_n2,
586 "dedupe(ops.operands) must equal n2; operands were {:?}",
587 ops.operands
588 );
589
590 let mut got: Vec<&str> = unique.into_iter().collect();
591 got.sort_unstable();
592 expected_operands.sort_unstable();
593 assert_eq!(got, expected_operands, "operand vocabulary for {file}");
594 }
595
596 /// Asserts each keyword in `keywords` reaches the operand vocabulary
597 /// and reaches the operator vocabulary nowhere.
598 ///
599 /// Both halves are load-bearing for #1380. A keyword listed in two
600 /// arms is billed twice — once into `n1`/`N1` and once into
601 /// `n2`/`N2` — and an assertion that only looked for its arrival
602 /// among the operands would pass on that. The operator side is what
603 /// pins the removal.
604 #[cfg(any(
605 feature = "cpp",
606 feature = "csharp",
607 feature = "groovy",
608 feature = "java",
609 feature = "kotlin",
610 feature = "mozcpp",
611 feature = "objc",
612 feature = "typescript"
613 ))]
614 #[track_caller]
615 fn assert_keywords_are_operands_only<T: crate::MetricSuite>(
616 source: &str,
617 file: &str,
618 keywords: &[&str],
619 ) {
620 let ops = ops_of::<T>(source, file);
621
622 for keyword in keywords {
623 assert!(
624 ops.operands.iter().any(|o| o == keyword),
625 "{file}: `{keyword}` must be an operand; operands were {:?}",
626 ops.operands
627 );
628 assert!(
629 !ops.operators.iter().any(|o| o == keyword),
630 "{file}: `{keyword}` must not be an operator; operators were {:?}",
631 ops.operators
632 );
633 }
634 }
635
636 /// How one space's vocabularies classify a single keyword.
637 #[derive(Debug, PartialEq, Eq)]
638 enum Role {
639 OperatorOnly,
640 OperandOnly,
641 Both,
642 }
643
644 /// Records `keyword`'s role in `ops` and in every space below it.
645 ///
646 /// A space that bills the keyword in neither vocabulary contributes
647 /// no row, so the result is the set of places the keyword actually
648 /// reaches — which is what distinguishes a parent gate from its own
649 /// inverse, and what the merged root vocabulary cannot show.
650 #[cfg(feature = "csharp")]
651 fn collect_keyword_roles(ops: &crate::ops::Ops, keyword: &str, out: &mut Vec<Role>) {
652 let operator = ops.operators.iter().any(|o| o == keyword);
653 let operand = ops.operands.iter().any(|o| o == keyword);
654 match (operator, operand) {
655 (true, true) => out.push(Role::Both),
656 (true, false) => out.push(Role::OperatorOnly),
657 (false, true) => out.push(Role::OperandOnly),
658 (false, false) => {}
659 }
660 for space in &ops.spaces {
661 collect_keyword_roles(space, keyword, out);
662 }
663 }
664
665 /// Pins the three grammar facts every #1380 operand arm rests on,
666 /// over a fixture that samples one self- or super-reference per
667 /// syntactic position the language allows.
668 ///
669 /// Each `keywords` row is `(spelling, kind_id, occurrences)`.
670 ///
671 /// * **Grammar-dispatch section 1.** Every node the grammar spells
672 /// `this` / `super` / `base` / `this@` / `super@` carries the one
673 /// `kind_id` its arm lists. An alias arises in a *different*
674 /// syntactic position, which no fixture can enumerate, so this is
675 /// the weaker half of the alias evidence — the strong half is that
676 /// these generated enums carry numeric-suffix aliases in quantity
677 /// and none spells a `This2`. What this loop catches is a grammar
678 /// bump introducing one under an existing fixture.
679 /// * **Grammar-dispatch section 5.** Each is a childless leaf and no
680 /// node *containing* it is classified, so the reference cannot be
681 /// billed twice. Every ancestor is checked, not just the parent:
682 /// Kotlin puts `this_expression` *and* `navigation_expression`
683 /// between the leaf and the nearest classified node, and the claim
684 /// the arms make is about containment, not parentage.
685 /// * **Grammar-dispatch section 11.** The arm is unconditional. A
686 /// parent-scoped guard added later — the shape the two deliberate
687 /// carve-outs use, and so the shape a reviewer might reasonably
688 /// add — would satisfy the single-position count tests and still
689 /// drop `this` in a method reference or a constructor delegation.
690 ///
691 /// The fixtures deliberately exclude the two gated positions (C#'s
692 /// indexer declarator, Java's wildcard bound); those are operators,
693 /// and their own tests pin them.
694 #[cfg(any(feature = "csharp", feature = "java", feature = "kotlin"))]
695 #[track_caller]
696 fn assert_self_reference_leaves<L: LanguageInfo + Getter>(
697 source: &str,
698 keywords: &[(&str, u16, usize)],
699 label: &str,
700 ) {
701 let code = source.as_bytes();
702 let mut seen = vec![0_usize; keywords.len()];
703
704 for_each_node_with_chain::<L>(code, |node, chain| {
705 let Some(index) = keywords.iter().position(|(kw, _, _)| *kw == node.kind()) else {
706 return;
707 };
708 let (keyword, kind_id, _) = keywords[index];
709 seen[index] += 1;
710
711 assert_eq!(
712 node.kind_id(),
713 kind_id,
714 "{label}: a `{keyword}` carries kind_id {} rather than the {kind_id} its arm \
715 lists — an alias the arm cannot see",
716 node.kind_id()
717 );
718 assert_eq!(
719 node.child_count(),
720 0,
721 "{label}: `{keyword}` grew children, which the operand arm would now \
722 double-count against"
723 );
724 for (depth, ancestor) in chain.iter().enumerate() {
725 assert!(
726 matches!(
727 L::get_op_type_with_code(ancestor, code, Ancestors::known(&chain[..depth])),
728 TokenRole::Unknown
729 ),
730 "{label}: `{}` contains a `{keyword}` and is itself classified, so the \
731 reference now counts twice",
732 ancestor.kind()
733 );
734 }
735 // `_with_code` is the spelling `compute_halstead` calls. The
736 // default forwards to the byte-less form, so asking the wrong
737 // one reads as correct right up until a language grows an
738 // override (grammar-dispatch section 7).
739 assert!(
740 matches!(
741 L::get_op_type_with_code(node, code, Ancestors::known(chain)),
742 TokenRole::Operand
743 ),
744 "{label}: a `{keyword}` under `{}` is not an operand",
745 chain.last().map_or("<root>", Node::kind)
746 );
747 });
748
749 for ((keyword, _, expected), got) in keywords.iter().zip(&seen) {
750 assert_eq!(
751 got, expected,
752 "{label}: fixture holds {got} `{keyword}` rather than {expected}"
753 );
754 }
755 }
756
757 /// Asserts the root space's `[n1, N1, n2, N2]`, naming `label` when
758 /// it does not hold.
759 ///
760 /// The delimiter-invariance tests (#1256 Elixir, #1312 Ruby and
761 /// Perl) each loop over spellings of one literal and need the
762 /// spelling in the failure message; `check_metrics` expands to a
763 /// plain `fn` that cannot capture a loop variable, so they reach
764 /// for the closure-taking helper it wraps. Three copies of that
765 /// dance is two too many.
766 #[cfg(any(
767 feature = "bash",
768 feature = "c",
769 feature = "cpp",
770 feature = "csharp",
771 feature = "elixir",
772 feature = "groovy",
773 feature = "irules",
774 feature = "java",
775 feature = "javascript",
776 feature = "kotlin",
777 feature = "mozcpp",
778 feature = "mozjs",
779 feature = "objc",
780 feature = "perl",
781 feature = "php",
782 feature = "ruby",
783 feature = "tcl",
784 feature = "typescript",
785 ))]
786 fn assert_halstead_counts<T: crate::MetricSuite>(
787 source: &str,
788 file: &str,
789 expected: [u64; 4],
790 label: &str,
791 ) {
792 crate::test_support::check_func_space_only::<T, _>(
793 source,
794 file,
795 &[crate::Metric::Halstead],
796 |space| {
797 let halstead = &space.metrics.halstead;
798 assert_eq!(
799 [
800 halstead.unique_operators(),
801 halstead.total_operators(),
802 halstead.unique_operands(),
803 halstead.total_operands(),
804 ],
805 expected,
806 "{label}"
807 );
808 },
809 );
810 }
811
812 #[cfg(feature = "python")]
813 #[test]
814 fn python_operators_and_operands() {
815 check_metrics::<PythonParser>(
816 "def foo():
817 def bar():
818 def toto():
819 a = 1 + 1
820 b = 2 + a
821 c = 3 + 3",
822 "foo.py",
823 |metric| {
824 // unique operators: def, (), :, =, +
825 // operators: def, (), : for each of the three headers,
826 // then =, =, =, +, +, + (the `()` and `:` since #1486)
827 // unique operands: foo, bar, toto, a, b, c, 1, 2, 3
828 // operands: foo, bar, toto, a, b, c, 1, 1, 2, a, 3, 3
829 assert_eq!(metric.halstead.unique_operators(), 5);
830 assert_eq!(metric.halstead.total_operators(), 15);
831 insta::assert_json_snapshot!(
832 metric.halstead,
833 @r#"
834 {
835 "unique_operators": 5,
836 "total_operators": 15,
837 "unique_operands": 9,
838 "total_operands": 12,
839 "length": 27,
840 "estimated_program_length": 40.13896548741762,
841 "purity_ratio": 1.4866283513858378,
842 "vocabulary": 14,
843 "volume": 102.79858289555531,
844 "difficulty": 3.3333333333333335,
845 "level": 0.3,
846 "effort": 342.6619429851844,
847 "time": 19.03677461028802,
848 "bugs": 0.01632259960095138
849 }
850 "#
851 );
852 },
853 );
854 }
855
856 /// Pointer-arithmetic operators: `*` (dereference), `&` (address-of),
857 /// `->` (member-of-pointer), `+` (pointer + offset). Each is counted
858 /// once in `n1`; multiple uses bump `N1`. The headline integer values
859 /// (`u_operators`, `u_operands`) anchor the snapshot per the
860 /// snapshot-anchor policy.
861 #[cfg(feature = "c")]
862 #[test]
863 fn c_pointer_arithmetic_operators() {
864 check_metrics::<CParser>(
865 "int g(int* p, int* q) {
866 return *(p + 1) + *q;
867 }",
868 "foo.c",
869 |metric| {
870 // Unique operators: int, *, (), {, }, +, ;, return (= 8)
871 // `*` covers both pointer-type and dereference; the grammar
872 // does NOT split them. `,` does not appear (only one
873 // parameter on each side of the body).
874 // Unique operands: g, p, q, 1 (= 4)
875 assert_eq!(metric.halstead.unique_operators(), 8);
876 assert_eq!(metric.halstead.unique_operands(), 4);
877 insta::assert_json_snapshot!(metric.halstead);
878 },
879 );
880 }
881
882 /// Bitwise (`&`, `|`, `^`, `~`, `<<`, `>>`) and logical (`&&`, `||`,
883 /// `!`) operators are distinct kind_ids and count as separate unique
884 /// operators in Halstead. `&` (bitwise-and) and `&&` (logical-and)
885 /// must NOT collapse, even though both render as ampersands.
886 #[cfg(feature = "c")]
887 #[test]
888 fn c_bitwise_and_logical_operators() {
889 check_metrics::<CParser>(
890 "int f(int a, int b) {
891 int x = (a & b) | (a ^ b);
892 int y = ~a;
893 int z = (a << 1) >> 2;
894 return (a && b) || !x;
895 }",
896 "foo.c",
897 |metric| {
898 // Expect: 6 bitwise op kinds (& | ^ ~ << >>), 3 logical (&& || !).
899 // Plus int, (), {, }, =, ;, return, , — 8 syntactic / arithmetic
900 // operator kinds. Six bitwise + three logical + eight = 17 unique
901 // operators is the upper bound; actuals depend on grammar collapse,
902 // so we assert a lower-bound and anchor via snapshot below.
903 let s = &metric.halstead;
904 assert!(
905 s.unique_operators() >= 14,
906 "expected >= 14 unique operators (bitwise + logical + syntax), got {}",
907 s.unique_operators(),
908 );
909 assert_eq!(s.unique_operands(), 8); // f, a, b, x, y, z, 1, 2
910 insta::assert_json_snapshot!(metric.halstead);
911 },
912 );
913 }
914
915 /// Increment / decrement (`++`, `--`) and `sizeof` / cast operators
916 /// each contribute distinct unique operators. C-style casts in the
917 /// tree-sitter grammar surface as `cast_expression` with the type
918 /// token classified as a primitive_type operator.
919 #[cfg(feature = "c")]
920 #[test]
921 fn c_increment_decrement_and_sizeof() {
922 check_metrics::<CParser>(
923 "void f(int* p) {
924 int n = sizeof(int);
925 ++p;
926 --n;
927 long w = (long) n;
928 }",
929 "foo.c",
930 |metric| {
931 // Unique operators include: void, int, long, *, =, sizeof, ++, --, (), {, }, ;
932 // Unique operands: f, p, n, w
933 let s = &metric.halstead;
934 assert!(
935 s.unique_operators() >= 10,
936 "expected >= 10 unique operators including ++ / -- / sizeof / cast, got {}",
937 s.unique_operators(),
938 );
939 assert_eq!(s.unique_operands(), 4);
940 insta::assert_json_snapshot!(metric.halstead);
941 },
942 );
943 }
944
945 #[cfg(feature = "cpp")]
946 #[test]
947 fn cpp_operators_and_operands() {
948 // Define operators and operands for C/C++ grammar according to this specification:
949 // https://www.verifysoft.com/en_halstead_metrics.html
950 // The only difference with the specification above is that
951 // primitive types are treated as operators, since the definition of a
952 // primitive type can be seen as the creation of a slot of a certain size.
953 // i.e. The `int a;` definition creates a n-bytes slot.
954 check_metrics::<CppParser>(
955 "main()
956 {
957 int a, b, c, avg;
958 scanf(\"%d %d %d\", &a, &b, &c);
959 avg = (a + b + c) / 3;
960 printf(\"avg = %d\", avg);
961 }",
962 "foo.c",
963 |metric| {
964 // unique operators: (), {}, int, &, =, +, /, ,, ;
965 // unique operands: main, a, b, c, avg, scanf, "%d %d %d", 3, printf, "avg = %d"
966 insta::assert_json_snapshot!(
967 metric.halstead,
968 @r#"
969 {
970 "unique_operators": 9,
971 "total_operators": 24,
972 "unique_operands": 10,
973 "total_operands": 18,
974 "length": 42,
975 "estimated_program_length": 61.74860596185444,
976 "purity_ratio": 1.470204903853677,
977 "vocabulary": 19,
978 "volume": 178.41295556463058,
979 "difficulty": 8.1,
980 "level": 0.1234567901234568,
981 "effort": 1445.1449400735075,
982 "time": 80.28583000408375,
983 "bugs": 0.04260752914034329
984 }
985 "#
986 );
987 },
988 );
989 }
990
991 /// A `sized_type_specifier` carries its `unsigned`/`signed`/`long`/
992 /// `short` modifiers as bare keyword tokens (distinct kind_ids), not
993 /// as `primitive_type` children. Prior to issue #466 those tokens
994 /// fell through to the `Unknown` arm and were dropped from `n1`/`N1`,
995 /// so `unsigned int` collapsed to just `int` and `signed long`
996 /// contributed nothing. They must each count as a distinct operator,
997 /// while `long long`'s two `long` tokens fold to one `n1` entry but
998 /// two `N1` hits. Regression test for issue #466.
999 #[cfg(feature = "cpp")]
1000 #[test]
1001 fn cpp_sized_type_specifier_operators() {
1002 let source = "unsigned int u = 3; signed long b = 4; long long c = 5;";
1003 check_metrics::<CppParser>(source, "foo.cpp", |metric| {
1004 // Distinct operators (n1): unsigned, signed, long, int, =, ; = 6
1005 // Total operators (N1):
1006 // unsigned(1) + int(1) + =(3) + ;(3) + signed(1) + long(3) = 12
1007 // (`long` appears once in `signed long` and twice in `long long`)
1008 // Distinct/total operands: u, b, c, 3, 4, 5 = 6 / 6
1009 assert_eq!(metric.halstead.unique_operators(), 6);
1010 assert_eq!(metric.halstead.total_operators(), 12);
1011 assert_eq!(metric.halstead.unique_operands(), 6);
1012 assert_eq!(metric.halstead.total_operands(), 6);
1013 });
1014
1015 // Pin the lesson-4 `n1 == dedupe(ops.operators)` invariant: the
1016 // kind_id-keyed metrics store and the text-keyed `--ops` store are
1017 // independent, so a modifier classified in one but not the other
1018 // would diverge here.
1019 let path = PathBuf::from("foo.cpp");
1020 let parser = CppParser::new(source.as_bytes().to_vec(), &path, None);
1021 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
1022 let unique_operators: HashSet<&str> = ops.operators.iter().map(String::as_str).collect();
1023 assert_eq!(
1024 unique_operators.len(),
1025 6,
1026 "dedupe(ops.operators) must equal n1; operators were {:?}",
1027 ops.operators
1028 );
1029 for modifier in ["unsigned", "signed", "long"] {
1030 assert!(
1031 unique_operators.contains(modifier),
1032 "sized_type_specifier modifier {modifier:?} missing from ops.operators: {:?}",
1033 ops.operators
1034 );
1035 }
1036 }
1037
1038 /// C++20 spaceship operator `<=>` (`Cpp::LTEQGT`) is a comparison
1039 /// operator and must be counted in Halstead, like its sibling
1040 /// comparison operators `<`, `>`, `<=`, `>=`, `==`, `!=`. Prior to
1041 /// this fix it fell through to the `Unknown` arm and was silently
1042 /// dropped from `n1` / `N1`, under-reporting volume / effort on any
1043 /// C++20+ codebase that defines `operator<=>`. Regression test for
1044 /// issue #197.
1045 #[cfg(feature = "cpp")]
1046 #[test]
1047 fn cpp_spaceship_operator_is_halstead_operator() {
1048 check_metrics::<CppParser>(
1049 "int f(int a, int b) {
1050 return (a <=> b) != 0;
1051 }",
1052 "foo.cpp",
1053 |metric| {
1054 // Unique operators (grammar collapses matched delimiters
1055 // to a single kind_id): int, (), {}, <=>, !=, return, ;, ,
1056 // `<=>` is the regression target — without the fix it
1057 // would be Unknown and `u_operators` would be 7.
1058 // Unique operands: f, a, b, 0
1059 let s = &metric.halstead;
1060 assert_eq!(s.unique_operators(), 8);
1061 assert_eq!(s.unique_operands(), 4);
1062 insta::assert_json_snapshot!(
1063 s,
1064 @r#"
1065 {
1066 "unique_operators": 8,
1067 "total_operators": 11,
1068 "unique_operands": 4,
1069 "total_operands": 6,
1070 "length": 17,
1071 "estimated_program_length": 32.0,
1072 "purity_ratio": 1.8823529411764706,
1073 "vocabulary": 12,
1074 "volume": 60.94436251225965,
1075 "difficulty": 6.0,
1076 "level": 0.16666666666666666,
1077 "effort": 365.6661750735579,
1078 "time": 20.31478750408655,
1079 "bugs": 0.01704519358507665
1080 }
1081 "#
1082 );
1083 },
1084 );
1085 }
1086
1087 /// C++ compound subtract-assign `-=` (`Cpp::DASHEQ`) must be counted
1088 /// in Halstead like every other compound assignment (`+=`, `*=`,
1089 /// `/=`, etc.). Prior to the fix it fell through to the `Unknown`
1090 /// arm and was silently dropped from `n1` / `N1` — under-reporting
1091 /// volume / effort wherever C++ code subtracts in place. Regression
1092 /// test for issue #198.
1093 #[cfg(feature = "cpp")]
1094 #[test]
1095 fn cpp_dash_eq_is_halstead_operator() {
1096 check_metrics::<CppParser>("void f(int a, int b) { a -= b; }", "foo.cpp", |metric| {
1097 // Unique operators: void, (), {}, int, ,, -=, ;
1098 // `-=` is the regression target — without the fix it
1099 // would be Unknown and `u_operators` would be 6.
1100 // Unique operands: f, a, b
1101 let s = &metric.halstead;
1102 assert_eq!(s.unique_operators(), 7);
1103 assert_eq!(s.unique_operands(), 3);
1104 });
1105 }
1106
1107 /// C++ pointer-to-member access `.*` (`Cpp::DOTSTAR`) must be
1108 /// counted in Halstead. Prior to the fix it fell through to the
1109 /// `Unknown` arm and was silently dropped from `n1` / `N1`.
1110 /// Regression test for issue #198.
1111 ///
1112 /// The snippet uses an `operator.*` declaration because that is
1113 /// where the C++ tree-sitter grammar reliably emits a single
1114 /// `DOTSTAR` leaf; in expression position (`a.*b`) some grammar
1115 /// versions split the token into `DOT` + `STAR` and the regression
1116 /// would be masked.
1117 #[cfg(feature = "cpp")]
1118 #[test]
1119 fn cpp_dot_star_is_halstead_operator() {
1120 check_metrics::<CppParser>("struct S { void operator.*(int); };", "foo.cpp", |metric| {
1121 // Unique operators with fix: {}, ;, (), int, void, .*, the
1122 // `operator` keyword (#1296) and `struct` (#1552).
1123 // `.*` is the regression target — without the fix it
1124 // falls through to `Unknown` and `u_operators` is 7.
1125 // Unique operands: S
1126 let s = &metric.halstead;
1127 assert_eq!(s.unique_operators(), 8);
1128 assert_eq!(s.unique_operands(), 1);
1129 });
1130 }
1131
1132 /// C++ pointer-to-member access through pointer `->*`
1133 /// (`Cpp::DASHGTSTAR`) must be counted in Halstead. Prior to the
1134 /// fix it fell through to the `Unknown` arm and was silently
1135 /// dropped from `n1` / `N1`. Regression test for issue #198.
1136 ///
1137 /// The snippet uses an `operator->*` declaration because that is
1138 /// where the C++ tree-sitter grammar reliably emits a single
1139 /// `DASHGTSTAR` leaf; in expression position (`a->*b`) the grammar
1140 /// splits the token into `DASHGT` + `STAR` and the regression would
1141 /// be masked.
1142 #[cfg(feature = "cpp")]
1143 #[test]
1144 fn cpp_dash_gt_star_is_halstead_operator() {
1145 check_metrics::<CppParser>(
1146 "struct S { void operator->*(int); };",
1147 "foo.cpp",
1148 |metric| {
1149 // Unique operators with fix: {}, ;, (), int, void, ->*,
1150 // the `operator` keyword (#1296) and `struct` (#1552).
1151 // `->*` is the regression target — without the fix it
1152 // falls through to `Unknown` and `u_operators` is 7.
1153 // Unique operands: S
1154 let s = &metric.halstead;
1155 assert_eq!(s.unique_operators(), 8);
1156 assert_eq!(s.unique_operands(), 1);
1157 },
1158 );
1159 }
1160
1161 #[cfg(feature = "cpp")]
1162 #[test]
1163 fn cpp_raw_string_delimiter_is_not_an_operator() {
1164 // Regression: issue #1314, the C++ sibling of Elixir #1256 and
1165 // Ruby/Perl #1312. A `raw_string_literal` carries its `R"(`
1166 // opener as a bare `LPAREN` child — the kind id a call uses —
1167 // so `auto a = R"(raw)";` reported a `()` operator with no call
1168 // in the source.
1169 //
1170 // The fixture holds both sides at once: two raw strings and one
1171 // real call. A guard widened past the literal would drop
1172 // `f(a)`'s parenthesis and fail here rather than silently
1173 // passing.
1174 //
1175 // The second literal uses the custom-delimiter form to pin that
1176 // shape too — it adds a `raw_string_delimiter` child but keeps
1177 // the same `(` — and its distinct text makes n2 differ from N2.
1178 //
1179 // expected: operators `;` × 3, `=` × 3, `int`, `()` × 1 →
1180 // n1 = 4, N1 = 8. Operands the two literals, `a` × 2, `b`, `c`,
1181 // `f` → n2 = 6, N2 = 7. Before the guard the two openers added
1182 // two more `()` → N1 = 10.
1183 check_metrics::<CppParser>(
1184 "auto a = R\"(raw)\";\nauto b = R\"tag(raw)tag\";\nint c = f(a);\n",
1185 "foo.cpp",
1186 |metric| {
1187 assert_eq!(metric.halstead.unique_operators(), 4);
1188 assert_eq!(metric.halstead.total_operators(), 8);
1189 assert_eq!(metric.halstead.unique_operands(), 6);
1190 assert_eq!(metric.halstead.total_operands(), 7);
1191 },
1192 );
1193 }
1194
1195 #[cfg(feature = "rust")]
1196 #[test]
1197 fn rust_operators_and_operands() {
1198 check_metrics::<RustParser>(
1199 "fn main() {
1200 let a = 5; let b = 5; let c = 5;
1201 let avg = (a + b + c) / 3;
1202 println!(\"{}\", avg);
1203 }",
1204 "foo.rs",
1205 |metric| {
1206 // unique operators: fn, (), {}, let, =, +, /, ;, !, ,
1207 // unique operands: main, a, b, c, avg, 5, 3, println, "{}"
1208 insta::assert_json_snapshot!(
1209 metric.halstead,
1210 @r#"
1211 {
1212 "unique_operators": 10,
1213 "total_operators": 23,
1214 "unique_operands": 9,
1215 "total_operands": 15,
1216 "length": 38,
1217 "estimated_program_length": 61.74860596185444,
1218 "purity_ratio": 1.624963314785643,
1219 "vocabulary": 19,
1220 "volume": 161.42124551085624,
1221 "difficulty": 8.333333333333334,
1222 "level": 0.12,
1223 "effort": 1345.177045923802,
1224 "time": 74.7320581068779,
1225 "bugs": 0.040619232256751396
1226 }
1227 "#
1228 );
1229 },
1230 );
1231 }
1232
1233 #[cfg(feature = "rust")]
1234 #[test]
1235 fn rust_aliased_primitive_type_classification() {
1236 // Regression for issue #95 (lesson #2): the Rust grammar emits 17
1237 // distinct `kind_id`s for `primitive_type` (one base plus 16
1238 // numeric-suffixed alias variants). `RustCode::is_primitive` in
1239 // `big-code-analysis-ast/src/checker.rs` must list every variant;
1240 // if a future regression omits one, primitive type names emitted
1241 // in that aliased position silently drop into the kind_id-keyed
1242 // operators bucket instead of the text-keyed primitive_operators
1243 // map, miscounting Halstead n1.
1244 //
1245 // The snippet exercises every primitive scalar type across many
1246 // syntactic positions (function parameter types, return types,
1247 // let-binding annotations, `as` casts, const items, type aliases,
1248 // struct fields, function pointer types, tuple types, array types,
1249 // reference types, generic type arguments). Empirically, ordinary
1250 // Rust source emits the base `Rust::PrimitiveType` variant from
1251 // all of these positions; the 16 suffixed alias variants are
1252 // produced by specific grammar productions not reachable from
1253 // user-written code. Mutation-verified: dropping
1254 // `Rust::PrimitiveType` from `is_primitive` fails this test
1255 // (u_operators 30→15). Dropping any single suffixed variant
1256 // currently leaves the test passing; if a future grammar bump
1257 // makes any suffixed variant reachable from idiomatic source,
1258 // extend the snippet so the test fires for that variant too.
1259 check_metrics::<RustParser>(
1260 "const C: u8 = 0;
1261 type T = i64;
1262 struct S { x: u32, y: u64 }
1263 fn g(p: fn(u8) -> u16) -> bool { let _ = p(0); true }
1264 fn f(a: u8, b: u16, c: u32, d: u64) -> u128 {
1265 let _x: i8 = 0;
1266 let _y: i16 = 0;
1267 let _z: i32 = 0;
1268 let _w: i64 = 0;
1269 let _v: i128 = 0;
1270 let _p: f32 = 1.0;
1271 let _q: f64 = 2.0;
1272 let _r: bool = true;
1273 let _s: char = 'x';
1274 let _t: usize = 0;
1275 let _u: isize = 0;
1276 let _arr: [u32; 4] = [0; 4];
1277 let _ref: &u8 = &0;
1278 let _tup: (u32, u64) = (0, 0);
1279 let _opt: Option<u32> = None;
1280 a as u128 + b as u128 + c as u128 + d
1281 }",
1282 "foo.rs",
1283 |metric| {
1284 // Headline: u_operators is the load-bearing assertion —
1285 // the 16 distinct primitive type names dedupe by text in
1286 // the primitive_operators map. Total operators (N1) and
1287 // operand counts pin the rest of the Halstead state.
1288 // Grew from 30 → 33 with the issue #394 fix: `const`,
1289 // `type`, and `struct` keywords are now classified as
1290 // operators (one occurrence each). #1486 adds the
1291 // annotation `:` — one entry, 23 occurrences: `C`, the
1292 // two fields, `p`, the four params of `f`, and the
1293 // fifteen `let` bindings. #1559 bills `Option<u32>`'s
1294 // brackets as one `<>` pair: one entry and one
1295 // occurrence fewer.
1296 assert_eq!(metric.halstead.unique_operators(), 33);
1297 assert_eq!(metric.halstead.total_operators(), 143);
1298 // u_operands / operands grew (was 31/50 before #390): the
1299 // fix now classifies TypeIdentifier (`T`, `S`, `Option`)
1300 // and FieldIdentifier (struct fields `x`, `y`) as operands
1301 // alongside the existing primitive type names.
1302 assert_eq!(metric.halstead.unique_operands(), 36);
1303 assert_eq!(metric.halstead.total_operands(), 55);
1304 },
1305 );
1306 }
1307
1308 #[cfg(feature = "rust")]
1309 #[test]
1310 fn rust_field_identifier_is_operand() {
1311 // Regression for issue #390: prior to the fix, `FieldIdentifier`
1312 // (e.g. the `x` / `y` in `p.x`, `p.y`) fell through to
1313 // `TokenRole::Unknown`, so the field names were not counted
1314 // as operands. Both C++ and Go already classify FieldIdentifier
1315 // as an operand. After the fix:
1316 // unique operators: fn, (), {}, let, =, +, ;, ., ,, :
1317 // unique operands : main, p, Point, x, y, sum, 0, 1
1318 // Field names `x` and `y` each appear twice (`p.x + p.y` and
1319 // the struct literal `Point { x: 0, y: 1 }`).
1320 check_metrics::<RustParser>(
1321 "fn main() {
1322 let p = Point { x: 0, y: 1 };
1323 let sum = p.x + p.y;
1324 }",
1325 "foo.rs",
1326 |metric| {
1327 // Headline: pre-fix, FieldIdentifier (`x`, `y`) and
1328 // TypeIdentifier (`Point`) fell through to Unknown, so
1329 // u_operands was 5 (main, p, sum, 0, 1). After the
1330 // fix, +Point, +x, +y → 8 distinct names.
1331 assert_eq!(metric.halstead.unique_operands(), 8);
1332 assert_eq!(metric.halstead.total_operands(), 12);
1333 // N1: fn, (), {} ×2, let ×2, = ×2, +, ; ×2, . ×2, `,`,
1334 // and the struct literal's two field `:` (#1486) = 16.
1335 assert_eq!(metric.halstead.unique_operators(), 10);
1336 assert_eq!(metric.halstead.total_operators(), 16);
1337 insta::assert_json_snapshot!(
1338 metric.halstead,
1339 @r#"
1340 {
1341 "unique_operators": 10,
1342 "total_operators": 16,
1343 "unique_operands": 8,
1344 "total_operands": 12,
1345 "length": 28,
1346 "estimated_program_length": 57.219280948873624,
1347 "purity_ratio": 2.043545748174058,
1348 "vocabulary": 18,
1349 "volume": 116.75790004038474,
1350 "difficulty": 7.5,
1351 "level": 0.13333333333333333,
1352 "effort": 875.6842503028855,
1353 "time": 48.64912501682697,
1354 "bugs": 0.03051010983631521
1355 }
1356 "#
1357 );
1358 },
1359 );
1360 }
1361
1362 #[cfg(feature = "rust")]
1363 #[test]
1364 fn rust_type_identifier_is_operand() {
1365 // Regression for issue #390: `TypeIdentifier` (e.g. `Vec`,
1366 // `HashMap`, `String` when used as a path name) was dropped to
1367 // `TokenRole::Unknown` for Rust. C++ and Go classify them as
1368 // operands. After the fix, u_operands = 8:
1369 // main, v, m, Vec, HashMap, new, K, V
1370 // (`i32` is a primitive type, classified as an operator.)
1371 //
1372 // Also covers issue #394: `::` is now an operator. The snippet
1373 // has two `::` tokens (`Vec::new`, `HashMap::new`), so n1 grew
1374 // from 10 → 11 and N1 from 17 → 19. #1486 then billed the two
1375 // annotation `:` tokens as their own entry: 12 and 21. #1559
1376 // billed each generic's brackets as one `<>` pair, replacing the
1377 // `<` and `>` entries and their four occurrences: 11 and 19.
1378 check_metrics::<RustParser>(
1379 "fn main() {
1380 let v: Vec<i32> = Vec::new();
1381 let m: HashMap<K, V> = HashMap::new();
1382 }",
1383 "foo.rs",
1384 |metric| {
1385 // Headline: u_operands includes `Vec`, `HashMap`, `K`,
1386 // `V` (and `i32` as a primitive operator). Without the
1387 // fix, Vec/HashMap/K/V silently dropped to Unknown.
1388 assert_eq!(metric.halstead.unique_operands(), 8);
1389 assert_eq!(metric.halstead.total_operands(), 11);
1390 // `::` appears twice (Vec::new, HashMap::new); without
1391 // the #394 fix u_operators was 10 and operators 17.
1392 // `:` appears twice and stays distinct from `::`.
1393 assert_eq!(metric.halstead.unique_operators(), 11);
1394 assert_eq!(metric.halstead.total_operators(), 19);
1395 insta::assert_json_snapshot!(
1396 metric.halstead,
1397 @r#"
1398 {
1399 "unique_operators": 11,
1400 "total_operators": 19,
1401 "unique_operands": 8,
1402 "total_operands": 11,
1403 "length": 30,
1404 "estimated_program_length": 62.05374780501027,
1405 "purity_ratio": 2.068458260167009,
1406 "vocabulary": 19,
1407 "volume": 127.43782540330756,
1408 "difficulty": 7.5625,
1409 "level": 0.1322314049586777,
1410 "effort": 963.7485546125134,
1411 "time": 53.54158636736186,
1412 "bugs": 0.03252279825177962
1413 }
1414 "#
1415 );
1416 },
1417 );
1418 }
1419
1420 /// #1486: Rust's single `:` is vocabulary in every position it is
1421 /// spelled, and stays a separate entry from `::`.
1422 #[cfg(feature = "rust")]
1423 #[test]
1424 fn rust_colon_is_operator_1486() {
1425 const ROWS: &[(&str, &str, [u64; 2])] = &[
1426 // `let`, `:`, `u8`, `=`, `;`
1427 ("let annotation", "let x: u8 = 1;\n", [5, 5]),
1428 // `struct`, `{}`, `:`, `u8`
1429 ("field declaration", "struct S { a: u8 }\n", [4, 4]),
1430 // `const`, `:` twice, `=`, `{}`, `;`
1431 ("field initializer", "const C: S = S { a: 1 };\n", [5, 6]),
1432 // `fn`, `{}` twice, `()`, `let`, `{}` (pattern), `:`, `=`, `;`
1433 ("field pattern", "fn f() { let S { a: b } = s; }\n", [7, 8]),
1434 // `fn`, `()`, `:`, `u8`, `{}`
1435 ("parameter", "fn f(x: u8) {}\n", [5, 5]),
1436 // `fn`, `<>`, `:`, `()`, `{}`
1437 ("trait bound", "fn f<T: Clone>() {}\n", [5, 5]),
1438 // `fn`, `<>`, `()`, `where`, `:`, `{}`
1439 ("where clause", "fn f<T>() where T: Clone {}\n", [6, 6]),
1440 // `fn`, `()`, `{}` twice, `:`, `loop`, `break`, `;`
1441 ("loop label", "fn f() { 'a: loop { break 'a; } }\n", [7, 8]),
1442 // `fn`, `()`, `{}`, `let`, `=`, `|` twice, `:`, `u8`, `;`
1443 (
1444 "closure parameter",
1445 "fn f() { let c = |q: u8| q; }\n",
1446 [9, 10],
1447 ),
1448 // `fn`, `()` twice, `{}`, `let`, `:`, `<>`, `u8`, `=`, `::`,
1449 // `;` — `:` and `::` are two entries
1450 (
1451 "beside a path",
1452 "fn f() { let v: Vec<u8> = Vec::new(); }\n",
1453 [10, 11],
1454 ),
1455 ];
1456 assert_operator_rows::<RustParser>("foo.rs", ROWS);
1457 }
1458
1459 #[cfg(feature = "rust")]
1460 #[test]
1461 fn rust_path_separator_is_operator() {
1462 // Regression for issue #394: `::` (`COLONCOLON`) was missing
1463 // from the Rust `get_op_type` operator arm even though C++,
1464 // Java, C#, and Kotlin all classify it as an operator. Path-
1465 // heavy code (`std::collections::HashMap`, `Vec::new`,
1466 // `T::method`) had every `::` silently dropped into
1467 // TokenRole::Unknown.
1468 //
1469 // Snippet has three `::` tokens (`std::collections::HashMap`,
1470 // counted as two `::` separators, plus `HashMap::new`).
1471 check_metrics::<RustParser>(
1472 "fn main() {
1473 let m = std::collections::HashMap::new();
1474 }",
1475 "foo.rs",
1476 |metric| {
1477 // `::` appears 3 times across the two path expressions
1478 // (`std::collections::HashMap` contributes two; the
1479 // `HashMap::new` contributes one). Pre-fix all three
1480 // dropped to Unknown: u_operators would be 6 (no `::`
1481 // distinct) and total_operators() would be 7 (minus 3 `::`
1482 // occurrences). With the fix u_operators=7 and
1483 // operators=10.
1484 //
1485 // unique operators (post-fix): fn, LPAREN, LBRACE,
1486 // let, =, ::, ;. unique operands: main, m, std,
1487 // collections, HashMap, new.
1488 assert_eq!(metric.halstead.unique_operators(), 7);
1489 assert_eq!(metric.halstead.total_operators(), 10);
1490 assert_eq!(metric.halstead.unique_operands(), 6);
1491 assert_eq!(metric.halstead.total_operands(), 6);
1492 },
1493 );
1494 }
1495
1496 #[cfg(feature = "rust")]
1497 #[test]
1498 fn rust_declaration_keywords_are_operators() {
1499 // Regression for issue #394: the Rust impl already accepted 17
1500 // keywords as operators (As, Async, Await, …, Fn) but omitted
1501 // 14 declaration / visibility keywords. The fix adds `Const`,
1502 // `Static`, `Enum`, `Struct`, `Trait`, `Impl`, `Use`, `Mod`,
1503 // `Pub`, `Type`, `Union`, `Where`, `Extern`, `Dyn`.
1504 //
1505 // Snippet exercises `use`, `pub`, `struct`, and `impl` (one of
1506 // each); together they account for 4 new operator occurrences
1507 // and 4 new unique operators.
1508 check_metrics::<RustParser>(
1509 "use std::fmt;
1510 pub struct S;
1511 impl S { fn n() -> u8 { 0 } }",
1512 "foo.rs",
1513 |metric| {
1514 // expected: unique operators (11) = use, ::, ;, pub,
1515 // struct, impl, LBRACE, fn, LPAREN, DASHGT, u8. Without
1516 // the #394 fix, `use`, `pub`, `struct`, and `impl`
1517 // would each drop to Unknown and u_operators would be
1518 // 7. unique operands (5): std, fmt, S, n, 0.
1519 assert_eq!(metric.halstead.unique_operators(), 11);
1520 assert_eq!(metric.halstead.total_operators(), 13);
1521 assert_eq!(metric.halstead.unique_operands(), 5);
1522 assert_eq!(metric.halstead.total_operands(), 6);
1523 },
1524 );
1525 }
1526
1527 #[cfg(feature = "javascript")]
1528 #[test]
1529 fn javascript_operators_and_operands() {
1530 check_metrics::<JavascriptParser>(
1531 "function main() {
1532 var a, b, c, avg;
1533 a = 5; b = 5; c = 5;
1534 avg = (a + b + c) / 3;
1535 console.log(\"{}\", avg);
1536 }",
1537 "foo.js",
1538 |metric| {
1539 // unique operators: function, (), {}, var, =, +, /, ,, ., ;
1540 // unique operands: main, a, b, c, avg, 3, 5, console, log, "{}"
1541 // `console.log` is the `.` operator applied to the two
1542 // identifier leaves; the composite `member_expression`
1543 // text is deliberately not a third operand (#1263), so
1544 // n2/N2 are 10/20 rather than the pre-#1263 11/21.
1545 insta::assert_json_snapshot!(
1546 metric.halstead,
1547 @r#"
1548 {
1549 "unique_operators": 10,
1550 "total_operators": 24,
1551 "unique_operands": 10,
1552 "total_operands": 20,
1553 "length": 44,
1554 "estimated_program_length": 66.43856189774725,
1555 "purity_ratio": 1.5099673158578921,
1556 "vocabulary": 20,
1557 "volume": 190.16483617504394,
1558 "difficulty": 10.0,
1559 "level": 0.1,
1560 "effort": 1901.6483617504396,
1561 "time": 105.64713120835775,
1562 "bugs": 0.05116412536051621
1563 }
1564 "#
1565 );
1566 },
1567 );
1568 }
1569
1570 #[cfg(feature = "mozjs")]
1571 #[test]
1572 fn mozjs_operators_and_operands() {
1573 check_metrics::<MozjsParser>(
1574 "function main() {
1575 var a, b, c, avg;
1576 a = 5; b = 5; c = 5;
1577 avg = (a + b + c) / 3;
1578 console.log(\"{}\", avg);
1579 }",
1580 "foo.js",
1581 |metric| {
1582 // unique operators: function, (), {}, var, =, +, /, ,, ., ;
1583 // unique operands: main, a, b, c, avg, 3, 5, console, log, "{}"
1584 // `console.log` is the `.` operator applied to the two
1585 // identifier leaves; the composite `member_expression`
1586 // text is deliberately not a third operand (#1263), so
1587 // n2/N2 are 10/20 rather than the pre-#1263 11/21.
1588 insta::assert_json_snapshot!(
1589 metric.halstead,
1590 @r#"
1591 {
1592 "unique_operators": 10,
1593 "total_operators": 24,
1594 "unique_operands": 10,
1595 "total_operands": 20,
1596 "length": 44,
1597 "estimated_program_length": 66.43856189774725,
1598 "purity_ratio": 1.5099673158578921,
1599 "vocabulary": 20,
1600 "volume": 190.16483617504394,
1601 "difficulty": 10.0,
1602 "level": 0.1,
1603 "effort": 1901.6483617504396,
1604 "time": 105.64713120835775,
1605 "bugs": 0.05116412536051621
1606 }
1607 "#
1608 );
1609 },
1610 );
1611 }
1612
1613 #[cfg(feature = "typescript")]
1614 #[test]
1615 fn typescript_operators_and_operands() {
1616 check_metrics::<TypescriptParser>(
1617 "function main() {
1618 var a, b, c, avg;
1619 a = 5; b = 5; c = 5;
1620 avg = (a + b + c) / 3;
1621 console.log(\"{}\", avg);
1622 }",
1623 "foo.ts",
1624 |metric| {
1625 // unique operators: function, (), {}, var, =, +, /, ,, ., ;
1626 // unique operands: main, a, b, c, avg, 3, 5, console, log, "{}"
1627 // `console.log` is the `.` operator applied to the two
1628 // identifier leaves; the composite `member_expression`
1629 // text is deliberately not a third operand (#1263), so
1630 // n2/N2 are 10/20 rather than the pre-#1263 11/21.
1631 insta::assert_json_snapshot!(
1632 metric.halstead,
1633 @r#"
1634 {
1635 "unique_operators": 10,
1636 "total_operators": 24,
1637 "unique_operands": 10,
1638 "total_operands": 20,
1639 "length": 44,
1640 "estimated_program_length": 66.43856189774725,
1641 "purity_ratio": 1.5099673158578921,
1642 "vocabulary": 20,
1643 "volume": 190.16483617504394,
1644 "difficulty": 10.0,
1645 "level": 0.1,
1646 "effort": 1901.6483617504396,
1647 "time": 105.64713120835775,
1648 "bugs": 0.05116412536051621
1649 }
1650 "#
1651 );
1652 },
1653 );
1654 }
1655
1656 #[cfg(feature = "typescript")]
1657 #[test]
1658 fn tsx_operators_and_operands() {
1659 check_metrics::<TsxParser>(
1660 "function main() {
1661 var a, b, c, avg;
1662 a = 5; b = 5; c = 5;
1663 avg = (a + b + c) / 3;
1664 console.log(\"{}\", avg);
1665 }",
1666 "foo.ts",
1667 |metric| {
1668 // unique operators: function, (), {}, var, =, +, /, ,, ., ;
1669 // unique operands: main, a, b, c, avg, 3, 5, console, log, "{}"
1670 // `console.log` is the `.` operator applied to the two
1671 // identifier leaves; the composite `member_expression`
1672 // text is deliberately not a third operand (#1263), so
1673 // n2/N2 are 10/20 rather than the pre-#1263 11/21.
1674 insta::assert_json_snapshot!(
1675 metric.halstead,
1676 @r#"
1677 {
1678 "unique_operators": 10,
1679 "total_operators": 24,
1680 "unique_operands": 10,
1681 "total_operands": 20,
1682 "length": 44,
1683 "estimated_program_length": 66.43856189774725,
1684 "purity_ratio": 1.5099673158578921,
1685 "vocabulary": 20,
1686 "volume": 190.16483617504394,
1687 "difficulty": 10.0,
1688 "level": 0.1,
1689 "effort": 1901.6483617504396,
1690 "time": 105.64713120835775,
1691 "bugs": 0.05116412536051621
1692 }
1693 "#
1694 );
1695 },
1696 );
1697 }
1698
1699 #[cfg(feature = "javascript")]
1700 #[test]
1701 fn javascript_template_string_plain_is_operand() {
1702 // Regression: issue #192. A backtick-delimited `` `hello` ``
1703 // without `${...}` is semantically identical to `"hello"` /
1704 // `'hello'` and must contribute exactly one operand — before
1705 // the fix `TemplateString` fell through to `TokenRole::Unknown`
1706 // and contributed zero. expected: operands are `f` (function
1707 // name) and the wrapping `` `hello` `` template literal →
1708 // u_operands = 2, N2 = 2 (matches the equivalent
1709 // `function f() { return "hello"; }` baseline).
1710 check_metrics::<JavascriptParser>("function f() { return `hello`; }", "foo.js", |metric| {
1711 assert_eq!(metric.halstead.unique_operands(), 2);
1712 assert_eq!(metric.halstead.total_operands(), 2);
1713 });
1714 }
1715
1716 /// Regression for #695. The `get` / `set` property-accessor keywords
1717 /// are operators, matching the C# getter's `Get | Set | Init | Add |
1718 /// Remove` accessor arm. Before #695 the JS family classified them as
1719 /// operands, so the same accessor keyword landed in opposite Halstead
1720 /// groups across languages. This pins them in the operator store and
1721 /// out of the operand store.
1722 #[cfg(feature = "javascript")]
1723 #[test]
1724 fn js_get_set_accessors_are_operators() {
1725 let source = "class C { get x() { return 1; } set x(v) { this._x = v; } }";
1726 let path = PathBuf::from("foo.js");
1727 let parser = JavascriptParser::new(source.as_bytes().to_vec(), &path, None);
1728 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
1729 assert!(
1730 ops.operators.iter().any(|o| o.as_str() == "get")
1731 && ops.operators.iter().any(|o| o.as_str() == "set"),
1732 "`get`/`set` accessors must be operators; operators were {:?}",
1733 ops.operators
1734 );
1735 assert!(
1736 !ops.operands.iter().any(|o| o.as_str() == "get")
1737 && !ops.operands.iter().any(|o| o.as_str() == "set"),
1738 "`get`/`set` accessors must not be operands; operands were {:?}",
1739 ops.operands
1740 );
1741 }
1742
1743 #[cfg(feature = "javascript")]
1744 #[test]
1745 fn javascript_template_string_interpolation_no_double_count() {
1746 // Regression: issue #192. An interpolated template literal
1747 // `` `Hi ${name}!` `` used to fall through to `Unknown`,
1748 // dropping the wrapper from the count entirely; the inner
1749 // `name` was still walked and counted via the
1750 // `TemplateSubstitution` child. Mirrors #183 (C#), #191
1751 // (Kotlin), #199 (Perl): the wrapper is skipped when a
1752 // `TemplateSubstitution` child is present so the inner
1753 // expression is not double-counted.
1754 //
1755 // expected: for `function f(name) { return ` + "`Hi ${name}!`"
1756 // + `; }`, operands are `f` and `name` (twice — `name` as the
1757 // parameter, then again inside the interpolation), so
1758 // u_operands = 2 and N2 = 3. Without the wrapper-skip guard
1759 // the wrapping literal would also be counted, lifting
1760 // u_operands to 3 and N2 to 4.
1761 check_metrics::<JavascriptParser>(
1762 "function f(name) { return `Hi ${name}!`; }",
1763 "foo.js",
1764 |metric| {
1765 assert_eq!(metric.halstead.unique_operands(), 2);
1766 assert_eq!(metric.halstead.total_operands(), 3);
1767 },
1768 );
1769 }
1770
1771 #[cfg(feature = "mozjs")]
1772 #[test]
1773 fn mozjs_template_string_plain_is_operand() {
1774 // Regression: issue #192. Mirrors
1775 // `javascript_template_string_plain_is_operand` for the
1776 // Firefox-mode dialect — the four JS-family `get_op_type`
1777 // impls share the same template-literal handling.
1778 check_metrics::<MozjsParser>("function f() { return `hello`; }", "foo.js", |metric| {
1779 assert_eq!(metric.halstead.unique_operands(), 2);
1780 assert_eq!(metric.halstead.total_operands(), 2);
1781 });
1782 }
1783
1784 #[cfg(feature = "mozjs")]
1785 #[test]
1786 fn mozjs_template_string_interpolation_no_double_count() {
1787 // Regression: issue #192. Mirrors
1788 // `javascript_template_string_interpolation_no_double_count`
1789 // for the Firefox-mode dialect.
1790 check_metrics::<MozjsParser>(
1791 "function f(name) { return `Hi ${name}!`; }",
1792 "foo.js",
1793 |metric| {
1794 assert_eq!(metric.halstead.unique_operands(), 2);
1795 assert_eq!(metric.halstead.total_operands(), 3);
1796 },
1797 );
1798 }
1799
1800 /// One JSX shape per row of #1483's decision: the source, the
1801 /// expected `n2` / `N2`, and the deduplicated operand vocabulary.
1802 /// Every row is a single element bound to `a`, so the operands
1803 /// outside the element's content are always `a` plus the tag names.
1804 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
1805 const JSX_TEXT_CASES: &[(&str, &str, [u64; 2], &[&str])] = &[
1806 // Element text is one operand. N2: a, p x2 (both tags), hi.
1807 ("text", "const a = <p>hi</p>;\n", [3, 4], &["a", "p", "hi"]),
1808 // The same-line space between `<b/>` and `<i/>` is a jsx_text
1809 // node of its own, and bills nothing. Billing it would key an
1810 // empty operand: (5, 6).
1811 (
1812 "whitespace",
1813 "const a = <p><b/> <i/></p>;\n",
1814 [4, 5],
1815 &["a", "p", "b", "i"],
1816 ),
1817 // `Start` at two indentation depths is one distinct operand,
1818 // because the key is the trimmed text. Untrimmed: (5, 7).
1819 (
1820 "indentation",
1821 "const a = (\n <p>\n Start\n <span>\n Start\n </span>\n </p>\n);\n",
1822 [4, 7],
1823 &["a", "p", "span", "Start"],
1824 ),
1825 // An entity splits the text into three siblings, each an
1826 // operand. Without the entity arm: (4, 5).
1827 (
1828 "entity in text",
1829 "const a = <p>x & y</p>;\n",
1830 [5, 6],
1831 &["a", "p", "x", "&", "y"],
1832 ),
1833 // Inside an attribute string the entity is part of the string
1834 // operand and bills nothing of its own. Without the parent
1835 // guard: (5, 5).
1836 (
1837 "entity in attribute",
1838 "const a = <p title=\"x & y\" />;\n",
1839 [4, 4],
1840 &["a", "p", "title", "\"x & y\""],
1841 ),
1842 // An expression child is unchanged by #1483: its identifier was
1843 // already an operand.
1844 (
1845 "expression",
1846 "const a = <p>{v}</p>;\n",
1847 [3, 4],
1848 &["a", "p", "v"],
1849 ),
1850 ];
1851
1852 /// Asserts every row of [`JSX_TEXT_CASES`] under parser `T`, through
1853 /// the metrics store (`n2` / `N2`) and the `--ops` vocabulary.
1854 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
1855 #[track_caller]
1856 fn assert_jsx_text_operands<T: crate::MetricSuite>(file: &str) {
1857 for (label, source, expected, vocabulary) in JSX_TEXT_CASES {
1858 crate::test_support::check_func_space_only::<T, _>(
1859 source,
1860 file,
1861 &[crate::Metric::Halstead],
1862 |space| {
1863 let halstead = &space.metrics.halstead;
1864 assert_eq!(
1865 [halstead.unique_operands(), halstead.total_operands()],
1866 *expected,
1867 "{file}: {label}"
1868 );
1869 },
1870 );
1871 let ops = ops_of::<T>(source, file);
1872 let got: BTreeSet<&str> = ops.operands.iter().map(String::as_str).collect();
1873 let want: BTreeSet<&str> = vocabulary.iter().copied().collect();
1874 assert_eq!(got, want, "{file}: {label} operand vocabulary");
1875 }
1876 }
1877
1878 /// Regression for #1483: JSX element text and its character
1879 /// references are operands. One row per grammar, so reverting one
1880 /// invocation's `jsx:` argument fails its own row.
1881 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
1882 #[test]
1883 fn jsx_text_is_an_operand() {
1884 // A whitespace-only node bills nothing, so no count can tell the
1885 // "whitespace" row's fixture from one that stopped spelling the
1886 // same-line space. Pin the node's presence directly.
1887 let (_, source, _, _) = JSX_TEXT_CASES
1888 .iter()
1889 .find(|(label, ..)| *label == "whitespace")
1890 .expect("the whitespace row exists");
1891 let parser =
1892 JavascriptParser::new(source.as_bytes().to_vec(), &PathBuf::from("foo.jsx"), None);
1893 assert!(
1894 parser.root().preorder().any(|n| {
1895 n.kind_id() == Javascript::JsxText as u16
1896 && source.as_bytes()[n.start_byte()..n.end_byte()]
1897 .trim_ascii()
1898 .is_empty()
1899 }),
1900 "the whitespace row must carry a whitespace-only jsx_text node"
1901 );
1902
1903 assert_jsx_text_operands::<JavascriptParser>("foo.jsx");
1904 assert_jsx_text_operands::<MozjsParser>("foo.js");
1905 assert_jsx_text_operands::<TsxParser>("foo.tsx");
1906 }
1907
1908 /// The `.ts` grammar's enum carries `JsxText` in the externals it
1909 /// shares with TSX, but no production emits it, which is why the
1910 /// TypeScript invocation passes no `jsx:` argument (#1483). Pinned
1911 /// so a grammar that starts emitting it cannot go unbilled quietly.
1912 #[cfg(feature = "typescript")]
1913 #[test]
1914 fn typescript_never_emits_jsx_text() {
1915 let parser = TypescriptParser::new(
1916 b"const a = <p>hi</p>;\nlet b = <T>(x);\n".to_vec(),
1917 &PathBuf::from("foo.ts"),
1918 None,
1919 );
1920 // The positive half: the parse reached the angle-bracket syntax
1921 // TSX would read as JSX, so the absence below is a property of
1922 // the grammar rather than of a parse that produced nothing.
1923 assert!(
1924 ast_has_kind_id(&parser, Typescript::TypeAssertion as u16),
1925 "fixture lost the `<T>(x)` type assertion"
1926 );
1927 assert!(
1928 !ast_has_kind_id(&parser, Typescript::JsxText as u16),
1929 "the .ts grammar now emits jsx_text; give its invocation a `jsx:` argument"
1930 );
1931 }
1932
1933 #[cfg(feature = "typescript")]
1934 #[test]
1935 fn typescript_template_string_plain_is_operand() {
1936 // Regression: issue #192. Mirrors
1937 // `javascript_template_string_plain_is_operand` for
1938 // TypeScript — the four JS-family `get_op_type` impls share
1939 // the same template-literal handling.
1940 //
1941 // The `: string` annotation contributes no operand — its
1942 // keyword counts once, as the text-keyed operator (#1261) — so
1943 // the operands are `f` and `` `hello` `` (2 each). The headline
1944 // of this test — that the plain template literal contributes
1945 // one operand — is unaffected.
1946 check_metrics::<TypescriptParser>(
1947 "function f(): string { return `hello`; }",
1948 "foo.ts",
1949 |metric| {
1950 assert_eq!(metric.halstead.unique_operands(), 2);
1951 assert_eq!(metric.halstead.total_operands(), 2);
1952 },
1953 );
1954 }
1955
1956 #[cfg(feature = "typescript")]
1957 #[test]
1958 fn typescript_template_string_interpolation_no_double_count() {
1959 // Regression: issue #192. Mirrors
1960 // `javascript_template_string_interpolation_no_double_count`
1961 // for TypeScript.
1962 //
1963 // The `: string` annotations contribute no operands (#1261).
1964 // Unique operands: `f`, `name` (2). Total operands: `f`, `name`
1965 // (param), `name` (in the interpolation) (3). The interpolation
1966 // guard from #192 still holds — the wrapping `` `Hi ${name}!` ``
1967 // is `Unknown`, not double-counted.
1968 check_metrics::<TypescriptParser>(
1969 "function f(name: string): string { return `Hi ${name}!`; }",
1970 "foo.ts",
1971 |metric| {
1972 assert_eq!(metric.halstead.unique_operands(), 2);
1973 assert_eq!(metric.halstead.total_operands(), 3);
1974 },
1975 );
1976 }
1977
1978 #[cfg(feature = "typescript")]
1979 #[test]
1980 fn tsx_template_string_plain_is_operand() {
1981 // Regression: issue #192. Mirrors
1982 // `javascript_template_string_plain_is_operand` for the
1983 // TSX (TypeScript + JSX) variant.
1984 //
1985 // TSX's type-keyword `string` (`String3`) contributes no
1986 // operand, mirroring TS::String2 (#1261): operands are `f` and
1987 // `` `hello` `` (2 each).
1988 check_metrics::<TsxParser>(
1989 "function f(): string { return `hello`; }",
1990 "foo.tsx",
1991 |metric| {
1992 assert_eq!(metric.halstead.unique_operands(), 2);
1993 assert_eq!(metric.halstead.total_operands(), 2);
1994 },
1995 );
1996 }
1997
1998 #[cfg(feature = "typescript")]
1999 #[test]
2000 fn tsx_template_string_interpolation_no_double_count() {
2001 // Regression: issue #192. Mirrors
2002 // `javascript_template_string_interpolation_no_double_count`
2003 // for the TSX (TypeScript + JSX) variant.
2004 //
2005 // The `: string` annotations contribute no `String3` operands
2006 // (#1261); see `typescript_template_string_…` for the count
2007 // derivation.
2008 check_metrics::<TsxParser>(
2009 "function f(name: string): string { return `Hi ${name}!`; }",
2010 "foo.tsx",
2011 |metric| {
2012 assert_eq!(metric.halstead.unique_operands(), 2);
2013 assert_eq!(metric.halstead.total_operands(), 3);
2014 },
2015 );
2016 }
2017
2018 /// The JS-family regex fixture, asserted against all four grammars.
2019 ///
2020 /// `impl_js_family_get_op_type!` is instantiated four times against
2021 /// four distinct `kind_id` enums (`SLASH` 87/81/90/87, `Regex`
2022 /// 224/250/264/225), so each expansion is a separate compiled arm
2023 /// and a drift in one grammar is invisible if only one is checked
2024 /// (grammar-dispatch section 11).
2025 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2026 fn assert_js_family_counts(source: &str, expected: [u64; 4]) {
2027 assert_halstead_counts::<JavascriptParser>(source, "foo.js", expected, "javascript");
2028 assert_halstead_counts::<MozjsParser>(source, "foo.jsm", expected, "mozjs");
2029 assert_halstead_counts::<TypescriptParser>(source, "foo.ts", expected, "typescript");
2030 assert_halstead_counts::<TsxParser>(source, "foo.tsx", expected, "tsx");
2031 }
2032
2033 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2034 #[test]
2035 fn js_family_regex_delimiters_are_not_operators() {
2036 // Regression: issue #1314, the JS-family sibling of Elixir
2037 // #1256 and Ruby/Perl #1312. A `regex` literal spells both of
2038 // its delimiters `SLASH` — the kind id real division uses — so
2039 // `const a = /abc/g;` reported a `/` operator with no division
2040 // in the source, and n1/N1 counted the literal's punctuation as
2041 // arithmetic.
2042 //
2043 // The same fixture pins the second, independent half: `Regex`
2044 // was in neither arm, so the literal contributed no operand
2045 // either and reached the vocabulary from *neither* side.
2046 //
2047 // expected: operators `const`, `=`, `;`, `let` → n1 = 4;
2048 // `const` `=` `;` on line 1, `let` `=` `;` on line 2, `=` `;`
2049 // on line 3 → N1 = 8. Operands `a`, `/abc/g`, `b` → n2 = 3,
2050 // with `a` used three times and `b` twice → N2 = 6.
2051 //
2052 // Before the fix: n1 = 5 and N1 = 10 (the two fabricated `/`),
2053 // n2 = 2 and N2 = 5 (no operand for the literal).
2054 //
2055 // The four values are deliberately distinct so no transposition
2056 // of the unique-vs-total axes inside `assert_halstead_counts`
2057 // can pass (#1312).
2058 assert_js_family_counts("const a = /abc/g;\nlet b = a;\nb = a;\n", [4, 8, 3, 6]);
2059 }
2060
2061 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2062 #[test]
2063 fn js_family_division_survives_the_regex_guard() {
2064 // Control for #1314: the guard is scoped to a `Regex` parent,
2065 // so real division must still count. This fixture holds both
2066 // sides at once — two divisions and one regex literal — so a
2067 // guard widened to every `SLASH` fails here rather than
2068 // silently passing the test above.
2069 //
2070 // expected: operators `const`, `=`, `;`, `/` → n1 = 4; two
2071 // `const`, two `=`, two `;` and two `/` → N1 = 8. Operands
2072 // `q`, `a`, `b`, `c`, `r`, `/x/` → n2 = N2 = 6.
2073 assert_js_family_counts("const q = a / b / c;\nconst r = /x/;\n", [4, 8, 6, 6]);
2074 }
2075
2076 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2077 #[test]
2078 fn js_regex_delimiter_guard_is_parent_scoped_is_unobservable() {
2079 // Companion to the two above, and a statement of what they do
2080 // *not* cover. Ruby's guard has
2081 // `ruby_regex_guard_is_parent_scoped_not_ancestor_scoped`
2082 // because a division inside `#{…}` sits under a `Regex`
2083 // ancestor without being its child. No JS fixture can do that:
2084 // a regex literal admits no nested expression at all, its
2085 // `regex_pattern` and `regex_flags` children being leaves. So
2086 // the ancestor-scoped mutant of this guard — the one #1256's
2087 // post-mortem says survives every ordinary fixture — is
2088 // unobservable here. Measured, not assumed.
2089 //
2090 // Rather than write a fixture that would pass under both
2091 // spellings and read as coverage, pin the grammar property the
2092 // claim rests on: within a fixture that puts a division, a
2093 // template substitution and a regex in one file, every `SLASH`
2094 // reachable *below* a `Regex` is its immediate child. Should a
2095 // bump start nesting expressions inside a regex, this turns red
2096 // and the distinction becomes both observable and worth a real
2097 // test.
2098 //
2099 // Checked against all four grammars, not just JavaScript: the
2100 // guard is instantiated four times against four distinct enums,
2101 // and the property this test exists to watch could hold in one
2102 // and lapse in another.
2103 let source = b"const a = /abc/g;\nconst q = x / y;\nconst t = `p ${x / y} ${/zz/} q`;\n";
2104 assert_regex_slashes_are_immediate_children::<crate::langs::JavascriptCode>(
2105 source,
2106 Javascript::SLASH as u16,
2107 Javascript::Regex as u16,
2108 "javascript",
2109 );
2110 assert_regex_slashes_are_immediate_children::<crate::langs::MozjsCode>(
2111 source,
2112 Mozjs::SLASH as u16,
2113 Mozjs::Regex as u16,
2114 "mozjs",
2115 );
2116 assert_regex_slashes_are_immediate_children::<crate::langs::TypescriptCode>(
2117 source,
2118 Typescript::SLASH as u16,
2119 Typescript::Regex as u16,
2120 "typescript",
2121 );
2122 assert_regex_slashes_are_immediate_children::<crate::langs::TsxCode>(
2123 source,
2124 Tsx::SLASH as u16,
2125 Tsx::Regex as u16,
2126 "tsx",
2127 );
2128 }
2129
2130 /// Asserts every `slash` token below a `regex` node in `source` is
2131 /// that node's *immediate* child, for one grammar.
2132 ///
2133 /// Backs `js_regex_delimiter_guard_is_parent_scoped_is_unobservable`
2134 /// — see there for why the property is worth pinning.
2135 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2136 fn assert_regex_slashes_are_immediate_children<L: crate::traits::LanguageInfo>(
2137 source: &[u8],
2138 slash: u16,
2139 regex: u16,
2140 label: &str,
2141 ) {
2142 let mut slashes_below_a_regex = 0;
2143 let visited = for_each_node_with_chain::<L>(source, |node: &Node<'_>, chain| {
2144 if node.kind_id() != slash {
2145 return;
2146 }
2147 let Some(depth) = chain.iter().position(|a| a.kind_id() == regex) else {
2148 return;
2149 };
2150 slashes_below_a_regex += 1;
2151 assert_eq!(
2152 depth,
2153 chain.len() - 1,
2154 "{label}: a slash at row {} has a regex ancestor that is not its parent, so \
2155 the parent-vs-ancestor mutant is now observable and needs a real test",
2156 node.start_row()
2157 );
2158 });
2159 assert!(visited > 20, "{label}: fixture is too small to prove much");
2160 // Without this the assertion above is vacuous whenever the
2161 // fixture stops containing a regex at all — the failure mode a
2162 // filter that matches nothing always has.
2163 assert_eq!(
2164 slashes_below_a_regex, 4,
2165 "{label}: expected the two regex literals' four delimiters; the fixture no \
2166 longer exercises what this test claims"
2167 );
2168 }
2169
2170 // Issue #281: optional chaining (`?.`) was double-counted as a
2171 // Halstead operator in TypeScript and TSX because the grammar
2172 // exposes both an `optional_chain` named wrapper AND a child
2173 // `?.` token, and both were classified as `Operator`. The fix
2174 // counts only the bare `?.` token (`QMARKDOT`) in TS/TSX so each
2175 // textual `?.` contributes exactly once, matching JS / MozJS
2176 // (whose grammars expose only `OptionalChain` — the `?.` token
2177 // itself).
2178 //
2179 // The four assertions below all compare against the same totals:
2180 // for `function f(a) { return a?.b?.c; }` the operator stream is
2181 // `function`, `(`, `{`, `return`, `?.`, `?.`, `;` (7 total, 6
2182 // unique — `LPAREN`/`LBRACE` count once, closing tokens are not
2183 // in the operator set). Before the fix, TS/TSX reported 9/7
2184 // instead of 7/6.
2185 #[cfg(feature = "javascript")]
2186 #[test]
2187 fn javascript_optional_chain_not_double_counted_in_halstead_281() {
2188 check_metrics::<JavascriptParser>("function f(a) { return a?.b?.c; }", "foo.js", |m| {
2189 assert_eq!(m.halstead.unique_operators(), 6);
2190 assert_eq!(m.halstead.total_operators(), 7);
2191 });
2192 }
2193
2194 #[cfg(feature = "mozjs")]
2195 #[test]
2196 fn mozjs_optional_chain_not_double_counted_in_halstead_281() {
2197 check_metrics::<MozjsParser>("function f(a) { return a?.b?.c; }", "foo.js", |m| {
2198 assert_eq!(m.halstead.unique_operators(), 6);
2199 assert_eq!(m.halstead.total_operators(), 7);
2200 });
2201 }
2202
2203 #[cfg(feature = "typescript")]
2204 #[test]
2205 fn typescript_optional_chain_not_double_counted_in_halstead_281() {
2206 // The TS grammar wraps member-expression `?.` in an
2207 // `optional_chain` named node containing the bare `?.`
2208 // token; classifying both as `Operator` double-counted the
2209 // chain. We now count only the bare token, so TS matches JS.
2210 check_metrics::<TypescriptParser>("function f(a) { return a?.b?.c; }", "foo.ts", |m| {
2211 assert_eq!(m.halstead.unique_operators(), 6);
2212 assert_eq!(m.halstead.total_operators(), 7);
2213 });
2214 }
2215
2216 #[cfg(feature = "typescript")]
2217 #[test]
2218 fn tsx_optional_chain_not_double_counted_in_halstead_281() {
2219 check_metrics::<TsxParser>("function f(a) { return a?.b?.c; }", "foo.tsx", |m| {
2220 assert_eq!(m.halstead.unique_operators(), 6);
2221 assert_eq!(m.halstead.total_operators(), 7);
2222 });
2223 }
2224
2225 // Issue #299: parity guard for the JS-family `get_op_type` macro
2226 // on the optional-chain operator token (#281's prior regression
2227 // surface). All four languages must classify the bare `?.` token
2228 // identically — `OptionalChain` in JS/MozJS, `QMARKDOT` in
2229 // TS/TSX — and emit the same totals for
2230 // `function f(a) { return a?.b?.c; }`:
2231 //
2232 // * Operators: `function`, `(`, `{`, `return`, `?.`, `?.`, `;`
2233 // (7 total, 6 unique).
2234 // * Operands: `f`, `a` (parameter), `a`, `b`, `c` — the identifier
2235 // and property leaves only (5 total, 4 unique). Until #1263 the
2236 // two wrapping member expressions (`a?.b`, `a?.b?.c`) were
2237 // classified as `MemberExpression*` operands on top of the leaves
2238 // they contain, making this 7 total / 6 unique.
2239 //
2240 // Verified by test-via-revert: dropping `OptionalChain` from
2241 // JS/MozJS, or `QMARKDOT` from TS/TSX, trips the test
2242 // (u_operators 6→5). This input does NOT exercise every operand
2243 // alias in the per-language `operand_extras` (`Identifier2`, the
2244 // JS/MozJS/TSX string-literal `String2`); drift in
2245 // those is out of scope for this regression guard and would need a
2246 // separate fixture. The `PredefinedType` operator path (`: void`
2247 // double-count) is now covered by `ts_void_return_type_single_operator_453`
2248 // below.
2249 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2250 #[test]
2251 fn js_family_get_op_type_parity_optional_chain_member_299() {
2252 // Non-capturing closure (coerced to the `fn` pointer that
2253 // `check_metrics` accepts) avoids the
2254 // `clippy::needless_pass_by_value` warning that a free `fn`
2255 // taking `CodeMetrics` by value would trigger.
2256 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2257 const SRC: &str = "function f(a) { return a?.b?.c; }";
2258 let check = |m: crate::CodeMetrics| {
2259 assert_eq!(m.halstead.unique_operators(), 6);
2260 assert_eq!(m.halstead.total_operators(), 7);
2261 assert_eq!(m.halstead.unique_operands(), 4);
2262 assert_eq!(m.halstead.total_operands(), 5);
2263 };
2264
2265 check_metrics::<JavascriptParser>(SRC, "foo.js", check);
2266 check_metrics::<MozjsParser>(SRC, "foo.js", check);
2267 check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2268 check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2269 }
2270
2271 // Issue #1263: a member access contributes its leaves and the `.`
2272 // operator, never the `member_expression` composite as well. The
2273 // classification does not stop the walk, so `a` and `b` were always
2274 // counted; listing the wrapper billed a third operand keyed on the
2275 // whole `a.b` text, which no other language here does.
2276 //
2277 // expected, for `var r = a.b;`:
2278 //
2279 // * Operators: `var`, `=`, `.`, `;` — 4 total, 4 unique.
2280 // * Operands: `r`, `a`, `b` — 3 total, 3 unique. Before the fix
2281 // the `member_expression` wrapper added `a.b`, making both 4.
2282 //
2283 // All four JS-family languages are asserted because
2284 // `impl_js_family_get_op_type!` emits one shared operand arm: the
2285 // lockstep is the point of the macro, and a per-language extras
2286 // list is exactly where a future edit could break it.
2287 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2288 #[test]
2289 fn js_family_member_access_counts_leaves_not_the_composite_1263() {
2290 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2291 const SRC: &str = "var r = a.b;";
2292 let check = |m: crate::CodeMetrics| {
2293 assert_eq!(m.halstead.unique_operators(), 4);
2294 assert_eq!(m.halstead.total_operators(), 4);
2295 assert_eq!(m.halstead.unique_operands(), 3);
2296 assert_eq!(m.halstead.total_operands(), 3);
2297 };
2298
2299 check_metrics::<JavascriptParser>(SRC, "foo.js", check);
2300 check_metrics::<MozjsParser>(SRC, "foo.js", check);
2301 check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2302 check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2303 }
2304
2305 // Issue #1263, the grammar-dispatch section 6 half: dropping
2306 // `MemberExpression*` from the operand arm would have regressed
2307 // private-field access to *zero* operands for the field, because
2308 // `PrivatePropertyIdentifier` — the `#x` leaf — was in no operand
2309 // list and the composite `this.#x` had been its only count. Adding
2310 // the leaf also fixes the declaration site `#x = 1`, which no
2311 // wrapper covered and which therefore counted nothing at all.
2312 //
2313 // expected, for `class C { #x = 1; m() { return this.#x; } }`:
2314 //
2315 // * Operators: `class`, `{`×2 (class body, method body), `=`,
2316 // `;`×2, `(`, `return`, `.` — 9 total, 7 unique. (`class` joined
2317 // the JS-family operator arm in #1552.)
2318 // * Operands: `C`, `#x`, `1`, `m`, `this`, `#x` — 6 total, 5
2319 // unique in all four grammars. Under TS/TSX the class *name* `C`
2320 // is a `type_identifier`, which those getters did not classify
2321 // until #1557, so they read 5/4 before it.
2322 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2323 #[test]
2324 fn js_family_private_field_leaf_is_the_operand_1263() {
2325 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2326 const SRC: &str = "class C { #x = 1; m() { return this.#x; } }";
2327 let check = |m: crate::CodeMetrics| {
2328 assert_eq!(m.halstead.unique_operators(), 7);
2329 assert_eq!(m.halstead.total_operators(), 9);
2330 assert_eq!(m.halstead.unique_operands(), 5);
2331 assert_eq!(m.halstead.total_operands(), 6);
2332 };
2333
2334 check_metrics::<JavascriptParser>(SRC, "foo.js", check);
2335 check_metrics::<MozjsParser>(SRC, "foo.js", check);
2336 check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2337 check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2338 }
2339
2340 // Issue #1263, the other section 6 half: `meta_property` is the one
2341 // composite the leaves-not-composites drop has to keep. `import.meta`
2342 // / `new.target` have no classified leaf — `meta` and `target` are
2343 // anonymous tokens in no arm — so with `MemberExpression*` gone the
2344 // meta-object contributed no operand at all while `this.env.x` still
2345 // yielded three.
2346 //
2347 // expected operands, for `var t = import.meta.url; function f() {
2348 // return new.target; }`: `t`, `import.meta`, `url`, `f`,
2349 // `new.target` — 5 total, 5 unique. Operators are deliberately not
2350 // asserted: the `import` / `new` keyword tokens inside the
2351 // meta-property keep their pre-existing operator classification,
2352 // which this fixture neither pins nor contests.
2353 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2354 #[test]
2355 fn js_family_meta_property_is_one_operand_1263() {
2356 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
2357 const SRC: &str = "var t = import.meta.url; function f() { return new.target; }";
2358 let check = |m: crate::CodeMetrics| {
2359 assert_eq!(m.halstead.unique_operands(), 5);
2360 assert_eq!(m.halstead.total_operands(), 5);
2361 };
2362
2363 check_metrics::<JavascriptParser>(SRC, "foo.js", check);
2364 check_metrics::<MozjsParser>(SRC, "foo.js", check);
2365 check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2366 check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2367 }
2368
2369 // Issue #1263: TS/TSX `nested_identifier` (`namespace N.M`) is the
2370 // same container/leaf double-count as `member_expression`.
2371 //
2372 // expected, for `namespace N.M { }`:
2373 //
2374 // * Operators: `namespace`, `.`, `{` — 3 total, 3 unique.
2375 // (`namespace` joined the TS operator extras in #1557.)
2376 // * Operands: `N`, `M` — 2 total, 2 unique. Before the fix the
2377 // `nested_identifier` added `N.M`, making both 3.
2378 #[cfg(feature = "typescript")]
2379 #[test]
2380 fn ts_nested_identifier_counts_leaves_not_the_composite_1263() {
2381 const SRC: &str = "namespace N.M { }";
2382 let check = |m: crate::CodeMetrics| {
2383 assert_eq!(m.halstead.unique_operators(), 3);
2384 assert_eq!(m.halstead.total_operators(), 3);
2385 assert_eq!(m.halstead.unique_operands(), 2);
2386 assert_eq!(m.halstead.total_operands(), 2);
2387 };
2388
2389 check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2390 check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2391 }
2392
2393 // Issue #1261 (inverting the #313 pin): the `"string"` type-keyword
2394 // aliases the TS / TSX grammars expose must contribute NO operand.
2395 // #313 put them in `operand_extras` for parity with the then-wider
2396 // `Checker::is_string`, but the `predefined_type` wrapper already
2397 // counts as the text-keyed `"string"` operator, so one `: string`
2398 // token tallied as operator AND operand while `: number` counted
2399 // once. #1261 drops the aliases from both `operand_extras` and
2400 // `is_string`, so the keyword counts once, as the operator.
2401 //
2402 // For the input `let x: string = "y";`:
2403 //
2404 // * TypeScript emits `Typescript::String2` for the `string` type
2405 // keyword (kind_id 135, in the type-keyword block of the enum).
2406 // * TSX emits `Tsx::String3` for the same role (kind_id 141).
2407 //
2408 // Verified by test-via-revert: restoring `String2` to TS's
2409 // `operand_extras` (or `String3` to TSX's) trips this test on
2410 // `u_operands` / `operands` for the affected language.
2411 #[cfg(feature = "typescript")]
2412 #[test]
2413 fn ts_family_type_keyword_counts_once_1261() {
2414 const SRC: &str = "let x: string = \"y\";";
2415 // Operators (n1 = 5, N1 = 5):
2416 // `let`, `:`, `=`, `;`, plus `string` (PredefinedType wrapper,
2417 // routed through `is_primitive` so it's keyed by its lexeme
2418 // `"string"` in `primitive_operators`).
2419 // Operands (n2 = 2, N2 = 2):
2420 // `x` and the `"y"` literal. Under #313 the type-keyword
2421 // child of `predefined_type` added a third, phantom
2422 // `"string"` operand (n2 = 3 / N2 = 3).
2423 let check = |m: crate::CodeMetrics| {
2424 assert_eq!(m.halstead.unique_operators(), 5);
2425 assert_eq!(m.halstead.total_operators(), 5);
2426 assert_eq!(m.halstead.unique_operands(), 2);
2427 assert_eq!(m.halstead.total_operands(), 2);
2428 };
2429
2430 check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2431 check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2432 }
2433
2434 // Issue #1261 regression, the issue's reproducer plus a literal
2435 // whose contents spell the keyword: `: string` and `: number` must
2436 // contribute symmetrically — one text-keyed operator each, zero
2437 // operands — while a string *literal* `"string"` stays an operand
2438 // (distinct from the keyword: TS kind `String`, TSX kind `String2`,
2439 // both quoted in the operand key).
2440 #[cfg(feature = "typescript")]
2441 #[test]
2442 fn ts_family_string_annotation_symmetric_with_number_1261() {
2443 const SRC: &str = "let x: string = \"a\";\nlet y: number = 1;\nlet s = \"string\";";
2444 // Operators (n1 = 6, N1 = 13):
2445 // `let` ×3, `:` ×2, `=` ×3, `;` ×3, `string` ×1, `number` ×1.
2446 // Pre-fix N1 was identical — the wrapper operator was always
2447 // counted; the defect was the extra operand below.
2448 // Operands (n2 = 6, N2 = 6):
2449 // `x`, `"a"`, `y`, `1`, `s`, `"string"` — one each. Pre-fix
2450 // the `: string` keyword added a bare `string` operand
2451 // (n2 = 7 / N2 = 7) that `: number` had no analogue of.
2452 let check = |m: crate::CodeMetrics| {
2453 assert_eq!(m.halstead.unique_operators(), 6);
2454 assert_eq!(m.halstead.total_operators(), 13);
2455 assert_eq!(m.halstead.unique_operands(), 6);
2456 assert_eq!(m.halstead.total_operands(), 6);
2457 };
2458
2459 check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2460 check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2461 }
2462
2463 /// Drift marker for #1261 (lesson 34 / grammar-dispatch §2): the
2464 /// anonymous `string` type-keyword token appears **only** as a
2465 /// `predefined_type` child.
2466 ///
2467 /// That is the whole argument for classifying the keyword without a
2468 /// parent guard — the wrapper is guaranteed to be there to carry the
2469 /// operator. The two tests above measure the consequence and would
2470 /// still pass if the grammar started emitting the keyword somewhere
2471 /// else, as long as their own two fixtures kept their counts; this one
2472 /// measures the premise, over every position #1261's dump probes
2473 /// covered: annotation, parameter and return type, union member,
2474 /// generic argument, and template-literal type. A string *literal*
2475 /// spelling `"string"` is a different kind and must not be confused
2476 /// for the keyword, so one is in the fixture too.
2477 #[cfg(feature = "typescript")]
2478 #[test]
2479 fn ts_family_type_keyword_only_appears_under_predefined_type_1261() {
2480 // Exercises each position the keyword can take. Valid in both
2481 // grammars: no angle-bracket cast, which TSX would read as JSX.
2482 const SRC: &str = "const a: string = \"string\";\n\
2483 function f(x: string): string {\n\
2484 return x;\n\
2485 }\n\
2486 type U = string | number;\n\
2487 type A = Array<string>;\n\
2488 type M = Map<string, number>;\n\
2489 type T = `id-${string}`;\n";
2490 // Seven type positions: `a`, `x`, `f`'s return, the union member,
2491 // `Array`'s argument, `Map`'s first argument, and the template
2492 // placeholder. The `"string"` initialiser is a literal, not the
2493 // keyword, and must not be among them.
2494 const EXPECTED_OCCURRENCES: usize = 7;
2495
2496 fn keyword_occurrences<P: ParserTrait>(
2497 path: &str,
2498 keyword: u16,
2499 predefined_type: u16,
2500 ) -> usize {
2501 let parser = P::new(SRC.as_bytes().to_vec(), &PathBuf::from(path), None);
2502 parser
2503 .root()
2504 .preorder()
2505 .filter(|node| node.kind_id() == keyword)
2506 .inspect(|node| {
2507 assert_eq!(
2508 node.parent().map(|parent| parent.kind_id()),
2509 Some(predefined_type),
2510 "the `string` type keyword surfaced outside \
2511 `predefined_type` in {path}; the operator is carried \
2512 by the wrapper, so `get_op_type` needs a parent guard \
2513 before that arm can be trusted (#1261)",
2514 );
2515 })
2516 .count()
2517 }
2518
2519 // Kind ids re-read from the generated enums, not carried over: TS
2520 // `String2` = 135, TSX `String3` = 141 (TSX's `String2` = 261 is the
2521 // string-literal production and stays an operand).
2522 assert_eq!(
2523 keyword_occurrences::<TypescriptParser>(
2524 "foo.ts",
2525 Typescript::String2 as u16,
2526 Typescript::PredefinedType as u16,
2527 ),
2528 EXPECTED_OCCURRENCES,
2529 "TypeScript no longer emits the `string` type keyword in every \
2530 position #1261 probed",
2531 );
2532 assert_eq!(
2533 keyword_occurrences::<TsxParser>(
2534 "foo.tsx",
2535 Tsx::String3 as u16,
2536 Tsx::PredefinedType as u16,
2537 ),
2538 EXPECTED_OCCURRENCES,
2539 "TSX no longer emits the `string` type keyword in every position \
2540 #1261 probed",
2541 );
2542 }
2543
2544 // Issue #453: a `void` return type must contribute exactly one
2545 // Halstead operator. The TS / TSX grammars parse `: void` as a
2546 // `predefined_type` wrapper around an inner `void` token. `is_primitive`
2547 // routes the wrapper into the text-keyed `primitive_operators` map as
2548 // `"void"`, while the inner `Void` token is independently a standalone
2549 // expression operator (`void 0`). Pre-fix both classified as operators
2550 // and one source `void` counted as TWO distinct Halstead operators.
2551 // The fix suppresses the wrapper when its child is a `Void` token, so
2552 // only the inner token carries the operator — matching expression
2553 // `void 0` and keeping the kind_id-keyed count consistent.
2554 //
2555 // For `function f(): void { return; }`:
2556 //
2557 // * Operators (n1 = 7, N1 = 7): `function`, `()`, `{}`, `:`, `return`,
2558 // `;`, and a single `void`. (The untyped form is n1 = 5; the `: void`
2559 // annotation adds the `:` operator and one `void`, NOT two — the
2560 // issue's "n1 = 6" target overlooked the annotation colon.)
2561 //
2562 // Verified by test-via-revert: removing the `predefined_void` guard
2563 // restores the pre-fix `u_operators` 7 -> 8 with a duplicate `"void"`
2564 // (one kind_id-keyed, one in `primitive_operators`). Both `metrics()`
2565 // and the `ops`-list dedup invariant (`ts_void_return_and_expression_*`
2566 // in `ops.rs`) are pinned per lesson 4.
2567 #[cfg(feature = "typescript")]
2568 #[test]
2569 fn ts_void_return_type_single_operator_453() {
2570 const SRC: &str = "function f(): void { return; }";
2571 let check = |m: crate::CodeMetrics| {
2572 assert_eq!(m.halstead.unique_operators(), 7);
2573 assert_eq!(m.halstead.total_operators(), 7);
2574 };
2575
2576 check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2577 check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2578 }
2579
2580 // Issue #453 over-suppression guard: expression `void 0` (a
2581 // `unary_expression`, NOT a `predefined_type` wrapper) must still
2582 // count `void` as exactly one operator. The fix keys only on a
2583 // `predefined_type` whose child is a `Void` token, so the bare
2584 // expression operator is untouched.
2585 //
2586 // For `const x = void 0;`:
2587 //
2588 // * Operators (n1 = 4, N1 = 4): `const`, `=`, `void`, `;`.
2589 // * Operands (n2 = 2, N2 = 2): `x`, `0`.
2590 #[cfg(feature = "typescript")]
2591 #[test]
2592 fn ts_void_expression_still_single_operator_453() {
2593 const SRC: &str = "const x = void 0;";
2594 let check = |m: crate::CodeMetrics| {
2595 assert_eq!(m.halstead.unique_operators(), 4);
2596 assert_eq!(m.halstead.total_operators(), 4);
2597 assert_eq!(m.halstead.unique_operands(), 2);
2598 assert_eq!(m.halstead.total_operands(), 2);
2599 };
2600
2601 check_metrics::<TypescriptParser>(SRC, "foo.ts", check);
2602 check_metrics::<TsxParser>(SRC, "foo.tsx", check);
2603 }
2604
2605 #[cfg(feature = "python")]
2606 #[test]
2607 fn python_wrong_operators() {
2608 // Not valid Python — the parser recovers `()` as a tuple, the `[`
2609 // as a bare token and `{}` as a dict — but each opener is still a
2610 // delimiter the source spells, so since #1486 the three are
2611 // billed once each as `()`, `[]` and `{}`. The closers never are.
2612 check_metrics::<PythonParser>("()[]{}", "foo.py", |metric| {
2613 assert_eq!(metric.halstead.unique_operators(), 3);
2614 assert_eq!(metric.halstead.total_operators(), 3);
2615 insta::assert_json_snapshot!(
2616 metric.halstead,
2617 @r#"
2618 {
2619 "unique_operators": 3,
2620 "total_operators": 3,
2621 "unique_operands": 0,
2622 "total_operands": 0,
2623 "length": 3,
2624 "estimated_program_length": 4.754887502163468,
2625 "purity_ratio": 1.584962500721156,
2626 "vocabulary": 3,
2627 "volume": 4.754887502163468,
2628 "difficulty": 0.0,
2629 "level": 0.0,
2630 "effort": 0.0,
2631 "time": 0.0,
2632 "bugs": 0.0
2633 }
2634 "#
2635 );
2636 });
2637 }
2638
2639 #[cfg(feature = "python")]
2640 #[test]
2641 fn python_check_metrics() {
2642 check_metrics::<PythonParser>(
2643 "def f():
2644 pass",
2645 "foo.py",
2646 |metric| {
2647 // expected operators: def, (), :, pass — one each (#1486
2648 // added the `()` and the block `:`).
2649 assert_eq!(metric.halstead.unique_operators(), 4);
2650 assert_eq!(metric.halstead.total_operators(), 4);
2651 insta::assert_json_snapshot!(
2652 metric.halstead,
2653 @r#"
2654 {
2655 "unique_operators": 4,
2656 "total_operators": 4,
2657 "unique_operands": 1,
2658 "total_operands": 1,
2659 "length": 5,
2660 "estimated_program_length": 8.0,
2661 "purity_ratio": 1.6,
2662 "vocabulary": 5,
2663 "volume": 11.60964047443681,
2664 "difficulty": 2.0,
2665 "level": 0.5,
2666 "effort": 23.21928094887362,
2667 "time": 1.289960052715201,
2668 "bugs": 0.002712967490108627
2669 }
2670 "#
2671 );
2672 },
2673 );
2674 }
2675
2676 #[cfg(feature = "java")]
2677 #[test]
2678 fn java_operators_and_operands() {
2679 check_metrics::<JavaParser>(
2680 "public class Main {
2681 public static void main(string args[]) {
2682 int a, b, c, avg;
2683 a = 5; b = 5; c = 5;
2684 avg = (a + b + c) / 3;
2685 MessageFormat.format(\"{0}\", avg);
2686 }
2687 }",
2688 "foo.java",
2689 |metric| {
2690 // Operators (n1=14): public static class {} void () [] , . ; int = + /
2691 // (`public` x2 and `static` since #1565)
2692 // Operands (n2=13): Main main string args a b c avg 5 3 MessageFormat format "{0}"
2693 // (`string` is the parameter's type name, an operand since #1560)
2694 assert_eq!(metric.halstead.unique_operators(), 14);
2695 assert_eq!(metric.halstead.total_operators(), 30);
2696 insta::assert_json_snapshot!(
2697 metric.halstead,
2698 @r#"
2699 {
2700 "unique_operators": 14,
2701 "total_operators": 30,
2702 "unique_operands": 13,
2703 "total_operands": 23,
2704 "length": 53,
2705 "estimated_program_length": 101.40868524464065,
2706 "purity_ratio": 1.913371419710201,
2707 "vocabulary": 27,
2708 "volume": 252.0090376146638,
2709 "difficulty": 12.384615384615385,
2710 "level": 0.08074534161490683,
2711 "effort": 3121.035004304683,
2712 "time": 173.3908335724824,
2713 "bugs": 0.07118871793971378
2714 }
2715 "#
2716 );
2717 },
2718 );
2719 }
2720
2721 #[cfg(feature = "java")]
2722 #[test]
2723 fn java_primitive_types_and_booleans() {
2724 check_metrics::<JavaParser>(
2725 "public class Prims {
2726 byte a = 1;
2727 short b = 2;
2728 int c = 3;
2729 long d = 4;
2730 char e = 'x';
2731 float f = 1.0f;
2732 double g = 2.0;
2733 boolean h = true;
2734 boolean i = false;
2735 }",
2736 "foo.java",
2737 |metric| {
2738 // Verifies all 8 Java primitive-type keywords (byte, short, int, long,
2739 // char, float, double, boolean) are counted as distinct operators, and
2740 // that true/false are counted as operands. The other five operators
2741 // are `public` (#1565), `class` (#1552), `{}`, `=` and `;`.
2742 assert_eq!(metric.halstead.unique_operators(), 13);
2743 insta::assert_json_snapshot!(
2744 metric.halstead,
2745 @r#"
2746 {
2747 "unique_operators": 13,
2748 "total_operators": 30,
2749 "unique_operands": 19,
2750 "total_operands": 19,
2751 "length": 49,
2752 "estimated_program_length": 128.8163390912623,
2753 "purity_ratio": 2.6289048794135166,
2754 "vocabulary": 32,
2755 "volume": 245.0,
2756 "difficulty": 6.5,
2757 "level": 0.15384615384615385,
2758 "effort": 1592.5,
2759 "time": 88.47222222222223,
2760 "bugs": 0.04545674902336332
2761 }
2762 "#
2763 );
2764 },
2765 );
2766 }
2767
2768 // #1380: `this` and `super` were swept into the operator arm under a
2769 // `// Operator: … keywords` heading — classification by lexical
2770 // class rather than by Halstead role. A member access is
2771 // `<receiver> <op> <field>`, so billing the receiver as an operator
2772 // scored `this.x` as a binary operator with one operand while `p.x`
2773 // is one operator and two operands. Both are operands now, matching
2774 // the eleven other languages that classify a self-reference.
2775 #[cfg(feature = "java")]
2776 #[test]
2777 fn java_self_and_super_references_are_operands() {
2778 let source = "class T {\n int f() { return this.x + super.y; }\n}";
2779 // Operators (n1=8, N1=10): class (#1552), {} x2, int, (),
2780 // return, . x2, +, ;
2781 // Operands (n2=6, N2=6): T, f, this, x, super, y
2782 // Before the fix this read (10, 12, 4, 4) — `this` and `super`
2783 // billed into the operator side of both counts.
2784 assert_halstead_counts::<JavaParser>(
2785 source,
2786 "foo.java",
2787 [8, 10, 6, 6],
2788 "java self/super references",
2789 );
2790 assert_keywords_are_operands_only::<JavaParser>(source, "foo.java", &["this", "super"]);
2791 }
2792
2793 // The `super` of a wildcard type bound denotes no value, so it keeps
2794 // the operator classification `? extends T`'s `extends` already has
2795 // — the two keywords are siblings under the same `wildcard` node and
2796 // nothing but the parent separates them from a real self-reference
2797 // (#1380). The explicit receiver parameter in the same fixture is
2798 // the independent path through the operand arm (grammar-dispatch
2799 // section 11): only it can put `this` in the operand vocabulary
2800 // here, and only the wildcard can put `super` in the operator one.
2801 #[cfg(feature = "java")]
2802 #[test]
2803 fn java_wildcard_super_bound_stays_an_operator() {
2804 let source = "import java.util.List;\n\
2805 class T {\n \
2806 void m(T T.this, List<? super String> a, List<? extends String> b) { }\n\
2807 }";
2808 let ops = ops_of::<JavaParser>(source, "foo.java");
2809
2810 assert!(
2811 ops.operators.iter().any(|o| o == "super"),
2812 "`? super String` must keep `super` an operator; operators were {:?}",
2813 ops.operators
2814 );
2815 assert!(
2816 !ops.operands.iter().any(|o| o == "super"),
2817 "a wildcard bound must not bill `super` as an operand; operands were {:?}",
2818 ops.operands
2819 );
2820 assert!(
2821 ops.operators.iter().any(|o| o == "extends"),
2822 "the fixture must still contain the `? extends String` \
2823 bound this arm mirrors; operators were {:?}",
2824 ops.operators
2825 );
2826 assert_keywords_are_operands_only::<JavaParser>(source, "foo.java", &["this"]);
2827 }
2828
2829 /// One `this` and one `super` per container the pinned
2830 /// `tree-sitter-java` can put them in: a delegating constructor
2831 /// call, an explicit superclass constructor call, a field access, a
2832 /// method-invocation receiver, a method reference, a call argument,
2833 /// and the two qualified forms an inner class allows.
2834 #[cfg(feature = "java")]
2835 const JAVA_SELF_POSITIONS: &str = "class Pos extends P {
2836 int x;
2837 Pos() { this(1); }
2838 Pos(int v) { super(v); this.x = v; }
2839 int a() { return this.x; }
2840 int b() { return super.h(); }
2841 java.util.function.IntSupplier c() { return this::a; }
2842 void d() { g(this); }
2843 void g(Object o) { }
2844 class In {
2845 int e() { return Pos.this.x; }
2846 int f() { return Pos.super.h(); }
2847 }
2848 }";
2849
2850 #[cfg(feature = "java")]
2851 #[test]
2852 fn java_self_and_super_leaves_are_unaliased_and_unconditional() {
2853 assert_self_reference_leaves::<JavaCode>(
2854 JAVA_SELF_POSITIONS,
2855 &[
2856 ("this", Java::This as u16, 6),
2857 ("super", Java::Super as u16, 3),
2858 ],
2859 "java",
2860 );
2861 }
2862
2863 // Groovy's `Super` arm, pinned from all three sides. The pinned
2864 // grammar emits the `super` token only as a `wildcard` bound, where it
2865 // is an operator exactly as in Java above; every super-*reference* is
2866 // a plain `identifier`, and so an operand.
2867 //
2868 // Since #1419 the arm carries Java's `Wildcard` parent gate, so the
2869 // bound half below exercises the gate's operator branch — inverting
2870 // the gate fails it. The gate's operand branch is unreachable under
2871 // the pin (only error recovery on invalid Groovy, `List<? super>`,
2872 // detaches a `super` token from its `wildcard`), so it is
2873 // deliberately untested rather than pinned against an invalid
2874 // fixture. The node census is the drift marker for it: a grammar bump
2875 // that routes a reference to `Groovy::Super` fails here by name.
2876 #[cfg(feature = "groovy")]
2877 #[test]
2878 fn groovy_wildcard_super_bound_stays_an_operator() {
2879 let bounds =
2880 "class T {\n void m(List<? super String> a, List<? extends String> b) { }\n}";
2881 let ops = ops_of::<GroovyParser>(bounds, "foo.groovy");
2882 assert!(
2883 ops.operators.iter().any(|o| o == "super"),
2884 "`? super String` must keep `super` an operator; operators were {:?}",
2885 ops.operators
2886 );
2887 assert!(
2888 !ops.operands.iter().any(|o| o == "super"),
2889 "a wildcard bound must not bill `super` as an operand; operands were {:?}",
2890 ops.operands
2891 );
2892 assert!(
2893 ops.operators.iter().any(|o| o == "extends"),
2894 "the fixture must still contain the `? extends String` bound this arm \
2895 mirrors; operators were {:?}",
2896 ops.operators
2897 );
2898
2899 let mixed = "class T extends B {\n void m(List<? super String> a) { super.h() }\n}";
2900 let mut tokens = 0;
2901 for_each_node_with_chain::<GroovyCode>(mixed.as_bytes(), |node, chain| {
2902 if node.kind_id() == Groovy::Super as u16 {
2903 tokens += 1;
2904 assert_eq!(
2905 chain.last().map(Node::kind_id),
2906 Some(Groovy::Wildcard as u16),
2907 "groovy: a `super` token outside a wildcard bound now reaches \
2908 the gate's operand branch, which the pinned grammar made \
2909 unreachable — re-check the arm against the new grammar (#1419)"
2910 );
2911 }
2912 });
2913 assert_eq!(
2914 tokens, 1,
2915 "the fixture's one wildcard bound must still spell `super`, and its \
2916 `super.h()` reference must not"
2917 );
2918
2919 // The reference direction, on valid input the gate must leave
2920 // alone: every super-reference form the grammar accepts stays an
2921 // operand, so splitting `Super` out of the operator alternation
2922 // cannot have moved one. `this` rides along for the same reason
2923 // Java's test carries it.
2924 let refs = "class A extends B {\n A() { super(1) }\n \
2925 def g() { return super.h() }\n \
2926 def i() { return A.super.h() }\n \
2927 def j() { return super::h }\n \
2928 def k() { return super?.h() }\n \
2929 def l() { return this.x }\n}";
2930 assert_keywords_are_operands_only::<GroovyParser>(refs, "foo.groovy", &["super", "this"]);
2931 }
2932
2933 #[cfg(feature = "groovy")]
2934 #[test]
2935 fn groovy_operators_and_operands() {
2936 check_metrics::<GroovyParser>(
2937 "class Main {
2938 static void main(String[] args) {
2939 int a, b, c, avg;
2940 a = 5; b = 5; c = 5;
2941 avg = (a + b + c) / 3;
2942 println(avg);
2943 }
2944 }",
2945 "foo.groovy",
2946 |metric| {
2947 // Groovy mirror of `java_operators_and_operands`. The juxt
2948 // call `println avg` exercises `juxt_function_call` in
2949 // place of Java's `MessageFormat.format(...)`. amaanq's
2950 // grammar inherits Java's tokenisation, so n1/N1/n2/N2
2951 // shapes match Java up to those substitutions.
2952 // The dekobon grammar parses the primitive type names
2953 // `void` and `int` as `type_identifier`, the kind of
2954 // `String`; since #1568 their spelling bills them as
2955 // operators, as Java's keyword tokens are, while
2956 // `String` stays an operand.
2957 // `class` is an operator since #1552, `static` since
2958 // #1565, and `void` and `int` since #1568.
2959 assert_eq!(metric.halstead.unique_operators(), 12);
2960 assert_eq!(metric.halstead.unique_operands(), 11);
2961 // `bugs` is `effort.powf(2/3) / 3000`, and the platform libm's
2962 // `powf` differs in the last ulp: macOS rendered this value
2963 // as `…92342184` where Linux and Windows gave `…923421845`.
2964 // Round it so the snapshot pins the metric, not the libm.
2965 insta::assert_json_snapshot!(
2966 metric.halstead,
2967 { ".bugs" => insta::rounded_redaction(12) },
2968 @r#"
2969 {
2970 "unique_operators": 12,
2971 "total_operators": 26,
2972 "unique_operands": 11,
2973 "total_operands": 21,
2974 "length": 47,
2975 "estimated_program_length": 81.07329781366414,
2976 "purity_ratio": 1.7249637832694498,
2977 "vocabulary": 23,
2978 "volume": 212.60741193467962,
2979 "difficulty": 11.454545454545455,
2980 "level": 0.0873015873015873,
2981 "effort": 2435.3212639790577,
2982 "time": 135.29562577661432,
2983 "bugs": 0.06033688972
2984 }
2985 "#
2986 );
2987 },
2988 );
2989 }
2990
2991 #[cfg(feature = "groovy")]
2992 #[test]
2993 fn groovy_primitive_types_and_booleans() {
2994 check_metrics::<GroovyParser>(
2995 "class Prims {
2996 byte a = 1
2997 short b = 2
2998 int c = 3
2999 long d = 4
3000 char e = 'x'
3001 float f = 1.0f
3002 double g = 2.0
3003 boolean h = true
3004 boolean i = false
3005 }",
3006 "foo.groovy",
3007 |metric| {
3008 // The dekobon grammar consolidates the 8 primitive
3009 // type names (`byte`, `short`, `int`, `long`, `char`,
3010 // `float`, `double`, `boolean`) under `type_identifier`;
3011 // since #1568 each bills as a distinct operator keyed by
3012 // its text, as Java's `java_primitive_ops` twin does.
3013 // Numeric literals collapse to one `NumberLiteral` shape
3014 // (no Hex/Octal/Binary/Decimal split), and `'x'` parses
3015 // as `StringLiteral` (Groovy single-quoted strings)
3016 // rather than as `CharacterLiteral`.
3017 // expected: operators `=` x9, `class`, `{}` and the eight
3018 // primitives (`boolean` twice) — n1 11, N1 20; operands
3019 // `Prims`, the nine names and nine values — n2 19, N2 19.
3020 assert_eq!(metric.halstead.unique_operators(), 11);
3021 assert_eq!(metric.halstead.total_operators(), 20);
3022 assert_eq!(metric.halstead.unique_operands(), 19);
3023 assert_eq!(metric.halstead.total_operands(), 19);
3024 insta::assert_json_snapshot!(
3025 metric.halstead,
3026 @r#"
3027 {
3028 "unique_operators": 11,
3029 "total_operators": 20,
3030 "unique_operands": 19,
3031 "total_operands": 19,
3032 "length": 39,
3033 "estimated_program_length": 118.76437056043838,
3034 "purity_ratio": 3.045240270780471,
3035 "vocabulary": 30,
3036 "volume": 191.36873322873222,
3037 "difficulty": 5.5,
3038 "level": 0.18181818181818182,
3039 "effort": 1052.5280327580272,
3040 "time": 58.47377959766818,
3041 "bugs": 0.0344906352248725
3042 }
3043 "#
3044 );
3045 },
3046 );
3047 }
3048
3049 // Issue #1263 swept Groovy alongside the JS family and C#: its
3050 // operand arm listed `QualifiedName` (a `package` / `import` path)
3051 // and `QualifiedType` on top of the identifier leaves the walker
3052 // already reached.
3053 //
3054 // Only the `QualifiedName` half was observable. The runtime emits
3055 // `qualified_type` as the *alias* `QualifiedType2` (kind_id 228),
3056 // which the arm never named — a lesson-2 miss that, by accident,
3057 // made that half already leaves-only and is why #1263's issue body
3058 // recorded Groovy as compliant. Both kinds are gone rather than
3059 // completed.
3060 //
3061 // expected, for `package com.example`: operators `.` (1/1);
3062 // operands `com`, `example` (2/2). Pre-fix the `qualified_name`
3063 // added `com.example`, making the operand counts 3/3.
3064 #[cfg(feature = "groovy")]
3065 #[test]
3066 fn groovy_qualified_name_counts_leaves_not_the_composite_1263() {
3067 check_metrics::<GroovyParser>("package com.example", "foo.groovy", |metric| {
3068 assert_eq!(metric.halstead.unique_operators(), 1);
3069 assert_eq!(metric.halstead.total_operators(), 1);
3070 assert_eq!(metric.halstead.unique_operands(), 2);
3071 assert_eq!(metric.halstead.total_operands(), 2);
3072 });
3073 }
3074
3075 // The type half of the same arm (#1352). #1263 dropped
3076 // `QualifiedType` alongside `QualifiedName`, but only the latter
3077 // was pinned: adding `QualifiedType2` back to the operand arm
3078 // failed none of the 3,523 tests then in the lib targets, so the
3079 // leaves-only reading of a qualified *type* was correct by accident
3080 // rather than by contract. The tempting "fix" for the alias miss
3081 // #1263 recorded is to complete the list with 228, which is exactly
3082 // the double count #1263 removed — this test is what stops that.
3083 //
3084 // The kind assertions are the grammar-dispatch section 1 / 2 drift
3085 // marker: if a grammar bump renumbers the alias, the operand
3086 // assertions below would keep passing while measuring a construct
3087 // this arm no longer describes.
3088 //
3089 // expected, for `java.util.List x = null`: operators `.` × 2 and
3090 // `=` → n1 = 2, N1 = 3; operands `java`, `util`, `List`, `x`,
3091 // `null` → n2 = 5, N2 = 5. Listing the wrapper would add the whole
3092 // span `java.util.List`, making the operand counts 6/6.
3093 #[cfg(feature = "groovy")]
3094 #[test]
3095 fn groovy_qualified_type_counts_leaves_not_the_composite_1352() {
3096 const SOURCE: &str = "java.util.List x = null";
3097
3098 let parser = GroovyParser::new(
3099 SOURCE.as_bytes().to_vec(),
3100 &PathBuf::from("foo.groovy"),
3101 None,
3102 );
3103 assert!(
3104 ast_has_kind_id(&parser, Groovy::QualifiedType2 as u16),
3105 "the dekobon grammar no longer emits `qualified_type` as the \
3106 alias `QualifiedType2`; re-derive the Groovy operand arm \
3107 before trusting the counts below",
3108 );
3109 assert!(
3110 !ast_has_kind_id(&parser, Groovy::QualifiedType as u16),
3111 "the unsuffixed `QualifiedType` is now reachable; it is a \
3112 second wrapper this arm must keep excluded",
3113 );
3114
3115 check_metrics::<GroovyParser>(SOURCE, "foo.groovy", |metric| {
3116 assert_eq!(metric.halstead.unique_operators(), 2);
3117 assert_eq!(metric.halstead.total_operators(), 3);
3118 assert_eq!(metric.halstead.unique_operands(), 5);
3119 assert_eq!(metric.halstead.total_operands(), 5);
3120 });
3121 }
3122
3123 #[cfg(feature = "groovy")]
3124 #[test]
3125 fn groovy_closure_operators_and_operands() {
3126 // `twice`, not the original `double`: Groovy reserves the
3127 // primitive type names, so `def double` is not valid Groovy and
3128 // `double` would bill as the primitive operator (#1568).
3129 check_metrics::<GroovyParser>("def twice = { x -> x * 2 }", "foo.groovy", |metric| {
3130 // Closure with arrow-style parameter list.
3131 // Distinct operators: def, =, {}, ->, * = 5.
3132 // Distinct operands: twice, x, 2 = 3.
3133 assert_eq!(metric.halstead.unique_operators(), 5);
3134 assert_eq!(metric.halstead.unique_operands(), 3);
3135 });
3136 }
3137
3138 /// Regression for issue #247: every Groovy-specific operator the
3139 /// prior amaanq grammar dropped to ERROR or mis-shaped as a Java
3140 /// node now parses as a distinct lexer token in the dekobon
3141 /// grammar, so Halstead counts each one. The fixture below
3142 /// exercises Elvis `?:`, safe-nav `?.`, safe-chain `??.`,
3143 /// spread-dot `*.`, method-pointer `.&`, direct-field `.@`,
3144 /// identity `===` / `!==`, spaceship `<=>`, regex `=~` / `==~`,
3145 /// exclusive ranges `..<` / `<..` / `<..<`, `as` coercion, and
3146 /// `?[` safe index — every distinct operator kind must appear in
3147 /// `u_operators` (the count grows by exactly the number of new
3148 /// distinct operator tokens introduced).
3149 #[cfg(feature = "groovy")]
3150 #[test]
3151 fn groovy_dekobon_operator_coverage_247() {
3152 check_metrics::<GroovyParser>(
3153 "def f(a, b, list, s) {
3154 def x = a ?: b
3155 def y = a?.field
3156 def z = a??.field
3157 def items = list*.size()
3158 def ptr = a.&size
3159 def fld = a.@field
3160 def id1 = a === b
3161 def id2 = a !== b
3162 def ship = a <=> b
3163 def find = s =~ /pat/
3164 def match = s ==~ /^pat\\$/
3165 def r1 = 0..<10
3166 def r2 = 0<..10
3167 def r3 = 0<..<10
3168 def cast = a as String
3169 def safe = list?[0]
3170 return x
3171 }",
3172 "foo.groovy",
3173 |metric| {
3174 // Exact pin: with the dekobon Groovy grammar this
3175 // fixture exercises 16 Groovy-specific tokens (`?:`,
3176 // `?.`, `??.`, `*.`, `.&`, `.@`, `===`, `!==`, `<=>`,
3177 // `=~`, `==~`, `..<`, `<..`, `<..<`, `as`, `?[`) plus
3178 // 6 ambient Java-shaped operators the fixture also
3179 // uses (`def`, `=`, `,`, `{}`, `()`, `return`), for a
3180 // total of 22 distinct operator kinds. A regression
3181 // that drops any one of the 16 #247 operators would
3182 // push the count below 22 and fail this assertion. The
3183 // complementary AST walk below pins each #247
3184 // operator's identity individually so a grammar change
3185 // that adds an unrelated operator (lifting
3186 // `u_operators` to 23) still flags the loss of a #247
3187 // operator at the per-token level.
3188 //
3189 // Was 23 until #1314. The extra entry was a `/` — the
3190 // fixture's two slashy literals (`/pat/`, `/^pat\$/`)
3191 // each spelled their closing delimiter with the
3192 // division kind, and the arm now guards them. The
3193 // enumeration above was wrong in two ways at that
3194 // count: it listed an ambient `[`, which this fixture
3195 // never emits (`list?[0]` is the single `?[` token),
3196 // and omitted the fabricated `/` that made up the
3197 // difference. Both are corrected here.
3198 assert_eq!(
3199 metric.halstead.unique_operators(),
3200 22,
3201 "u_operators changed; check whether a #247 operator was dropped or an unrelated operator added (and update the comment / token list above accordingly)",
3202 );
3203 },
3204 );
3205 }
3206
3207 #[cfg(feature = "groovy")]
3208 #[test]
3209 fn groovy_gstring_no_double_count() {
3210 // Issue #454: before the fix Groovy had no interpolation guard
3211 // at all — `StringLiteral` was classified as a plain operand, so
3212 // a GString counted the wrapping literal AND descended into its
3213 // interpolated expression, double-counting the inner identifier
3214 // in N2. The fix routes `StringLiteral` through
3215 // `string_operand_type` with both GString interpolation child
3216 // kinds (`gstring_brace_interpolation` / `gstring_dollar_-
3217 // interpolation`), so the wrapper is Unknown and only the inner
3218 // expression contributes.
3219 //
3220 // `def greet(name) {\n return "Hi ${name}"\n}\n`
3221 // operands by token text: `greet` × 1, `name` × 2 (param +
3222 // inside `${name}`). The wrapping `"Hi ${name}"` is suppressed
3223 // → u_operands = 2 (`greet`, `name`), N2 = 3. Without the fix
3224 // the wrapping literal would also count → u_operands = 3,
3225 // N2 = 4.
3226 let src = "def greet(name) {\n return \"Hi ${name}\"\n}\n";
3227 check_metrics::<GroovyParser>(src, "foo.groovy", |metric| {
3228 assert_eq!(metric.halstead.unique_operands(), 2);
3229 assert_eq!(metric.halstead.total_operands(), 3);
3230 });
3231 assert_ops_operands::<GroovyParser>(src, "foo.groovy", 2, vec!["greet", "name"]);
3232 }
3233
3234 #[cfg(feature = "groovy")]
3235 #[test]
3236 fn groovy_gstring_dollar_form_no_double_count() {
3237 // Issue #454: the short `$name` GString form emits a distinct
3238 // `gstring_dollar_interpolation` child whose inner `identifier`
3239 // text is `$name` (the grammar's identifier node spans the
3240 // leading `$`). The wrapper is suppressed; the inner `$name`
3241 // operand is distinct from the bare `name` param.
3242 //
3243 // `def greet(name) {\n return "Hi $name"\n}\n`
3244 // operands: `greet`, `name` (param), `$name` (interp) →
3245 // u_operands = 3, N2 = 3. Without the fix the wrapping
3246 // `"Hi $name"` would also count → u_operands = 4, N2 = 4.
3247 let src = "def greet(name) {\n return \"Hi $name\"\n}\n";
3248 check_metrics::<GroovyParser>(src, "foo.groovy", |metric| {
3249 assert_eq!(metric.halstead.unique_operands(), 3);
3250 assert_eq!(metric.halstead.total_operands(), 3);
3251 });
3252 assert_ops_operands::<GroovyParser>(src, "foo.groovy", 3, vec!["greet", "name", "$name"]);
3253 }
3254
3255 // `Groovy::Identifier2` (141) is the grammar's external token
3256 // `_gstring_dollar_path` — the `$a.b` path of a dollar interpolation —
3257 // whose public symbol is `identifier`, so the parser reports it as
3258 // the unsuffixed `Identifier` and never as 141 (#1475). That is why
3259 // the operand arm lists `Identifier` but not `Identifier2`; if a
3260 // grammar bump starts emitting 141, the dotted path drops out of N2
3261 // with no error.
3262 //
3263 // expected: operands `greet`, `name` (param), `$name.b` (the whole
3264 // path, one node) → n2 = 3, N2 = 3.
3265 #[cfg(feature = "groovy")]
3266 #[test]
3267 fn groovy_gstring_dollar_path_is_the_public_identifier() {
3268 let src = "def greet(name) {\n return \"Hi $name.b\"\n}\n";
3269 let parser = GroovyParser::new(src.as_bytes().to_vec(), &PathBuf::from("foo.groovy"), None);
3270 assert!(
3271 ast_has_kind_id(&parser, Groovy::GstringDollarInterpolation as u16),
3272 "the fixture no longer spells a dollar interpolation",
3273 );
3274 assert!(
3275 !ast_has_kind_id(&parser, Groovy::Identifier2 as u16),
3276 "the hidden `_gstring_dollar_path` token is now emitted as \
3277 `Identifier2`; add it to the Groovy operand arm",
3278 );
3279 check_metrics::<GroovyParser>(src, "foo.groovy", |metric| {
3280 assert_eq!(metric.halstead.unique_operands(), 3);
3281 assert_eq!(metric.halstead.total_operands(), 3);
3282 });
3283 assert_ops_operands::<GroovyParser>(src, "foo.groovy", 3, vec!["greet", "name", "$name.b"]);
3284 }
3285
3286 #[cfg(feature = "groovy")]
3287 #[test]
3288 fn groovy_plain_string_still_operand() {
3289 // Counterpart to `groovy_gstring_no_double_count`: a plain
3290 // non-interpolated literal has neither GString interpolation
3291 // child and must still contribute exactly one operand.
3292 //
3293 // `def f() {\n return "plain"\n}\n`
3294 // operands: `f`, `"plain"` → u_operands = 2, N2 = 2.
3295 let src = "def f() {\n return \"plain\"\n}\n";
3296 check_metrics::<GroovyParser>(src, "foo.groovy", |metric| {
3297 assert_eq!(metric.halstead.unique_operands(), 2);
3298 assert_eq!(metric.halstead.total_operands(), 2);
3299 });
3300 assert_ops_operands::<GroovyParser>(src, "foo.groovy", 2, vec!["f", "\"plain\""]);
3301 }
3302
3303 #[cfg(feature = "groovy")]
3304 #[test]
3305 fn groovy_slashy_string_delimiter_is_not_an_operator() {
3306 // Regression: issue #1314, the Groovy sibling of Elixir #1256
3307 // and Ruby/Perl #1312. A slashy string is a `StringLiteral`
3308 // whose closing delimiter is a `SLASH` — the kind id real
3309 // division uses — so `def b = /xyz/` reported a `/` operator
3310 // with no division in the source. Only the closer is a child
3311 // (the grammar folds the opening `/` into the literal's span),
3312 // so this fabricated one `/` per literal rather than Ruby's two.
3313 //
3314 // expected: operators `def` × 3, `=` × 3 → n1 = 2, N1 = 6.
3315 // Operands `b`, `/xyz/` × 2, `c`, `s` → n2 = 4, N2 = 6. Before
3316 // the guard the two closers added `/` → n1 = 3, N1 = 8.
3317 check_metrics::<GroovyParser>(
3318 "def b = /xyz/\ndef c = /xyz/\ndef s = b\n",
3319 "foo.groovy",
3320 |metric| {
3321 assert_eq!(metric.halstead.unique_operators(), 2);
3322 assert_eq!(metric.halstead.total_operators(), 6);
3323 assert_eq!(metric.halstead.unique_operands(), 4);
3324 assert_eq!(metric.halstead.total_operands(), 6);
3325 },
3326 );
3327 }
3328
3329 #[cfg(feature = "groovy")]
3330 #[test]
3331 fn groovy_division_survives_the_slashy_guard() {
3332 // Control for #1314: the guard is scoped to a `StringLiteral`
3333 // parent, so real division must still count. Both sides are in
3334 // one fixture — two divisions and one slashy literal — so a
3335 // guard widened to every `SLASH` fails here rather than
3336 // silently passing the test above.
3337 //
3338 // expected: operators `def` × 2, `=` × 2, `/` × 2 → n1 = 3,
3339 // N1 = 6. Operands `q`, `a`, `b`, `c`, `r`, `/x/` → n2 = N2 = 6.
3340 check_metrics::<GroovyParser>("def q = a / b / c\ndef r = /x/\n", "foo.groovy", |metric| {
3341 assert_eq!(metric.halstead.unique_operators(), 3);
3342 assert_eq!(metric.halstead.total_operators(), 6);
3343 assert_eq!(metric.halstead.unique_operands(), 6);
3344 assert_eq!(metric.halstead.total_operands(), 6);
3345 });
3346 }
3347
3348 #[cfg(feature = "groovy")]
3349 #[test]
3350 fn groovy_slashy_guard_is_parent_scoped_not_ancestor_scoped() {
3351 // The input that separates the parent-scoped guard from the
3352 // ancestor-scanning mutant of it — the mutant #1256's
3353 // post-mortem says survives every ordinary fixture. Groovy is
3354 // one of only two languages in #1314 where such an input
3355 // exists at all: a slashy string may carry a GString
3356 // interpolation, so `/x${a / b}y/` puts a real division under a
3357 // `StringLiteral` *ancestor* while its parent is the
3358 // `binary_expression`. An ancestor scan swallows it; the parent
3359 // check leaves it alone. (The JS, C++ and Tcl/iRules guards
3360 // have no such input — see
3361 // `js_regex_delimiter_guard_is_parent_scoped_is_unobservable`.)
3362 //
3363 // expected: operators `def` × 2, `=` × 2, `/` (the
3364 // interpolated division) → n1 = 3, N1 = 5. The wrapping literal
3365 // is not an operand — it carries an interpolation, so
3366 // `string_operand_type` yields `Unknown` and the inner
3367 // expression's operands carry the count (#454) — leaving `r`,
3368 // `a`, `b`, `s` → n2 = 4, with `a` twice → N2 = 5. Under the
3369 // ancestor-scoped mutant the division vanishes: n1 = 2, N1 = 4.
3370 check_metrics::<GroovyParser>(
3371 "def r = /x${a / b}y/\ndef s = a\n",
3372 "foo.groovy",
3373 |metric| {
3374 assert_eq!(metric.halstead.unique_operators(), 3);
3375 assert_eq!(metric.halstead.total_operators(), 5);
3376 assert_eq!(metric.halstead.unique_operands(), 4);
3377 assert_eq!(metric.halstead.total_operands(), 5);
3378 },
3379 );
3380 }
3381
3382 #[cfg(feature = "groovy")]
3383 #[test]
3384 fn groovy_every_string_spelling_scores_alike() {
3385 // Companion to the three above (#1314). Groovy has five ways to
3386 // write an inert one-character string, and the choice is
3387 // spelling rather than semantics, so all five must score
3388 // identically. Before the guard the two slashy forms reported
3389 // an extra `/` operator that the other three did not — the
3390 // author's delimiter choice moved n1/N1.
3391 //
3392 // The dollar-slashy and quoted forms are no-change controls:
3393 // `$/…/$` closes with `/$` (kind 144) and `"…"` with `"` (134),
3394 // neither of which the operator arm classifies. `'x'` is a
3395 // childless leaf. The escaped-slash row is the one that would
3396 // regress if the guard were ever narrowed to a literal whose
3397 // *only* child is the closer.
3398 //
3399 // expected per spelling: operators `def` × 3, `=` × 3 → n1 = 2,
3400 // N1 = 6; operands `a`, the literal, `b`, `c` → n2 = 4, with
3401 // `a` used three times → N2 = 6.
3402 for literal in ["/x/", "$/x/$", "'x'", "\"x\"", r"/esc\/aped/"] {
3403 assert_halstead_counts::<GroovyParser>(
3404 &format!("def a = {literal}\ndef b = a\ndef c = a\n"),
3405 "foo.groovy",
3406 [2, 6, 4, 6],
3407 &format!("literal {literal}"),
3408 );
3409 }
3410 }
3411
3412 #[cfg(feature = "csharp")]
3413 #[test]
3414 fn csharp_operators_and_operands() {
3415 // After issue #286, `void`, `string`, and `int` count as three
3416 // distinct Halstead operators rather than collapsing into one
3417 // `PredefinedType` kind_id entry, lifting u_operators from 13
3418 // to 15. Total operators (N1) is unchanged because the same
3419 // nodes are still counted, just keyed by lexeme.
3420 check_metrics::<CsharpParser>(
3421 "public class Main {
3422 public static void Run(string[] args) {
3423 int a, b, c, avg;
3424 a = 5; b = 5; c = 5;
3425 avg = (a + b + c) / 3;
3426 System.Console.WriteLine(\"{0}\", avg);
3427 }
3428 }",
3429 "foo.cs",
3430 |metric| {
3431 assert_eq!(metric.halstead.unique_operators(), 15);
3432 assert_eq!(metric.halstead.total_operators(), 32);
3433 assert_eq!(metric.halstead.unique_operands(), 13);
3434 assert_eq!(metric.halstead.total_operands(), 23);
3435 // Pin every Halstead field; values are whatever the
3436 // classifier produces and become the regression spec.
3437 insta::assert_json_snapshot!(metric.halstead);
3438 },
3439 );
3440 }
3441
3442 // Issue #1263: C#'s three name *containers* — `qualified_name`
3443 // (`System.Text`), `generic_name` (`List<int>`) and
3444 // `alias_qualified_name` (`global::Foo`) — were operands alongside
3445 // every leaf the walker already reached, so one occurrence of each
3446 // billed twice. `member_access_expression` never was, which is why
3447 // `csharp_operators_and_operands`' `System.Console.WriteLine` is
3448 // unaffected by this change: the bug lived in the *name* grammar,
3449 // not in member access.
3450 //
3451 // Three fixtures rather than one, so a regression names which
3452 // container came back. Each is hand-tallied; the removed composite
3453 // is called out per case.
3454 #[cfg(feature = "csharp")]
3455 #[test]
3456 fn csharp_name_containers_count_leaves_not_the_composite_1263() {
3457 // expected: operators `using`, `.`, `;` (3/3); operands
3458 // `System`, `Text` (2/2). Pre-fix the `qualified_name` added
3459 // `System.Text`, making the operand counts 3/3.
3460 check_metrics::<CsharpParser>("using System.Text;", "foo.cs", |metric| {
3461 assert_eq!(metric.halstead.unique_operators(), 3);
3462 assert_eq!(metric.halstead.total_operators(), 3);
3463 assert_eq!(metric.halstead.unique_operands(), 2);
3464 assert_eq!(metric.halstead.total_operands(), 2);
3465 });
3466
3467 // expected: operators `class`, `{`×2, `void`, `(`, `;`, `<>`,
3468 // `int` — 8 total, 7 unique (`int` is the text-keyed primitive
3469 // operator, per #286; `<>` is the type-argument pair, #1559). Operands `C`, `M`, `List`, `l`
3470 // — 4/4. Pre-fix the `generic_name` added `List<int>`, making
3471 // them 5/5.
3472 check_metrics::<CsharpParser>(
3473 "class C { void M() { List<int> l; } }",
3474 "foo.cs",
3475 |metric| {
3476 assert_eq!(metric.halstead.unique_operators(), 7);
3477 assert_eq!(metric.halstead.total_operators(), 8);
3478 assert_eq!(metric.halstead.unique_operands(), 4);
3479 assert_eq!(metric.halstead.total_operands(), 4);
3480 },
3481 );
3482
3483 // expected: operators `class`, `{`×2, `void`, `(`, `;`, `=`,
3484 // `::`, `.` — 9 total, 8 unique. `var` has no operator arm.
3485 // Operands `C`, `M`, `x`, `global`, `Foo`, `Bar` — 6/6. Pre-fix
3486 // the `alias_qualified_name` added `global::Foo`, making them
3487 // 7/7. The `::` staying an operator is what makes the leaf-only
3488 // tally lossless here, so it is asserted by the operator count
3489 // rather than assumed.
3490 check_metrics::<CsharpParser>(
3491 "class C { void M() { var x = global::Foo.Bar; } }",
3492 "foo.cs",
3493 |metric| {
3494 assert_eq!(metric.halstead.unique_operators(), 8);
3495 assert_eq!(metric.halstead.total_operators(), 9);
3496 assert_eq!(metric.halstead.unique_operands(), 6);
3497 assert_eq!(metric.halstead.total_operands(), 6);
3498 },
3499 );
3500 }
3501
3502 #[cfg(feature = "csharp")]
3503 #[test]
3504 fn csharp_primitive_types_and_booleans() {
3505 // After issue #286: each of `byte`, `short`, `int`, `long`,
3506 // `char`, `float`, `double`, `bool`, `object` is now a distinct
3507 // Halstead operator (9 primitives) rather than collapsing into
3508 // one `PredefinedType` kind_id entry. u_operators rises from 6
3509 // to 14 (5 non-primitive operators + 9 distinct primitives);
3510 // total operators (N1) is unchanged because the same nodes are
3511 // still counted, just keyed by lexeme.
3512 //
3513 // N2 dropped 23 → 21 with issue #1253: `true` and `false` each
3514 // reached the walker twice — once as `boolean_literal`, once as
3515 // the keyword leaf under it — so each added one spurious
3516 // occurrence. n2 is unchanged at 21 because operands are keyed
3517 // by source text, so the duplicate collapsed into the existing
3518 // vocabulary entry; that is exactly why the inflation was
3519 // invisible in n2. Every operand here is distinct, so
3520 // N2 == n2 == 21 after the fix.
3521 check_metrics::<CsharpParser>(
3522 "public class Prims {
3523 byte a = 1;
3524 short b = 2;
3525 int c = 3;
3526 long d = 4;
3527 char e = 'x';
3528 float f = 1.0f;
3529 double g = 2.0;
3530 bool h = true;
3531 bool i = false;
3532 object j = null;
3533 }",
3534 "foo.cs",
3535 |metric| {
3536 assert_eq!(metric.halstead.unique_operators(), 14);
3537 assert_eq!(metric.halstead.total_operators(), 33);
3538 assert_eq!(metric.halstead.unique_operands(), 21);
3539 assert_eq!(metric.halstead.total_operands(), 21);
3540 insta::assert_json_snapshot!(metric.halstead);
3541 },
3542 );
3543 }
3544
3545 #[cfg(feature = "csharp")]
3546 #[test]
3547 fn csharp_boolean_literal_counts_once() {
3548 // Regression: issue #1253. `boolean_literal: choice('true',
3549 // 'false')` wraps the keyword leaf, and both kinds sat in the
3550 // operand arm, so every `true` / `false` occurrence added +1 to
3551 // N2. Operands are keyed by source text, so the duplicate
3552 // collapsed into the same vocabulary entry and n2 stayed
3553 // correct — which is why nothing caught it.
3554 //
3555 // Source repeats `true` so N2 exceeds n2 and the assertions can
3556 // tell "counted once per occurrence" from "deduplicated into
3557 // the vocabulary".
3558 //
3559 // Operands by text key: `A`, `M`, `a`, `b`, `c`, `d`, `true` × 2,
3560 // `false`, `null` ⇒ n2 = 9, N2 = 10. Before the fix the keyword
3561 // leaves added one occurrence per boolean ⇒ N2 = 13.
3562 check_metrics::<CsharpParser>(
3563 "class A {\n void M() {\n bool a = true;\n bool b = false;\n bool c = true;\n object d = null;\n }\n}\n",
3564 "foo.cs",
3565 |metric| {
3566 assert_eq!(metric.halstead.unique_operands(), 9);
3567 assert_eq!(metric.halstead.total_operands(), 10);
3568 },
3569 );
3570 }
3571
3572 #[cfg(feature = "csharp")]
3573 #[test]
3574 fn csharp_operator_true_names_an_operator_not_a_value() {
3575 // C#'s overloadable-operator list emits a bare `true` / `false`
3576 // token with no `boolean_literal` wrapper — `operator_declaration`
3577 // is the grammar's only such position. There the keyword names
3578 // the operator being declared, as `+` does in `operator +`, so it
3579 // is an operator (#1296); the companion test above pins the
3580 // wrapped literal, which bills only its wrapper (#1253).
3581 //
3582 // Each declaration names one boolean and returns the other, so
3583 // the fixture carries each keyword once as a name and once as a
3584 // value.
3585 //
3586 // Operators (n1 = 11, N1 = 18): class 1, {} 1, public 2,
3587 // static 2, bool 2, operator 2, true 1, false 1, () 2, => 2,
3588 // ; 2. Operands (n2 = 4, N2 = 7): A × 3 (class name, two
3589 // parameter types), a × 2, and the literals false and true once
3590 // each. Before #1296 the two names were operands instead:
3591 // (9, 16, 4, 9). A blanket exclusion of the leaf would give
3592 // (9, 16, 4, 7), and no guard at all (11, 18, 4, 9).
3593 let source = "class A {\n public static bool operator true(A a) => false;\n public static bool operator false(A a) => true;\n}\n";
3594 assert_halstead_counts::<CsharpParser>(source, "foo.cs", [11, 18, 4, 7], "operator true");
3595
3596 // `true` and `false` are each an operator *and* an operand here,
3597 // once apiece: the name and the literal land in different halves.
3598 let ops = ops_of::<CsharpParser>(source, "foo.cs");
3599 for keyword in ["true", "false"] {
3600 for (half, list) in [("operators", &ops.operators), ("operands", &ops.operands)] {
3601 assert_eq!(
3602 list.iter().filter(|o| *o == keyword).count(),
3603 1,
3604 "`{keyword}` must appear once among the {half}; they were {list:?}"
3605 );
3606 }
3607 }
3608
3609 // The fallback arm: outside both positions — here a bare `true`
3610 // under error recovery, the only other parent the grammar
3611 // produces — the keyword keeps its role as a value.
3612 assert_halstead_counts::<CsharpParser>("true\n", "foo.cs", [0, 0, 1, 1], "bare true");
3613 }
3614
3615 #[cfg(feature = "csharp")]
3616 #[test]
3617 fn csharp_predefined_types_keyed_by_lexeme() {
3618 // Regression: issue #286. The C# grammar emits one `PredefinedType`
3619 // kind_id for every keyword type (`int`, `string`, `bool`, …).
3620 // Without keying by source text the entire family collapses into
3621 // a single Halstead operator (n1 += 1) instead of one per distinct
3622 // keyword. This test pins the post-fix behaviour using four
3623 // distinct primitives — `int`, `string`, `bool`, `object` —
3624 // appearing as parameter types so no other operators interact
3625 // with the count.
3626 //
3627 // expected: operators are `class`, `void`, `M`, `{}`, `()`, `,`
3628 // (×3 between 4 params), plus the four distinct predefined types
3629 // → u_operators = 5 + 4 = 9. Without the fix the four primitives
3630 // collapse to one entry, giving u_operators = 6.
3631 check_metrics::<CsharpParser>(
3632 "class C { void M(int a, string b, bool c, object d) {} }",
3633 "foo.cs",
3634 |metric| {
3635 // The headline assertion: four distinct primitive
3636 // keywords contribute four distinct operators, not one.
3637 assert_eq!(metric.halstead.unique_operators(), 9);
3638 },
3639 );
3640 }
3641
3642 #[cfg(feature = "csharp")]
3643 #[test]
3644 fn csharp_interpolated_string_no_double_count() {
3645 // Regression: issue #183. A C# `$"Hi {name}!"` used to be
3646 // classified as a Halstead operand (the wrapping
3647 // `InterpolatedStringExpression`) AND have its inner
3648 // `Interpolation`'s identifier classified as an operand too.
3649 // The fix routes `InterpolatedStringExpression` through a
3650 // conditional: when it has an `Interpolation` child, the inner
3651 // identifier already carries the operand contribution and the
3652 // wrapper is treated as `Unknown`; when it does not (static
3653 // `$"hello"`), the wrapper still counts as one operand.
3654 //
3655 // expected: operand contributions for
3656 // `class C { void M(string name) { string s = $"Hi {name}!"; } }`
3657 // — `C` (class), `M` (method), `name` (param), `s` (local),
3658 // and the inner `name` (inside `{...}`). With the fix,
3659 // u_operands = 4 (C, M, name, s); N2 = 5 (`name` twice).
3660 // Without the fix, the wrapping `$"Hi {name}!"` would also
3661 // count → u_operands = 5, N2 = 6.
3662 check_metrics::<CsharpParser>(
3663 "class C { void M(string name) { string s = $\"Hi {name}!\"; } }",
3664 "foo.cs",
3665 |metric| {
3666 assert_eq!(metric.halstead.unique_operands(), 4);
3667 assert_eq!(metric.halstead.total_operands(), 5);
3668 },
3669 );
3670 }
3671
3672 #[cfg(feature = "csharp")]
3673 #[test]
3674 fn csharp_static_interpolated_string_is_operand() {
3675 // Regression: issue #183. A `$"..."` with no `{...}` is
3676 // semantically identical to `"..."` and must still contribute
3677 // exactly one operand — the conditional `is_child(Interpolation)`
3678 // check distinguishes it from a true interpolation. expected:
3679 // operands are `C`, `M`, `s`, `$"hello"` → u_operands = 4, N2 = 4.
3680 // A naive "always Unknown" fix would yield u_operands = 3, N2 = 3,
3681 // diverging from the plain-string equivalent below.
3682 check_metrics::<CsharpParser>(
3683 "class C { void M() { string s = $\"hello\"; } }",
3684 "foo.cs",
3685 |metric| {
3686 assert_eq!(metric.halstead.unique_operands(), 4);
3687 assert_eq!(metric.halstead.total_operands(), 4);
3688 },
3689 );
3690 }
3691
3692 #[cfg(feature = "csharp")]
3693 #[test]
3694 fn csharp_plain_string_still_operand() {
3695 // The fix for #183 only changes how `InterpolatedStringExpression`
3696 // is classified; plain `StringLiteral` (and `VerbatimStringLiteral`
3697 // / `RawStringLiteral`) must still contribute exactly one operand
3698 // each. expected: operands are `C`, `M`, `s`, `"hi"` →
3699 // u_operands = 4, N2 = 4.
3700 check_metrics::<CsharpParser>(
3701 "class C { void M() { string s = \"hi\"; } }",
3702 "foo.cs",
3703 |metric| {
3704 assert_eq!(metric.halstead.unique_operands(), 4);
3705 assert_eq!(metric.halstead.total_operands(), 4);
3706 },
3707 );
3708 }
3709
3710 /// #1573: an interpolation's alignment `,` and format `:` delimit the
3711 /// field, as Python's format-spec `:` does (#1486), and bill nothing.
3712 /// The alignment value is an ordinary expression, so `8`, `Width`
3713 /// and `-w` keep their vocabulary. The guard reads the token's
3714 /// parent: the last seven rows put the same `,` / `:` inside and
3715 /// outside a hole in a position that is not a clause, and each keeps
3716 /// billing. Pre-fix, every row with a clause billed one extra `,`
3717 /// and/or `:`.
3718 ///
3719 /// Each row is `(label, source, operators, operands)`, the two
3720 /// vocabularies space-separated. No row repeats a token, so the
3721 /// expected `[n1, N1, n2, N2]` is the two vocabulary sizes, each twice.
3722 #[cfg(feature = "csharp")]
3723 #[test]
3724 fn csharp_interpolation_clause_delimiters_are_not_operators_1573() {
3725 const ROWS: &[(&str, &str, &str, &str)] = &[
3726 ("control", "s = $\"{x}\";", "; =", "s x"),
3727 ("format", "s = $\"{x:F2}\";", "; =", "s x"),
3728 ("alignment", "s = $\"{x,8}\";", "; =", "8 s x"),
3729 ("both", "s = $\"{x,8:F2}\";", "; =", "8 s x"),
3730 // a `const` width is a `constant_expression` the spec allows
3731 ("named width", "s = $\"{x,Width}\";", "; =", "Width s x"),
3732 ("negated width", "s = $\"{x,-w}\";", "- ; =", "s w x"),
3733 // the format string's `,` is text, not a token
3734 ("comma in format", "s = $\"{x:#,##0}\";", "; =", "s x"),
3735 ("raw", "s = $$\"\"\"{{x,8:F2}}\"\"\";", "; =", "8 s x"),
3736 ("verbatim", "s = $@\"{x:F2}\";", "; =", "s x"),
3737 ("call in hole", "s = $\"{f(a, b)}\";", "() , ; =", "a b f s"),
3738 (
3739 "ternary in hole",
3740 "s = $\"{(a ? b : c)}\";",
3741 "() : ; = ?",
3742 "a b c s",
3743 ),
3744 (
3745 "named arg in hole",
3746 "s = $\"{f(x: 1)}\";",
3747 "() : ; =",
3748 "1 f s x",
3749 ),
3750 ("ternary", "s = a ? b : c;", ": ; = ?", "a b c s"),
3751 ("named argument", "f(x: 1);", "() : ;", "1 f x"),
3752 (
3753 "case label",
3754 "switch (x) { case 1: break; }",
3755 "() : ; break case switch {}",
3756 "1 x",
3757 ),
3758 ("argument list", "f(a, b);", "() , ;", "a b f"),
3759 ];
3760 for (label, source, operators, operands) in ROWS {
3761 // `bca ops` sorts its vocabulary, so the observation is
3762 // compared as returned; the rows are written in that order.
3763 let operators: Vec<&str> = operators.split(' ').collect();
3764 let operands: Vec<&str> = operands.split(' ').collect();
3765 let ops = ops_of::<CsharpParser>(source, "foo.cs");
3766 assert_eq!(ops.operators, operators, "{label}: {source:?}");
3767 assert_eq!(ops.operands, operands, "{label}: {source:?}");
3768 let n1 = operators.len() as u64;
3769 let n2 = operands.len() as u64;
3770 assert_halstead_counts::<CsharpParser>(source, "foo.cs", [n1, n1, n2, n2], label);
3771 }
3772 }
3773
3774 // C# half of #1380 — see `java_self_and_super_references_are_operands`
3775 // for the structural argument. `base` moves with `this`: both are
3776 // receivers of a member access.
3777 #[cfg(feature = "csharp")]
3778 #[test]
3779 fn csharp_self_and_base_references_are_operands() {
3780 let source = "class T {\n int F() { return this.x + base.y; }\n}";
3781 // Operators (n1=8, N1=10): class, {} x2, int, (), return, . x2, +, ;
3782 // Operands (n2=6, N2=6): T, F, this, x, base, y
3783 // Before the fix this read (10, 12, 4, 4).
3784 assert_halstead_counts::<CsharpParser>(
3785 source,
3786 "foo.cs",
3787 [8, 10, 6, 6],
3788 "csharp self/base references",
3789 );
3790 assert_keywords_are_operands_only::<CsharpParser>(source, "foo.cs", &["this", "base"]);
3791 }
3792
3793 // An `indexer_declaration` spells the member's *name* with the same
3794 // `this` token kind the receiver uses (both are kind 91), so a
3795 // blanket move would have billed a declarator keyword as a value.
3796 // The declaration keeps the operator classification that the
3797 // `operator` keyword of an overload declaration already has (#1380).
3798 //
3799 // The fixture carries both uses, which is what makes the gate
3800 // observable: `this` has to reach *both* vocabularies from one file.
3801 // Asserting only the operand side would pass with the gate deleted,
3802 // and only the operator side would pass with the whole #1380 change
3803 // reverted (grammar-dispatch section 11).
3804 #[cfg(feature = "csharp")]
3805 #[test]
3806 fn csharp_indexer_declaration_keyword_is_not_a_self_reference() {
3807 let source = "class C {\n int[] _a;\n \
3808 public int this[int i] { get { return this._a[i]; } }\n}";
3809
3810 // Asserted per space, not through the root. The root merges
3811 // every space's vocabulary, and the fixture has one declarator
3812 // `this` and one receiver `this`, so at the root `this` is a
3813 // member of *both* vocabularies whether the gate is right or
3814 // exactly backwards — and every Halstead count is bit-identical
3815 // under the swap, one occurrence on each side either way. The
3816 // space tree is where the two uses stay apart: the declarator
3817 // sits in the class's own vocabulary and the receiver in the
3818 // accessor's.
3819 let mut roles = Vec::new();
3820 collect_keyword_roles(
3821 &ops_of::<CsharpParser>(source, "foo.cs"),
3822 "this",
3823 &mut roles,
3824 );
3825
3826 // `Both` can only arise from two `this` nodes in one space that
3827 // the getter classifies differently — it is the gate's whole
3828 // effect, and it disappears the moment `this` gets one role.
3829 // `OperandOnly` is the accessor, the innermost space and the one
3830 // holding the receiver alone; it is what flips when the gate is
3831 // inverted rather than dropped. Correct gives
3832 // `[Both, Both, OperandOnly]`; inverting gives
3833 // `[Both, Both, OperatorOnly]`; dropping the gate gives three
3834 // `OperandOnly`; reverting #1380 gives three `OperatorOnly`.
3835 // Asserting the whole sequence rather than two `contains` calls:
3836 // the comment above already derives it, and a `contains` pair
3837 // leaves the length unasserted, so a space appearing or vanishing
3838 // goes unnoticed and `[Both, X, OperandOnly]` passes for any `X`.
3839 assert_eq!(
3840 roles,
3841 vec![Role::Both, Role::Both, Role::OperandOnly],
3842 "the declarator and the receiver must disagree in the two outer spaces \
3843 and the accessor must bill `this` as an operand alone",
3844 );
3845 }
3846
3847 /// One `this` and one `base` per container the pinned
3848 /// `tree-sitter-c-sharp` can put them in: a delegating constructor
3849 /// initializer, a base-constructor initializer, a member access, an
3850 /// element access, and a call argument. No `indexer_declaration` —
3851 /// that position is the gated one and is an operator.
3852 #[cfg(feature = "csharp")]
3853 const CSHARP_SELF_POSITIONS: &str = "class Pos : B {
3854 int[] _a;
3855 public Pos() : this(1) { }
3856 public Pos(int x) : base(x) { }
3857 int A() { return this._a[0]; }
3858 int C() { return base.H(); }
3859 int E() { return base[0]; }
3860 int F() { return this[0]; }
3861 void G() { M(this); }
3862 void M(object o) { }
3863 }";
3864
3865 #[cfg(feature = "csharp")]
3866 #[test]
3867 fn csharp_self_and_base_leaves_are_unaliased_and_unconditional() {
3868 assert_self_reference_leaves::<CsharpCode>(
3869 CSHARP_SELF_POSITIONS,
3870 &[
3871 ("this", Csharp::This as u16, 4),
3872 ("base", Csharp::Base as u16, 3),
3873 ],
3874 "csharp",
3875 );
3876 }
3877
3878 /// Every spelling of the six keywords `_parameter_type_with_modifiers`
3879 /// aliases to `modifier`, plus two controls that were billed before
3880 /// #1418: the bare `params` of a parameter array (kind 94, never
3881 /// aliased) and the bare `out` of a call argument (kind 56).
3882 ///
3883 /// `D` is declared *and* called so the argument `out` has a caller
3884 /// to sit in, which is what makes the two spellings of one keyword
3885 /// meet in one file.
3886 #[cfg(feature = "csharp")]
3887 const CSHARP_PARAMETER_MODIFIERS: &str = "static class E {
3888 static void A(this Foo f) { }
3889 static void B(scoped ref Foo f) { }
3890 static void C(ref readonly Foo f) { }
3891 static void D(out int x) { x = 1; }
3892 static void G(in Foo f) { }
3893 static void H(params int[] xs) { }
3894 static void I(ref int y) { D(out y); }
3895}";
3896
3897 /// Each of the five operator keywords in *both* of its kind
3898 /// spellings — the childless `modifier` of a parameter, and the
3899 /// keyword's own token elsewhere: `ref` as a ref local, ref
3900 /// initializer and ref argument (kind 28); `readonly` as the leaf
3901 /// under a field's `modifier` wrapper (48); `in` as the `foreach`
3902 /// separator and an `in` argument (55); `out` as an out argument
3903 /// (56); `scoped` as a scoped ref local (93).
3904 ///
3905 /// Separate from [`CSHARP_PARAMETER_MODIFIERS`] because that fixture
3906 /// pairs the two spellings for `out` alone, which left four of the
3907 /// five bare kinds `CsharpCode::is_primitive` lists with no input
3908 /// (#1418).
3909 #[cfg(feature = "csharp")]
3910 const CSHARP_MODIFIER_BOTH_SPELLINGS: &str = "class S {
3911 static readonly int q = 0;
3912 static void M(scoped ref int a, ref readonly int b, in int c, out int d) { d = q; }
3913 static void N(int[] xs) {
3914 ref int r = ref xs[0];
3915 scoped ref int s = ref xs[1];
3916 foreach (var e in xs) { }
3917 M(ref r, in s, in s, out r);
3918 }
3919}";
3920
3921 /// The six aliased parameter modifiers reach a Halstead half, and
3922 /// each reaches the same one its bare token reaches elsewhere
3923 /// (#1418).
3924 #[cfg(feature = "csharp")]
3925 #[test]
3926 fn csharp_parameter_modifiers_are_classified_1418() {
3927 // Operators (n1 = 15, N1 = 48): class 1; {} 8 (the class body
3928 // and seven method bodies); static 8 (the class and seven
3929 // methods); void 7; () 8 (seven parameter lists and the call);
3930 // int 3 (`out int x`, `int[]`, `ref int y`); [] 1; = 1; ; 2;
3931 // scoped 1; ref 3; readonly 1; out 2 (the parameter and the
3932 // argument); in 1; params 1.
3933 //
3934 // Operands (n2 = 15, N2 = 24): E 1; A/B/C/G/H/I 1 each; D 2
3935 // (declaration and call); this 1; Foo 4; f 4; x 2; 1 1; xs 1;
3936 // y 2.
3937 //
3938 // Before the fix this read (11, 41, 14, 23). The eight childless
3939 // `modifier` nodes were billed nowhere, which cost seven
3940 // operator occurrences and one operand occurrence, and emptied
3941 // `scoped`, `ref`, `readonly` and `in` out of the operator
3942 // vocabulary entirely — `out` survived only because the call
3943 // argument spells it with the unaliased kind.
3944 assert_halstead_counts::<CsharpParser>(
3945 CSHARP_PARAMETER_MODIFIERS,
3946 "foo.cs",
3947 [15, 48, 15, 24],
3948 "csharp parameter modifiers",
3949 );
3950
3951 // The receiver is an operand, matching every other C# `this`
3952 // (#1380). Both halves matter: an arm listing it twice would
3953 // still put it among the operands.
3954 assert_keywords_are_operands_only::<CsharpParser>(
3955 CSHARP_PARAMETER_MODIFIERS,
3956 "foo.cs",
3957 &["this"],
3958 );
3959
3960 let ops = ops_of::<CsharpParser>(CSHARP_PARAMETER_MODIFIERS, "foo.cs");
3961 for keyword in ["ref", "out", "in", "scoped", "readonly", "params"] {
3962 assert!(
3963 ops.operators.iter().any(|o| o == keyword),
3964 "`{keyword}` must be an operator; operators were {:?}",
3965 ops.operators
3966 );
3967 assert!(
3968 !ops.operands.iter().any(|o| o == keyword),
3969 "`{keyword}` must not be an operand; operands were {:?}",
3970 ops.operands
3971 );
3972 }
3973
3974 // The vocabulary is not deduplicated — it is the concatenation
3975 // of the kind-keyed and the lexeme-keyed operator maps — so a
3976 // keyword appearing twice is the #453 shape: one keyword split
3977 // across the two maps and counted twice in `n1`. Keeping both
3978 // spellings in one map is what `CsharpCode::is_primitive`
3979 // listing the five bare kinds buys, and this is the only
3980 // assertion that can see it.
3981 //
3982 // It needs a fixture carrying *both* spellings of each keyword,
3983 // which `CSHARP_PARAMETER_MODIFIERS` does for `out` alone: its
3984 // `ref`, `in`, `scoped` and `readonly` are all in parameter
3985 // position, so for those the count can only ever be one and the
3986 // assertion is dead. That left four of the five bare kinds with
3987 // no input at all — dropping `Csharp::Ref`, `In`, `Readonly` or
3988 // `Scoped` from `is_primitive` failed no test in the workspace.
3989 assert_modifier_spellings_are_one_operator(
3990 CSHARP_MODIFIER_BOTH_SPELLINGS,
3991 &[
3992 ("ref", Csharp::Ref as u16),
3993 ("out", Csharp::Out as u16),
3994 ("in", Csharp::In as u16),
3995 ("scoped", Csharp::Scoped as u16),
3996 ("readonly", Csharp::Readonly as u16),
3997 ],
3998 );
3999 }
4000
4001 /// Asserts each keyword in `spellings` appears in `source` both as
4002 /// its own `kind` and as a childless aliased `modifier`, and that the
4003 /// two spellings render as exactly one entry in the operator
4004 /// vocabulary.
4005 ///
4006 /// The vocabulary is the concatenation of the kind-keyed and the
4007 /// lexeme-keyed operator maps, so a keyword listed twice is one
4008 /// keyword split across both maps and counted twice in `n1`.
4009 #[cfg(feature = "csharp")]
4010 #[track_caller]
4011 fn assert_modifier_spellings_are_one_operator(source: &str, spellings: &[(&str, u16)]) {
4012 let code = source.as_bytes();
4013 // (bare, aliased) occurrences per keyword.
4014 let mut seen = vec![(0_usize, 0_usize); spellings.len()];
4015
4016 for_each_node_with_chain::<CsharpCode>(code, |node, _| {
4017 let text = &code[node.start_byte()..node.end_byte()];
4018 let Some(index) = spellings.iter().position(|(kw, _)| kw.as_bytes() == text) else {
4019 return;
4020 };
4021 if node.kind_id() == Csharp::Modifier as u16 && node.child_count() == 0 {
4022 seen[index].1 += 1;
4023 } else if node.kind_id() == spellings[index].1 {
4024 seen[index].0 += 1;
4025 }
4026 });
4027
4028 // Without this the vocabulary count below is vacuous for any
4029 // keyword the fixture stopped spelling both ways — the decay
4030 // that once left four of #1418's bare kinds with no input.
4031 for ((keyword, _), (bare, aliased)) in spellings.iter().zip(&seen) {
4032 assert!(
4033 *bare > 0 && *aliased > 0,
4034 "fixture must spell `{keyword}` both ways; it has {bare} bare and {aliased} aliased"
4035 );
4036 }
4037
4038 let ops = ops_of::<CsharpParser>(source, "both.cs");
4039 for (keyword, _) in spellings {
4040 assert_eq!(
4041 ops.operators.iter().filter(|o| o == keyword).count(),
4042 1,
4043 "`{keyword}` must be one operator across both of its kind spellings; \
4044 operators were {:?}",
4045 ops.operators
4046 );
4047 }
4048 }
4049
4050 /// Pins the grammar shape the #1418 arm gates on, in both
4051 /// directions.
4052 ///
4053 /// The arm reads `modifier` nodes with no children as aliased bare
4054 /// keywords and leaves the rest to their classified keyword leaf.
4055 /// That is a claim about two populations, and a grammar bump can
4056 /// break either: giving an alias a token child would double-count
4057 /// it, and flattening a declaration modifier to a childless node
4058 /// would bill `public` / `static` as an unknown keyword. Both are
4059 /// checked here, so neither can move quietly.
4060 ///
4061 /// Childlessness says "aliased", *not* "parameter modifier" —
4062 /// `_lambda_expression_init` and `anonymous_method_expression` alias
4063 /// a bare `static` / `async` onto the same kind, which the second
4064 /// walk below pins so the distinction cannot be forgotten. Both are
4065 /// operators, as their declaration spelling is (#1482).
4066 #[cfg(feature = "csharp")]
4067 #[test]
4068 fn csharp_aliased_modifier_is_childless() {
4069 let code = CSHARP_PARAMETER_MODIFIERS.as_bytes();
4070 let (mut aliased, mut declaration) = (0_usize, 0_usize);
4071
4072 for_each_node_with_chain::<CsharpCode>(code, |node, chain| {
4073 if node.kind_id() != Csharp::Modifier as u16 {
4074 return;
4075 }
4076 let role = CsharpCode::get_op_type_with_code(node, code, Ancestors::known(chain));
4077 let text = &code[node.start_byte()..node.end_byte()];
4078
4079 if node.child_count() == 0 {
4080 aliased += 1;
4081 // Spelled out rather than `this`-versus-everything-else,
4082 // so a text this fixture does not carry fails loudly;
4083 // the lambda spellings are owned by the second walk.
4084 let (correct, expected) = match text {
4085 b"this" => (matches!(role, TokenRole::Operand), "an operand"),
4086 b"scoped" | b"ref" | b"out" | b"in" | b"readonly" => {
4087 (matches!(role, TokenRole::Operator), "an operator")
4088 }
4089 other => panic!(
4090 "`{}` is not one of the six parameter spellings this fixture carries",
4091 String::from_utf8_lossy(other)
4092 ),
4093 };
4094 assert!(
4095 correct,
4096 "`{}` is an aliased parameter modifier and must be {expected}",
4097 String::from_utf8_lossy(text)
4098 );
4099 // Grammar-dispatch section 5: nothing containing the
4100 // token is classified, so billing the token cannot
4101 // count the same source text twice. Every ancestor,
4102 // not just `parameter` — `parameter_list` and the
4103 // declaration above it contain it too.
4104 for (depth, ancestor) in chain.iter().enumerate() {
4105 assert!(
4106 matches!(
4107 CsharpCode::get_op_type_with_code(
4108 ancestor,
4109 code,
4110 Ancestors::known(&chain[..depth])
4111 ),
4112 TokenRole::Unknown
4113 ),
4114 "`{}` contains an aliased modifier and is itself classified, so the \
4115 keyword now counts twice",
4116 ancestor.kind()
4117 );
4118 }
4119 } else {
4120 declaration += 1;
4121 assert_eq!(
4122 node.child_count(),
4123 1,
4124 "a declaration `modifier` wraps exactly one keyword leaf"
4125 );
4126 assert!(
4127 matches!(role, TokenRole::Unknown),
4128 "the `{}` wrapper must stay unclassified; its leaf carries the operator",
4129 String::from_utf8_lossy(text)
4130 );
4131 let leaf = node.child(0).expect("child_count is 1");
4132 let mut leaf_chain = chain.to_vec();
4133 leaf_chain.push(*node);
4134 assert!(
4135 matches!(
4136 CsharpCode::get_op_type_with_code(
4137 &leaf,
4138 code,
4139 Ancestors::known(&leaf_chain)
4140 ),
4141 TokenRole::Operator
4142 ),
4143 "the leaf under a declaration `modifier` must carry the operator"
4144 );
4145 }
4146 });
4147
4148 // Eight of each: `this`, `scoped`, `ref` x3, `readonly`, `out`,
4149 // `in` in parameter position, and the `static` of the class and
4150 // of its seven methods. Asserted so a fixture that stopped
4151 // carrying one population cannot pass having checked nothing.
4152 assert_eq!(aliased, 8, "fixture lost an aliased parameter modifier");
4153 assert_eq!(declaration, 8, "fixture lost a declaration modifier");
4154
4155 // The second alias site. `static (int x) => …` and `async
4156 // delegate (int y) { … }` put a bare keyword under the same
4157 // childless `modifier`, so `csharp_is_aliased_modifier` holds
4158 // for a node that is not in a parameter at all — which is why
4159 // the getter dispatches on the text and not on the kind.
4160 let lambda = "static class L {
4161 static void M() {
4162 var a = static (int x) => x + 1;
4163 var b = async delegate (int y) { return y; };
4164 }
4165}"
4166 .as_bytes();
4167 let mut lambda_aliases = 0_usize;
4168
4169 for_each_node_with_chain::<CsharpCode>(lambda, |node, chain| {
4170 if node.kind_id() != Csharp::Modifier as u16 || node.child_count() != 0 {
4171 return;
4172 }
4173 lambda_aliases += 1;
4174 let text = &lambda[node.start_byte()..node.end_byte()];
4175 assert!(
4176 matches!(text, b"static" | b"async"),
4177 "`{}` is not a lambda modifier the grammar aliases",
4178 String::from_utf8_lossy(text)
4179 );
4180 assert!(
4181 matches!(
4182 CsharpCode::get_op_type_with_code(node, lambda, Ancestors::known(chain)),
4183 TokenRole::Operator
4184 ),
4185 "`{}` on a lambda must be an operator, as its declaration spelling is (#1482)",
4186 String::from_utf8_lossy(text)
4187 );
4188 });
4189
4190 assert_eq!(
4191 lambda_aliases, 2,
4192 "fixture lost a lambda / anonymous-method modifier alias"
4193 );
4194 }
4195
4196 /// `static` and `async` in both of their kind spellings: the keyword
4197 /// leaf under a method's `modifier` wrapper, and the childless
4198 /// `modifier` the grammar aliases a lambda's keyword to (#1482).
4199 #[cfg(feature = "csharp")]
4200 const CSHARP_LAMBDA_MODIFIER_BOTH_SPELLINGS: &str = "class L {
4201 static void N() { }
4202 async void M() { }
4203 void P() {
4204 var a = static (int x) => x;
4205 var b = async (int y) => y;
4206 }
4207}";
4208
4209 /// A C# lambda's bare parameter (`x => x`) is the operand its
4210 /// parenthesised twin (`(x) => x`) bills (#1544). Each row is one
4211 /// lambda in a method body; the twins differ only by the `()` the
4212 /// parenthesised spelling writes, so the operand halves must match
4213 /// and the operator halves differ by exactly that one `()`.
4214 #[cfg(feature = "csharp")]
4215 #[test]
4216 fn csharp_implicit_lambda_parameter_is_an_operand() {
4217 let wrap = |lambda: &str| {
4218 format!("class C {{\n void M() {{\n var f = {lambda};\n }}\n}}\n")
4219 };
4220 // Operators: class, {} x2, void, () x1 (M's parameter list), =,
4221 // =>, ; — n1 = 7, N1 = 8 — plus the lambda's own `()` in the
4222 // parenthesised spelling. Operands: C, M, f, and the parameter
4223 // twice; `var` is an unclassified `implicit_type`. `_ => 0`
4224 // names `_` once and adds the literal `0`.
4225 //
4226 // Before #1544 the bare rows read (7, 8, 4, 4) for both: `x`
4227 // lost one occurrence, and `_` left the vocabulary entirely.
4228 for (lambda, expected) in [
4229 ("x => x", [7, 8, 4, 5]),
4230 ("(x) => x", [7, 9, 4, 5]),
4231 ("_ => 0", [7, 8, 5, 5]),
4232 ("(_) => 0", [7, 9, 5, 5]),
4233 ] {
4234 assert_halstead_counts::<CsharpParser>(&wrap(lambda), "foo.cs", expected, lambda);
4235 }
4236
4237 // The bare parameter is billed under its own text, not under
4238 // the kind name, and as an operand only.
4239 assert_keywords_are_operands_only::<CsharpParser>(&wrap("_ => 0"), "foo.cs", &["_"]);
4240 }
4241
4242 /// A lambda's `static` / `async` is the same operator as the
4243 /// declaration's, counted once in `n1` across both spellings
4244 /// (#1482).
4245 #[cfg(feature = "csharp")]
4246 #[test]
4247 fn csharp_lambda_modifiers_are_operators_1482() {
4248 // Operators (n1 = 10, N1 = 25): class 1; {} 4 (the class body
4249 // and three method bodies); static 2; async 2; void 3; () 5
4250 // (three method parameter lists and two lambda ones); int 2;
4251 // = 2; ; 2; => 2.
4252 //
4253 // Operands (n2 = 8, N2 = 10): L, N, M, P, a, b once each; x 2
4254 // and y 2 (the parameter and the lambda body). `var` is an
4255 // unclassified `implicit_type` wrapper.
4256 //
4257 // Before the fix this read (10, 23, 8, 10): the two lambda
4258 // keywords were billed nowhere. Billing them without listing
4259 // `Static` / `Async` in `CsharpCode::is_primitive` reads
4260 // (12, 25, 8, 10) — each keyword split across the kind-keyed
4261 // and the lexeme-keyed operator map.
4262 assert_halstead_counts::<CsharpParser>(
4263 CSHARP_LAMBDA_MODIFIER_BOTH_SPELLINGS,
4264 "foo.cs",
4265 [10, 25, 8, 10],
4266 "csharp lambda modifiers",
4267 );
4268
4269 assert_modifier_spellings_are_one_operator(
4270 CSHARP_LAMBDA_MODIFIER_BOTH_SPELLINGS,
4271 &[
4272 ("static", Csharp::Static as u16),
4273 ("async", Csharp::Async as u16),
4274 ],
4275 );
4276 }
4277
4278 #[cfg(feature = "go")]
4279 #[test]
4280 fn go_operators_and_operands() {
4281 check_metrics::<GoParser>(
4282 "package main
4283 func sum(a, b int) int {
4284 return a + b
4285 }",
4286 "foo.go",
4287 |metric| {
4288 insta::assert_json_snapshot!(
4289 metric.halstead,
4290 @r#"
4291 {
4292 "unique_operators": 7,
4293 "total_operators": 7,
4294 "unique_operands": 5,
4295 "total_operands": 8,
4296 "length": 15,
4297 "estimated_program_length": 31.26112492884004,
4298 "purity_ratio": 2.0840749952560027,
4299 "vocabulary": 12,
4300 "volume": 53.77443751081734,
4301 "difficulty": 5.6,
4302 "level": 0.17857142857142858,
4303 "effort": 301.1368500605771,
4304 "time": 16.729825003365395,
4305 "bugs": 0.014975730436275946
4306 }
4307 "#
4308 );
4309 },
4310 );
4311 }
4312
4313 #[cfg(feature = "perl")]
4314 #[test]
4315 fn perl_operators_and_operands() {
4316 check_metrics::<PerlParser>(
4317 "sub sum {
4318 my ($a, $b) = @_;
4319 return $a + $b;
4320 }",
4321 "foo.pl",
4322 |metric| {
4323 insta::assert_json_snapshot!(
4324 metric.halstead,
4325 @r#"
4326 {
4327 "unique_operators": 10,
4328 "total_operators": 14,
4329 "unique_operands": 4,
4330 "total_operands": 6,
4331 "length": 20,
4332 "estimated_program_length": 41.219280948873624,
4333 "purity_ratio": 2.0609640474436812,
4334 "vocabulary": 14,
4335 "volume": 76.14709844115208,
4336 "difficulty": 7.5,
4337 "level": 0.13333333333333333,
4338 "effort": 571.1032383086406,
4339 "time": 31.727957683813365,
4340 "bugs": 0.02294502281013948
4341 }
4342 "#
4343 );
4344 },
4345 );
4346 }
4347
4348 #[cfg(feature = "perl")]
4349 #[test]
4350 fn perl_interpolated_string_no_double_count() {
4351 // Regression: issue #199. A `string_double_quoted` (and
4352 // `string_qq_quoted` / `backtick_quoted` / `command_qx_quoted`)
4353 // wrapping an `interpolation` child used to be counted as a
4354 // Halstead operand while the inner scalar/array/hash variable
4355 // was also walked and counted — double-counting the inner
4356 // variable's contribution to `N2`. Mirrors #180 (Bash/Elixir),
4357 // #183 (C#), #184 (PHP), #191 (Kotlin).
4358 //
4359 // expected: for
4360 // sub greet { my $name = shift; my $msg = "Hi $name"; return $msg; }
4361 // — operands are `greet`, `$name`, `shift`, `$msg`. With the
4362 // fix the wrapping `"Hi $name"` is skipped (has `Interpolation`
4363 // child), so u_operands = 4 and N2 = 6 (`$name` x2 from the
4364 // `my` binding and the interpolation; `$msg` x2 from the `my`
4365 // binding and `return`; `greet`, `shift` once each). Without
4366 // the fix the wrapping literal would also be counted, lifting
4367 // u_operands to 5 and N2 to 7.
4368 check_metrics::<PerlParser>(
4369 "sub greet { my $name = shift; my $msg = \"Hi $name\"; return $msg; }",
4370 "foo.pl",
4371 |metric| {
4372 assert_eq!(metric.halstead.unique_operands(), 4);
4373 assert_eq!(metric.halstead.total_operands(), 6);
4374 insta::assert_json_snapshot!(metric.halstead);
4375 },
4376 );
4377 }
4378
4379 #[cfg(feature = "perl")]
4380 #[test]
4381 fn perl_plain_string_still_operand() {
4382 // The fix for #199 only skips wrapping literals that carry an
4383 // `Interpolation` child; a plain `"hello"` (no `$…` inside)
4384 // must still contribute exactly one operand. expected: operands
4385 // `greet`, `$msg`, `"hello"` → u_operands = 3, N2 = 4 (`$msg`
4386 // appears in the `my` binding and the `return`).
4387 check_metrics::<PerlParser>(
4388 "sub greet { my $msg = \"hello\"; return $msg; }",
4389 "foo.pl",
4390 |metric| {
4391 assert_eq!(metric.halstead.unique_operands(), 3);
4392 assert_eq!(metric.halstead.total_operands(), 4);
4393 },
4394 );
4395 }
4396
4397 #[cfg(feature = "perl")]
4398 #[test]
4399 fn perl_single_quoted_string_never_interpolates() {
4400 // Single-quoted (`'…'`) and `q{…}` literals are not subject to
4401 // interpolation in Perl, so even when their text contains a
4402 // `$name`-shaped sequence the wrapper is still counted as one
4403 // operand and the inner text is not parsed as a variable.
4404 // expected: operands `greet`, `$msg`, `'Hi $name'` →
4405 // u_operands = 3, N2 = 4 (`$msg` x2).
4406 check_metrics::<PerlParser>(
4407 "sub greet { my $msg = 'Hi $name'; return $msg; }",
4408 "foo.pl",
4409 |metric| {
4410 assert_eq!(metric.halstead.unique_operands(), 3);
4411 assert_eq!(metric.halstead.total_operands(), 4);
4412 },
4413 );
4414 }
4415
4416 #[cfg(feature = "perl")]
4417 #[test]
4418 fn perl_plain_heredoc_counts_as_one_operand() {
4419 // Regression: issue #287. A plain (non-interpolating) Perl
4420 // heredoc body used to be classified `TokenRole::Unknown`,
4421 // so its visible `HeredocBodyStatement` node contributed
4422 // nothing to N2 even though it is a string literal. The fix
4423 // adds `HeredocBodyStatement` to the interpolation-aware
4424 // operand arm, so an inert heredoc counts as one operand.
4425 //
4426 // Source (heredoc body lives at the source_file level, not
4427 // inside any sub):
4428 // my $msg = <<END;
4429 // hello world
4430 // END
4431 //
4432 // Operands traversed:
4433 // * `$msg` (`scalar_variable`) × 1
4434 // * heredoc body (`heredoc_body_statement`) × 1
4435 // expected: u_operands = 2, N2 = 2.
4436 check_metrics::<PerlParser>("my $msg = <<END;\nhello world\nEND\n", "foo.pl", |metric| {
4437 assert_eq!(metric.halstead.unique_operands(), 2);
4438 assert_eq!(metric.halstead.total_operands(), 2);
4439 });
4440 }
4441
4442 #[cfg(feature = "perl")]
4443 #[test]
4444 fn perl_interpolated_heredoc_no_double_count() {
4445 // Regression: issue #287. An interpolating Perl heredoc
4446 // (`<<"TAG"` or bare `<<TAG`) carries an `Interpolation` child
4447 // when its body contains a `$var`. The wrapper must drop to
4448 // `Unknown` so the inner scalar variable carries the operand
4449 // count — same dispatch as the existing double-quoted /
4450 // backtick / qx wrappers (issue #199) and the PHP heredoc fix
4451 // (issue #184).
4452 //
4453 // Source:
4454 // my $name = "x";
4455 // my $msg = <<"END";
4456 // hi $name
4457 // END
4458 //
4459 // Operands by text key:
4460 // * `$name` × 2 (my-binding + interpolation inside heredoc)
4461 // * `"x"` × 1 (inert double-quoted string)
4462 // * `$msg` × 1
4463 // expected: u_operands = 3, N2 = 4. Without the
4464 // interpolation-aware drop the wrapping heredoc body would
4465 // also count, lifting u_operands to 4 and N2 to 5.
4466 check_metrics::<PerlParser>(
4467 "my $name = \"x\";\nmy $msg = <<\"END\";\nhi $name\nEND\n",
4468 "foo.pl",
4469 |metric| {
4470 assert_eq!(metric.halstead.unique_operands(), 3);
4471 assert_eq!(metric.halstead.total_operands(), 4);
4472 },
4473 );
4474 }
4475
4476 #[cfg(feature = "perl")]
4477 #[test]
4478 fn perl_bare_pattern_delimiters_are_not_operators() {
4479 // Regression: issue #1312, the Perl sibling of Elixir #1256.
4480 // The bare match form is the only one of Perl's regex literals
4481 // whose delimiters are spelled with an operator token kind —
4482 // `bca dump` shows `/abc/` emitting two `SLASH` under
4483 // `PatternMatcher` — so `$s =~ /abc/;` reported a `/` operator
4484 // with no division in the source.
4485 //
4486 // expected: operators `$` (the `scalar_variable` sigil), `=~`
4487 // and `;` → n1 = N1 = 3. Operands `$s` and the `/abc/` pattern
4488 // → n2 = N2 = 2. Before the guard the two delimiters added `/`
4489 // → n1 = 4, N1 = 5; the pattern operand arrived with #1314,
4490 // which promoted all three pattern spellings together (see
4491 // `perl_every_pattern_value_spelling_scores_alike`).
4492 check_metrics::<PerlParser>("$s =~ /abc/;\n", "foo.pl", |metric| {
4493 assert_eq!(metric.halstead.unique_operators(), 3);
4494 assert_eq!(metric.halstead.total_operators(), 3);
4495 assert_eq!(metric.halstead.unique_operands(), 2);
4496 assert_eq!(metric.halstead.total_operands(), 2);
4497 });
4498 }
4499
4500 #[cfg(feature = "perl")]
4501 #[test]
4502 fn perl_every_pattern_value_spelling_scores_alike() {
4503 // Companion to the test above (#1312, extended by #1314).
4504 // `m/abc/` is exactly `/abc/` in Perl and `qr/abc/` is the same
4505 // pattern as a value, so the three spellings of a pattern
4506 // *value* must score identically whatever delimiters they use.
4507 //
4508 // Until #1314 they scored alike at *zero*: no Perl pattern
4509 // wrapper was in the operand arm, so the literal never counted,
4510 // unlike Ruby's `Regex` and Elixir's `Sigil`. #1312 declined to
4511 // promote the bare form on its own precisely because that would
4512 // have scored `/abc/` at one operand and its synonyms at zero,
4513 // reintroducing the spelling sensitivity this test pins.
4514 // Promoting all three together closes the gap and keeps the
4515 // equality, which is what this test now asserts.
4516 //
4517 // `s///` and `tr///` are deliberately *not* rows here any more.
4518 // They are operations applied to a target rather than pattern
4519 // values, so #1314 made them operators; their own equality is
4520 // pinned by `perl_every_pattern_operation_spelling_scores_alike`
4521 // below. Splitting the one loop in two is the substantive
4522 // disagreement #1314 had with the reasoning recorded here: this
4523 // test governs *synonyms*, and `s///` is not a synonym of
4524 // `/abc/`.
4525 //
4526 // The fixture matches twice against one variable so that no two
4527 // of `[n1, N1, n2, N2]` are equal. A square tuple would leave
4528 // `assert_halstead_counts`' unique-vs-total axes unpinned —
4529 // transposing n1 with N1 inside the helper failed no test while
4530 // all three of its callers expected a square tuple.
4531 //
4532 // expected per variant: operators `$` × 2 (one per
4533 // `scalar_variable`), `=~` × 2, `and`, `;` → n1 = 4, N1 = 6.
4534 // The pattern contributes no *operator* — that is #1312's half
4535 // — and one operand, so with `$s` twice and the pattern twice
4536 // → n2 = 2, N2 = 4.
4537 for pattern in ["/abc/", "m/abc/", "m{abc}", "qr/abc/"] {
4538 assert_halstead_counts::<PerlParser>(
4539 &format!("$s =~ {pattern} and $s =~ {pattern};\n"),
4540 "foo.pl",
4541 [4, 6, 2, 4],
4542 &format!("pattern {pattern}"),
4543 );
4544 }
4545 }
4546
4547 #[cfg(feature = "perl")]
4548 #[test]
4549 fn perl_comment_leader_is_not_an_operator_1549() {
4550 // tree-sitter-perl emits a comment's leading `#` as a `#` token
4551 // under `comments`, and the getter listed it as an operator, so
4552 // every comment billed one `#` (#1549). The oracle is the same
4553 // source with its comments deleted, which must score identically.
4554 // `$#a` and `qw#…#` are kept to pin that dropping `#` leaves the
4555 // distinct `$#` operator and the `qw` delimiters alone.
4556 //
4557 // expected: operators `sub`, `{}`, `my`, `=`, `;` × 2, `return`,
4558 // `$#` × 2, `+` → n1 = 8, N1 = 10. Operands `f`, `@a`, `x`, `y`,
4559 // `a` × 2 → n2 = 5, N2 = 6. Before the fix the commented row
4560 // added `#` → n1 = 9, N1 = 13. Once excluded, a comment feeds no
4561 // axis to anchor on, so only the revert run guards the fixture:
4562 // keep its comments.
4563 for (source, label) in [
4564 (
4565 "sub f {\n # leading\n my @a = qw#x y#; # tail\n return $#a + $#a; # tail\n}\n",
4566 "commented",
4567 ),
4568 (
4569 "sub f {\n my @a = qw#x y#;\n return $#a + $#a;\n}\n",
4570 "comment-free twin",
4571 ),
4572 ] {
4573 assert_halstead_counts::<PerlParser>(source, "foo.pl", [8, 10, 5, 6], label);
4574 }
4575 }
4576
4577 #[cfg(feature = "perl")]
4578 #[test]
4579 fn perl_bare_pattern_text_bills_no_operator_1549() {
4580 // The bare `/…/` form keeps its pattern text in a
4581 // `regex_pattern_content` that spells `#`, `[`, `]` and `\` as
4582 // anonymous tokens, three of which an operator arm listed, so
4583 // `/a#b[c]\#/` billed `#`, `[]` and `\` operators. `m//` and
4584 // `qr//` emit no node for the same text, so they are the oracle:
4585 // all three spellings must score as `/abc/` does in
4586 // `perl_every_pattern_value_spelling_scores_alike`.
4587 //
4588 // expected per variant: operators `$` × 2, `=~` × 2, `and`, `;`
4589 // → n1 = 4, N1 = 6; operands `$s` × 2 and the pattern × 2 →
4590 // n2 = 2, N2 = 4. Before the fix the bare row scored n1 = 7,
4591 // N1 = 14. The `#`, `[c]` and `\#` feed no axis once excluded,
4592 // so nothing anchors them and only the revert run guards the
4593 // fixture: keep them in the bare row.
4594 for pattern in [r"/a#b[c]\#/", r"m/a#b[c]\#/", r"qr/a#b[c]\#/"] {
4595 assert_halstead_counts::<PerlParser>(
4596 &format!("$s =~ {pattern} and $s =~ {pattern};\n"),
4597 "foo.pl",
4598 [4, 6, 2, 4],
4599 &format!("pattern {pattern}"),
4600 );
4601 }
4602 }
4603
4604 #[cfg(feature = "perl")]
4605 #[test]
4606 fn perl_every_pattern_operation_spelling_scores_alike() {
4607 // The other half of the split (#1314). Substitution and
4608 // transliteration are operations applied to a target, so they
4609 // are operators — and like the value spellings, their delimiter
4610 // choice must not move the count. `y///` is a synonym of
4611 // `tr///` and shares `TransliterationTrOrY`, so the two fold to
4612 // one operator entry, which is why all four rows agree on n1.
4613 //
4614 // expected per variant: operators `$` × 2, `=~` × 2, `and`,
4615 // `;`, and the operation itself × 2 → n1 = 5, N1 = 8. The
4616 // pattern and replacement text is invisible to this grammar —
4617 // `substitution_pattern_s` emits only its keyword and
4618 // delimiters, no content node — so the sole operand is `$s`,
4619 // twice → n2 = 1, N2 = 2.
4620 for pattern in ["s/a/b/", "s{a}{b}", "tr/a/b/", "y/a/b/"] {
4621 assert_halstead_counts::<PerlParser>(
4622 &format!("$s =~ {pattern} and $s =~ {pattern};\n"),
4623 "foo.pl",
4624 [5, 8, 1, 2],
4625 &format!("pattern {pattern}"),
4626 );
4627 }
4628 }
4629
4630 #[cfg(feature = "perl")]
4631 #[test]
4632 fn perl_interpolated_pattern_operands_agree_but_operators_do_not() {
4633 // Two things at once (#1314), because they are the same
4634 // measurement: why the three pattern-value spellings route
4635 // through `string_operand_type` rather than a plain operand
4636 // arm, and what that routing does *not* fix.
4637 //
4638 // `m/$x/` and `qr/$x/` emit a real `Interpolation` wrapping a
4639 // `scalar_variable`, while the bare form keeps its `$x` inside
4640 // an unclassified `regex_pattern_content`. So:
4641 //
4642 // * Operands agree at n2 = N2 = 2 (`$s` plus one contribution
4643 // from the pattern) only because of the interpolation guard.
4644 // A plain operand arm would count the wrapper *and* the inner
4645 // `$x` for the suffixed forms — n2 = 3 — reintroducing
4646 // through the back door the divergence
4647 // `perl_every_pattern_value_spelling_scores_alike` exists to
4648 // prevent. Which node carries the one operand still differs
4649 // by spelling: the wrapper for the bare form, the inner `$x`
4650 // for the other two.
4651 // * Operators do *not* agree: the exposed `scalar_variable`
4652 // brings a `$` sigil, an operator here, that the bare form
4653 // has no node for. Over two matches N1 is 6 for `/$x/` and
4654 // 8 for the other two.
4655 //
4656 // The operator asymmetry is a grammar gap this classifier
4657 // cannot repair — there is nothing to classify in the bare
4658 // form — so it is pinned rather than papered over, the same
4659 // treatment `perl_division_emits_no_slash_token` gives the
4660 // missing division token. A bump that starts exposing the bare
4661 // form's interpolation turns this red, at which point the
4662 // expectations above need re-deriving.
4663 // Each fixture matches twice, so `N1 > n1` and `N2 > n2` and no
4664 // row is a square tuple that a transposition inside
4665 // `assert_halstead_counts` could pass (#1312).
4666 assert_halstead_counts::<PerlParser>(
4667 "$s =~ /$x/ and $s =~ /$x/;\n",
4668 "foo.pl",
4669 [4, 6, 2, 4],
4670 "bare /$x/",
4671 );
4672 for pattern in ["m/$x/", "qr/$x/"] {
4673 assert_halstead_counts::<PerlParser>(
4674 &format!("$s =~ {pattern} and $s =~ {pattern};\n"),
4675 "foo.pl",
4676 [4, 8, 2, 4],
4677 &format!("suffixed {pattern}"),
4678 );
4679 }
4680 }
4681
4682 #[cfg(feature = "perl")]
4683 #[test]
4684 fn perl_division_emits_no_slash_token() {
4685 // Drift marker, not an endorsement. Ruby's counterpart
4686 // (`ruby_division_survives_the_regex_guard`) proves #1312's
4687 // guard cannot swallow a real division; Perl has no such
4688 // fixture to write, because at the pinned grammar `$a / $b`
4689 // emits *no* `SLASH` token at all — `binary_expression`'s
4690 // children skip straight from one `scalar_variable` to the
4691 // other. Perl division therefore counts zero operators today,
4692 // a pre-existing grammar gap this fix neither causes nor
4693 // repairs.
4694 //
4695 // Pinning it keeps the gap in CI: a bump that starts emitting
4696 // the token turns this red, at which point the division would
4697 // begin counting (its parent is `BinaryExpression`, not
4698 // `PatternMatcher`, so the guard leaves it alone) and the
4699 // expectations above need re-deriving.
4700 //
4701 // The same gap is why no Perl test can distinguish the
4702 // parent-scoped guard from an ancestor-scoped one: with no
4703 // `SLASH` reachable below a `PatternMatcher`, that mutant is
4704 // unobservable here — measured, not assumed. Perl's guard is
4705 // parent-scoped for correctness by construction and for
4706 // symmetry with Ruby's, where the distinction *is* observable
4707 // and is pinned by
4708 // `ruby_regex_guard_is_parent_scoped_not_ancestor_scoped`.
4709 let path = PathBuf::from("foo.pl");
4710 let source = "my $z = $a / $b;\n";
4711 let parser = PerlParser::new(source.as_bytes().to_vec(), &path, None);
4712 assert!(
4713 !ast_has_kind_id(&parser, Perl::SLASH as u16),
4714 "tree-sitter-perl still emits no SLASH for `{source}`"
4715 );
4716 // Anchor the negative assertion to *this* fixture. Without it
4717 // the test stays green when `source` is edited to something
4718 // containing no division at all — measured: swapping in
4719 // `my $z = 1;` failed nothing.
4720 //
4721 // expected: operators `my`, `=`, `$` × 3 (one per
4722 // `scalar_variable`), `;` → n1 = 4, N1 = 6, with no `/` among
4723 // them. Operands `$z`, `$a`, `$b` → n2 = N2 = 3.
4724 check_metrics::<PerlParser>(source, "foo.pl", |metric| {
4725 assert_eq!(metric.halstead.unique_operators(), 4);
4726 assert_eq!(metric.halstead.total_operators(), 6);
4727 assert_eq!(metric.halstead.unique_operands(), 3);
4728 assert_eq!(metric.halstead.total_operands(), 3);
4729 });
4730 // Positive control: the same kind *is* reachable in this
4731 // grammar, so the assertion above is about division and not
4732 // about `Perl::SLASH` being enum-only dead weight.
4733 let matcher = PerlParser::new(b"$s =~ /abc/;\n".to_vec(), &path, None);
4734 assert!(
4735 ast_has_kind_id(&matcher, Perl::SLASH as u16),
4736 "Perl::SLASH must be the bare pattern delimiter kind"
4737 );
4738 }
4739
4740 #[cfg(feature = "perl")]
4741 #[test]
4742 fn perl_readline_closer_alias_never_reaches_kind_id() {
4743 // Drift marker for `Perl::GT2` (#1395). The grammar closes
4744 // `<FH>` / `<$fh>` with `token.immediate('>')`, which the
4745 // generated parser gives its own symbol — hence the `GT2`
4746 // variant sitting next to `FileHandle` in the enum — but whose
4747 // `public_symbol_map` entry collapses it onto `GT` before
4748 // `kind_id()`, exactly like `LPAREN2` in #768 and Ruby's
4749 // `SLASH2` in #1312. Nothing in `getter/perl.rs` names it, and
4750 // nothing should: `compute_halstead` keys a non-primitive
4751 // operator on `kind_id()`, so two live kinds both rendering
4752 // `">"` would split one `>` across two `n1` entries. If a bump
4753 // ever starts emitting it, the repair is to route `>` through
4754 // the lexeme-keyed map, not to list both — and this test is
4755 // what says so.
4756 //
4757 // The C++ grammars' template-list closer collapses the same way;
4758 // `cpp_template_closer_alias_never_reaches_kind_id` pins that.
4759 let path = PathBuf::from("foo.pl");
4760 for source in ["my $l = <FH>;\n", "my $l = <$fh>;\n", "my $b = $x > $y;\n"] {
4761 let parser = PerlParser::new(source.as_bytes().to_vec(), &path, None);
4762 assert!(
4763 !ast_has_kind_id(&parser, Perl::GT2 as u16),
4764 "Perl::GT2 must stay collapsed to Perl::GT for `{source}`"
4765 );
4766 // Positive control: the id the operator arm fires on is
4767 // present, so the assertion above cannot pass merely
4768 // because the fixture grew no `>` at all.
4769 assert!(
4770 ast_has_kind_id(&parser, Perl::GT as u16),
4771 "Perl::GT must be the `>` kind for `{source}`"
4772 );
4773 }
4774 }
4775
4776 /// Drift marker for `Cpp::GT2` / `Mozcpp::GT2`. Both grammars close
4777 /// a template parameter or argument list with
4778 /// `alias(token(prec(1, '>')), '>')`, which the generated parser
4779 /// numbers separately, but `ts_symbol_map` folds that symbol onto
4780 /// `GT` before `kind_id()`, as with Perl's readline closer above. The
4781 /// `GT2` arms in `getter/cpp.rs` and `getter/mozcpp.rs` are therefore
4782 /// defensive; this fails if a grammar bump makes them live, at which
4783 /// point the two kinds would split one `>` across two `n1` entries.
4784 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
4785 fn assert_template_closer_folds_to_gt<P: crate::ParserTrait>(gt: u16, gt2: u16, label: &str) {
4786 // One parameter list (`S<T>`'s declaration) and two argument
4787 // lists, the inner one closed by the first half of a `>>`.
4788 let source = "template <typename T> struct S {};\n\
4789 std::vector<std::vector<int>> v;\n\
4790 bool b = x > y;\n";
4791 let parser = P::new(source.as_bytes().to_vec(), &PathBuf::from("foo.cpp"), None);
4792 assert!(
4793 !ast_has_kind_id(&parser, gt2),
4794 "{label}::GT2 must stay collapsed to {label}::GT",
4795 );
4796 // Positive control: every template-list closer is present and
4797 // carries `GT`, so the assertion above cannot pass merely
4798 // because the fixture stopped producing template lists.
4799 let closers = parser
4800 .root()
4801 .preorder()
4802 .filter(|n| {
4803 n.kind_id() == gt
4804 && n.parent().is_some_and(|p| {
4805 matches!(
4806 p.kind(),
4807 "template_parameter_list" | "template_argument_list"
4808 )
4809 })
4810 })
4811 .count();
4812 assert_eq!(
4813 closers, 3,
4814 "{label}: each template-list closer must be a `GT`"
4815 );
4816 }
4817
4818 #[cfg(feature = "cpp")]
4819 #[test]
4820 fn cpp_template_closer_alias_never_reaches_kind_id() {
4821 assert_template_closer_folds_to_gt::<CppParser>(Cpp::GT as u16, Cpp::GT2 as u16, "Cpp");
4822 }
4823
4824 #[cfg(feature = "mozcpp")]
4825 #[test]
4826 fn mozcpp_template_closer_alias_never_reaches_kind_id() {
4827 assert_template_closer_folds_to_gt::<MozcppParser>(
4828 Mozcpp::GT as u16,
4829 Mozcpp::GT2 as u16,
4830 "Mozcpp",
4831 );
4832 }
4833
4834 /// Asserts a grammar's wrapped generic-list opener, `LT2`, never
4835 /// reaches a node, so the `LT` its getter pairs as `<>` is the only
4836 /// `<` a list opens with (#1559, grammar-dispatch §2). `lists` are the
4837 /// lists the wrapped token opens; the positive control counts their
4838 /// `LT` openers, so the assertion cannot pass on a fixture that
4839 /// stopped producing those lists.
4840 #[cfg(any(feature = "groovy", feature = "objc", feature = "rust"))]
4841 #[track_caller]
4842 fn assert_generic_opener_folds_to_lt<P: crate::ParserTrait>(
4843 source: &str,
4844 [lt, lt2]: [u16; 2],
4845 lists: &[&str],
4846 openers: usize,
4847 label: &str,
4848 ) {
4849 let parser = P::new(source.as_bytes().to_vec(), &PathBuf::from("foo"), None);
4850 assert!(
4851 !ast_has_kind_id(&parser, lt2),
4852 "{label}::LT2 must stay collapsed to {label}::LT",
4853 );
4854 let found = parser
4855 .root()
4856 .preorder()
4857 .filter(|n| n.kind_id() == lt && n.parent().is_some_and(|p| lists.contains(&p.kind())))
4858 .count();
4859 assert_eq!(
4860 found, openers,
4861 "{label}: each generic-list opener must be an `LT`"
4862 );
4863 }
4864
4865 #[cfg(feature = "rust")]
4866 #[test]
4867 fn rust_generic_opener_alias_never_reaches_kind_id() {
4868 assert_generic_opener_folds_to_lt::<RustParser>(
4869 "fn u<T>(x: Vec<T>) -> impl Sized + use<T> { x }\n",
4870 [Rust::LT as u16, Rust::LT2 as u16],
4871 &["type_arguments", "use_bounds"],
4872 2,
4873 "Rust",
4874 );
4875 }
4876
4877 #[cfg(feature = "groovy")]
4878 #[test]
4879 fn groovy_generic_opener_alias_never_reaches_kind_id() {
4880 assert_generic_opener_folds_to_lt::<GroovyParser>(
4881 "class A<T> { List<T> v }\n",
4882 [Groovy::LT as u16, Groovy::LT2 as u16],
4883 &["type_parameters", "type_arguments"],
4884 2,
4885 "Groovy",
4886 );
4887 }
4888
4889 #[cfg(feature = "objc")]
4890 #[test]
4891 fn objc_generic_opener_alias_never_reaches_kind_id() {
4892 assert_generic_opener_folds_to_lt::<ObjcParser>(
4893 "int f(void) { return g(NSArray<NSString *>); }\n",
4894 [Objc::LT as u16, Objc::LT2 as u16],
4895 &["argument_list"],
4896 1,
4897 "Objc",
4898 );
4899 }
4900
4901 #[cfg(feature = "perl")]
4902 #[test]
4903 fn perl_readline_brackets_are_operators() {
4904 // #1395: a readline's `<` / `>` delimit syntax, not a literal,
4905 // so under the policy on `Getter::get_op_type` they are
4906 // ordinary operators — even though ABC's `conditions` excludes
4907 // them (#1297). `<STDIN>` is the one spelling that cannot show
4908 // this: the grammar lexes it as a single `standard_input`
4909 // token, so there are no brackets to classify.
4910 //
4911 // expected: operators `sub`, `{}`, `my`, `$` × 2 (one per
4912 // `scalar_variable`), `=`, `<`, `>`, `;` × 2, `return` →
4913 // n1 = 9, N1 = 11. Operands `r`, `$l` × 2, `FH` →
4914 // n2 = 3, N2 = 4.
4915 check_metrics::<PerlParser>("sub r { my $l = <FH>; return $l; }\n", "foo.pl", |metric| {
4916 assert_eq!(metric.halstead.unique_operators(), 9);
4917 assert_eq!(metric.halstead.total_operators(), 11);
4918 assert_eq!(metric.halstead.unique_operands(), 3);
4919 assert_eq!(metric.halstead.total_operands(), 4);
4920 });
4921 }
4922
4923 /// Every (name wrapper, contained operand) pairing tree-sitter-perl's
4924 /// node-types.json admits, and the single source of truth for both
4925 /// halves of #1355's guard: its parent set is the distinct first
4926 /// components, the kinds it subsumes the distinct second ones.
4927 /// `perl_name_wrappers_bill_the_name_once_1355` witnesses every row
4928 /// and fails on an eleventh pairing.
4929 #[cfg(feature = "perl")]
4930 const PERL_NAME_WRAPPER_PAIRINGS: [(Perl, Perl); 10] = [
4931 (Perl::PackageName, Perl::Identifier),
4932 (Perl::PackageName, Perl::ScalarVariable),
4933 (Perl::PackageName, Perl::ArrayVariable),
4934 (Perl::PackageName, Perl::HashVariable),
4935 (Perl::PackageName, Perl::SpecialScalarVariable),
4936 (Perl::PackageName, Perl::Typeglob),
4937 (Perl::PackageName, Perl::PackageVariable),
4938 (Perl::PackageVariable, Perl::PackageName),
4939 (Perl::PackageVariable, Perl::ScalarVariable),
4940 (Perl::Typeglob, Perl::Identifier),
4941 ];
4942
4943 /// The anonymous tokens those wrappers also hold. `::` and `*` are
4944 /// operators (matched above the guard, so they keep that reading);
4945 /// `{` folds into the `{}` glyph and `}` has never been classified
4946 /// at all. Listing them is what lets
4947 /// `perl_name_wrappers_bill_the_name_once_1355` police *every*
4948 /// child rather than only the named ones — the operand arm carries
4949 /// token-shaped kinds too (`True`, `FILE`, `SUB`, …), and a bump
4950 /// that let one of those inside a wrapper would otherwise be
4951 /// silenced with nothing failing.
4952 #[cfg(feature = "perl")]
4953 const PERL_NAME_WRAPPER_TOKENS: [Perl; 4] =
4954 [Perl::COLONCOLON, Perl::STAR, Perl::LBRACE, Perl::RBRACE];
4955
4956 /// The occurrences #1355's guard suppresses in `source`, paired
4957 /// with the (wrapper kind, child kind) pairings they witness.
4958 ///
4959 /// Walks with `for_each_node_with_chain`, which maintains the
4960 /// ancestor chain exactly as `spaces::compute` does, so "parent"
4961 /// here means what `Ancestors::parent` means inside the guard
4962 /// rather than what a differently-built chain would say.
4963 #[cfg(feature = "perl")]
4964 fn perl_subsumed_operands(source: &str) -> (Vec<String>, HashSet<(u16, u16)>) {
4965 let wrappers: HashSet<u16> = PERL_NAME_WRAPPER_PAIRINGS
4966 .map(|(wrapper, _)| wrapper as u16)
4967 .into();
4968 let subsumed: HashSet<u16> = PERL_NAME_WRAPPER_PAIRINGS
4969 .map(|(_, child)| child as u16)
4970 .into();
4971 let tokens: HashSet<u16> = PERL_NAME_WRAPPER_TOKENS.map(|kind| kind as u16).into();
4972 let code = source.as_bytes();
4973 let mut hidden = Vec::new();
4974 let mut pairings = HashSet::new();
4975 for_each_node_with_chain::<PerlCode>(code, |node, chain| {
4976 let Some(parent) = chain.last() else { return };
4977 if !wrappers.contains(&parent.kind_id()) {
4978 return;
4979 }
4980 assert!(
4981 subsumed.contains(&node.kind_id()) || tokens.contains(&node.kind_id()),
4982 "`{source}`: a `{}` inside a `{}` is a child this guard was not \
4983 derived against; re-read node-types.json before trusting it",
4984 node.kind(),
4985 parent.kind(),
4986 );
4987 if subsumed.contains(&node.kind_id()) {
4988 pairings.insert((parent.kind_id(), node.kind_id()));
4989 hidden.push(
4990 node.utf8_text(code)
4991 .expect("fixture is valid UTF-8")
4992 .to_owned(),
4993 );
4994 }
4995 });
4996 (hidden, pairings)
4997 }
4998
4999 /// One row of `perl_name_wrappers_bill_the_name_once_1355`'s
5000 /// table: a fixture, what it must measure now, what it measured
5001 /// before #1355, and the operand text behind the counts.
5002 struct PerlNameWrapperCase {
5003 source: &'static str,
5004 /// `[n1, N1, n2, N2]` with the guard in place.
5005 counts: [u64; 4],
5006 /// `[n2, N2]` without it. Re-derived by the loop rather than
5007 /// trusted, so a stale row fails instead of misinforming.
5008 before: [u64; 2],
5009 operands: &'static [&'static str],
5010 }
5011
5012 /// Regression for #1355. `package_name`, `package_variable` and
5013 /// `typeglob` are operands spanning a whole name, so every
5014 /// operand-classified node *inside* one was billed a second time:
5015 /// `use strict;` scored `N2` 2 for one name, `our $Foo::count = 3;`
5016 /// n2 5 / N2 6 for two, and the vocabulary grew a bare `::` entry
5017 /// because a `package_variable`'s qualifier slot is itself a
5018 /// childless `package_name`.
5019 ///
5020 /// Each row's `before` column is what it measured without the
5021 /// guard, and the loop re-derives both halves from the current
5022 /// parse rather than trusting the column — the guard is the only
5023 /// difference between the two classifications, so the occurrences
5024 /// it removes are exactly the subsumed-kind children of a wrapper:
5025 ///
5026 /// - `N2` before minus `N2` after must equal how many of those
5027 /// there are. That identity *is* the defect: one spurious operand
5028 /// per contained name part.
5029 /// - `n2` before is the post-fix vocabulary unioned with their
5030 /// texts. It exceeds `n2` after wherever a part's spelling is not
5031 /// already an operand on its own (`Data`, `::`, `count`).
5032 ///
5033 /// The walk doubles as the grammar-dispatch §1 / §2 drift marker.
5034 /// It asserts that every *named* child of a wrapper is one of the
5035 /// eight subsumed kinds — which is what makes keying the arm on the
5036 /// parent alone safe — and that all ten pairings node-types.json
5037 /// admits are exercised here, so a bump that renumbers or re-parents
5038 /// one fails loudly instead of leaving the counts below measuring a
5039 /// construct the arm no longer reaches.
5040 ///
5041 /// Two mutants this does *not* catch, measured rather than assumed.
5042 /// Widening the guard from parent- to ancestor-scoped fails nothing,
5043 /// for the reason `perl_division_emits_no_slash_token` already
5044 /// records about the other guard in this getter: every operand-kinded
5045 /// descendant of a wrapper is also a direct child of one, and the
5046 /// intervening sigil tokens are matched by the operator arm above
5047 /// before the guard is reached. Parent-scoping stands on
5048 /// grammar-dispatch §5 and on symmetry with that guard, not on a
5049 /// test. What *is* pinned is the arm's position: moving it above the
5050 /// operator arm swallows `::`, `*` and the typeglob's opening brace,
5051 /// and the operator columns below fail.
5052 #[cfg(feature = "perl")]
5053 #[test]
5054 fn perl_name_wrappers_bill_the_name_once_1355() {
5055 let cases: [PerlNameWrapperCase; 12] = [
5056 // identifier under package_name, the single-segment form.
5057 PerlNameWrapperCase {
5058 source: "use strict;\n",
5059 counts: [2, 2, 1, 1],
5060 before: [1, 2],
5061 operands: &["strict"],
5062 },
5063 // …and the multi-segment one, twice over.
5064 PerlNameWrapperCase {
5065 source: "require Data::Dumper;\n",
5066 counts: [3, 3, 1, 1],
5067 before: [3, 3],
5068 operands: &["Data::Dumper"],
5069 },
5070 PerlNameWrapperCase {
5071 source: "package Foo::Bar;\n",
5072 counts: [3, 3, 1, 1],
5073 before: [3, 3],
5074 operands: &["Foo::Bar"],
5075 },
5076 // The `bar` of a qualified call is a *sibling* of the
5077 // `package_name`, not a child, so it still counts while the
5078 // `Foo` inside the wrapper does not. That is the row saying
5079 // the guard reads position and not kind: keying it on the
5080 // child kinds instead fails 14 tests here, this one among
5081 // them.
5082 PerlNameWrapperCase {
5083 source: "Foo::bar();\n",
5084 counts: [3, 3, 2, 2],
5085 before: [2, 3],
5086 operands: &["Foo", "bar"],
5087 },
5088 // identifier under typeglob, bare and brace-delimited.
5089 PerlNameWrapperCase {
5090 source: "my $g = *STDOUT;\n",
5091 counts: [5, 5, 2, 2],
5092 before: [3, 3],
5093 operands: &["$g", "*STDOUT"],
5094 },
5095 PerlNameWrapperCase {
5096 source: "my $t = *{Foo};\n",
5097 counts: [6, 6, 2, 2],
5098 before: [3, 3],
5099 operands: &["$t", "*{Foo}"],
5100 },
5101 // package_name and scalar_variable under package_variable,
5102 // and scalar_variable under package_name — the reported
5103 // fixture, where `$Foo` was billed twice and `::` once.
5104 PerlNameWrapperCase {
5105 source: "our $Foo::count = 3;\n",
5106 counts: [4, 4, 2, 2],
5107 before: [5, 6],
5108 operands: &["$Foo::count", "3"],
5109 },
5110 // array_variable / hash_variable / special_scalar_variable
5111 // under package_name: the same shape with the other sigils.
5112 PerlNameWrapperCase {
5113 source: "my @l = @Foo::list;\n",
5114 counts: [3, 3, 2, 2],
5115 before: [5, 6],
5116 operands: &["@l", "@Foo::list"],
5117 },
5118 PerlNameWrapperCase {
5119 source: "my %h = %Foo::hash;\n",
5120 counts: [3, 3, 2, 2],
5121 before: [5, 6],
5122 operands: &["%h", "%Foo::hash"],
5123 },
5124 PerlNameWrapperCase {
5125 source: "my $z = $_::x;\n",
5126 counts: [4, 5, 2, 2],
5127 before: [5, 6],
5128 operands: &["$z", "$_::x"],
5129 },
5130 // typeglob under package_name.
5131 PerlNameWrapperCase {
5132 source: "*Foo::glob = 1;\n",
5133 counts: [3, 3, 2, 2],
5134 before: [6, 7],
5135 operands: &["*Foo::glob", "1"],
5136 },
5137 // package_variable under package_name: the nesting that
5138 // makes qualifier depth unbounded. One variable reference
5139 // used to spell seven vocabulary entries.
5140 PerlNameWrapperCase {
5141 source: "my $x = $Foo::Bar::baz;\n",
5142 counts: [4, 5, 2, 2],
5143 before: [7, 10],
5144 operands: &["$x", "$Foo::Bar::baz"],
5145 },
5146 ];
5147
5148 let mut witnessed: HashSet<(u16, u16)> = HashSet::new();
5149 for PerlNameWrapperCase {
5150 source,
5151 counts,
5152 before: [n2_before, total_before],
5153 operands,
5154 } in cases
5155 {
5156 let (hidden, pairings) = perl_subsumed_operands(source);
5157 assert!(
5158 !hidden.is_empty(),
5159 "row {source:?} contains no name-wrapper child, so it witnesses nothing",
5160 );
5161 witnessed.extend(pairings);
5162
5163 // Phrased as an addition rather than a subtraction so a
5164 // future edit that inverts the two underflows nothing and
5165 // fails with the message below.
5166 assert_eq!(
5167 total_before,
5168 counts[3] + hidden.len() as u64,
5169 "row {source:?} must shed exactly one operand per contained \
5170 name part; recorded N2_before {total_before}, parts {hidden:?}",
5171 );
5172 let mut vocabulary: HashSet<&str> = operands.iter().copied().collect();
5173 vocabulary.extend(hidden.iter().map(String::as_str));
5174 assert_eq!(
5175 vocabulary.len() as u64,
5176 n2_before,
5177 "row {source:?}: n2 before the fix is the post-fix vocabulary \
5178 plus the contained name parts; got {vocabulary:?}",
5179 );
5180
5181 assert_halstead_counts::<PerlParser>(source, "foo.pl", counts, source);
5182 assert_ops_operands::<PerlParser>(source, "foo.pl", operands.len(), operands.to_vec());
5183 }
5184
5185 let mut got: Vec<(u16, u16)> = witnessed.into_iter().collect();
5186 got.sort_unstable();
5187 let mut expected_pairings: Vec<(u16, u16)> = PERL_NAME_WRAPPER_PAIRINGS
5188 .map(|(wrapper, child)| (wrapper as u16, child as u16))
5189 .into();
5190 expected_pairings.sort_unstable();
5191 assert_eq!(
5192 got, expected_pairings,
5193 "the table must exercise every (wrapper, child) pairing \
5194 node-types.json admits, and no other",
5195 );
5196 }
5197
5198 /// The over-suppression half of #1355 (grammar-dispatch §6 and §11).
5199 /// The guard is keyed on the parent, so the same kinds it silences
5200 /// inside a name wrapper have to keep counting everywhere else —
5201 /// otherwise "one operand per name" would have been bought by
5202 /// zeroing ordinary variables and calls.
5203 ///
5204 /// `module_name` rides along because the issue asserted it shares
5205 /// the `identifier` leaf and would be collateral damage. It does
5206 /// not: `use 'Foo.pm'` parses to a leaf holding only its two quote
5207 /// tokens, so it wraps nothing and is untouched either way.
5208 #[cfg(feature = "perl")]
5209 #[test]
5210 fn perl_qw_list_bills_one_operand_per_element() {
5211 // `qw(a b c)` was invisible to Halstead — neither the elements,
5212 // the wrapper nor the `qw` keyword had an arm — so it billed
5213 // nothing where its synonym `("a", "b", "c")` billed three
5214 // operands. Each `list_item` is now one operand and the
5215 // `word_list_qw` wrapper is gated on holding one, the #1353
5216 // Ruby `%w[]` rule: one operand per element, or one for the
5217 // empty literal.
5218 assert_ops_operands::<PerlParser>(
5219 "my @a = qw(a b c);\n",
5220 "foo.pl",
5221 4,
5222 vec!["@a", "a", "b", "c"],
5223 );
5224 assert_ops_operands::<PerlParser>("my @a = qw();\n", "foo.pl", 2, vec!["@a", "qw()"]);
5225 assert_ops_operands::<PerlParser>(
5226 "use POSIX qw(floor ceil);\n",
5227 "foo.pl",
5228 3,
5229 vec!["POSIX", "floor", "ceil"],
5230 );
5231 // expected: [n1, N1, n2, N2] = [3, 3, 4, 4] for every delimiter —
5232 // operators `my`, `=`, `;`; the `qw` keyword and its delimiters
5233 // are unclassified, as Ruby's `%w[` is, so the choice of
5234 // delimiter cannot move the score (#1312).
5235 for spelling in [
5236 "qw(a b c)",
5237 "qw/a b c/",
5238 "qw{a b c}",
5239 "qw[a b c]",
5240 "qw<a b c>",
5241 ] {
5242 assert_halstead_counts::<PerlParser>(
5243 &format!("my @a = {spelling};\n"),
5244 "foo.pl",
5245 [3, 3, 4, 4],
5246 spelling,
5247 );
5248 }
5249 // The synonym: the same four operands, plus the `()` and `,`
5250 // operators the list spelling carries.
5251 assert_halstead_counts::<PerlParser>(
5252 "my @a = (\"a\", \"b\", \"c\");\n",
5253 "foo.pl",
5254 [5, 6, 4, 4],
5255 "list literal",
5256 );
5257 }
5258
5259 #[cfg(feature = "perl")]
5260 #[test]
5261 fn perl_qualified_name_leaves_still_count_elsewhere_1355() {
5262 // expected: operators `my` × 4, `$` × 3 (one per `$`-sigilled
5263 // variable), `=` × 4, `;` × 5, `()` × 3 and the fat comma
5264 // → n1 = 6, N1 = 20. Operands are the four declared variables,
5265 // the three integers, the hash key, `$_` and the call target
5266 // → n2 = N2 = 10. Five of the eight kinds the guard silences
5267 // under a name wrapper appear among them — `scalar_variable`,
5268 // `array_variable`, `hash_variable`, `special_scalar_variable`
5269 // and `identifier` — and all five still count here.
5270 let bare = "my $x = 1;\nmy @a = (2);\nmy %h = (k => 3);\nmy $u = $_;\nfoo();\n";
5271 assert_halstead_counts::<PerlParser>(bare, "foo.pl", [6, 20, 10, 10], bare);
5272 assert_ops_operands::<PerlParser>(
5273 bare,
5274 "foo.pl",
5275 10,
5276 vec!["$x", "1", "@a", "2", "%h", "k", "3", "$u", "$_", "foo"],
5277 );
5278 let (hidden, _) = perl_subsumed_operands(bare);
5279 assert!(
5280 hidden.is_empty(),
5281 "no name wrapper appears here, so the guard must be inert; got {hidden:?}",
5282 );
5283
5284 // expected: operators `use`, `;` → n1 = N1 = 2; the quoted
5285 // module name is the sole operand → n2 = N2 = 1.
5286 let quoted = "use 'Some.pm';\n";
5287 assert_halstead_counts::<PerlParser>(quoted, "foo.pl", [2, 2, 1, 1], quoted);
5288 assert_ops_operands::<PerlParser>(quoted, "foo.pl", 1, vec!["'Some.pm'"]);
5289 assert!(
5290 ast_has_kind_id(
5291 &PerlParser::new(quoted.as_bytes().to_vec(), &PathBuf::from("foo.pl"), None),
5292 Perl::ModuleName as u16,
5293 ),
5294 "the quoted `use` form no longer parses to `module_name`, so this \
5295 row no longer says anything about it",
5296 );
5297 }
5298
5299 #[cfg(feature = "lua")]
5300 #[test]
5301 fn lua_operators_and_operands() {
5302 check_metrics::<LuaParser>(
5303 "local function add(a, b)
5304 local result = a + b
5305 if result > 0 then
5306 return result
5307 end
5308 return 0
5309end",
5310 "foo.lua",
5311 |metric| {
5312 // n1=11: local,function,(,,,=,+,if,>,then,return,end
5313 // (after #695 the `)` closer no longer counts — only the
5314 // folded `(` opener does; was n1=12).
5315 // n2=5: add,a,b,result,0
5316 insta::assert_json_snapshot!(metric.halstead, @r#"
5317 {
5318 "unique_operators": 11,
5319 "total_operators": 14,
5320 "unique_operands": 5,
5321 "total_operands": 10,
5322 "length": 24,
5323 "estimated_program_length": 49.66338827944708,
5324 "purity_ratio": 2.0693078449769615,
5325 "vocabulary": 16,
5326 "volume": 96.0,
5327 "difficulty": 11.0,
5328 "level": 0.09090909090909091,
5329 "effort": 1056.0,
5330 "time": 58.666666666666664,
5331 "bugs": 0.03456644293839657
5332 }
5333 "#);
5334 },
5335 );
5336 }
5337
5338 /// Regression for #695. Lua/Bash/Tcl/iRules/PHP/Ruby/Elixir used to
5339 /// classify the *closing* delimiter (`)`/`]`/`}`) as a separate
5340 /// operator, while the C-family majority folds each balanced pair to a
5341 /// single glyph via `get_operator_id_as_str` and counts only the
5342 /// opener. A balanced `(1)` therefore double-counted as `()` + `)`,
5343 /// inflating n1 and N1. With the fix only the folded `(` opener counts:
5344 /// `local x = (1)` yields operators `local`, `=`, `()` — n1 = N1 = 3,
5345 /// with no standalone `)`.
5346 #[cfg(feature = "lua")]
5347 #[test]
5348 fn lua_balanced_paren_counts_opener_only() {
5349 let source = "local x = (1)\n";
5350 let path = PathBuf::from("foo.lua");
5351 let parser = LuaParser::new(source.as_bytes().to_vec(), &path, None);
5352 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
5353 let paren = ops.operators.iter().filter(|o| o.as_str() == "()").count();
5354 assert_eq!(
5355 paren, 1,
5356 "balanced `(1)` must be one `()` operator; operators were {:?}",
5357 ops.operators
5358 );
5359 assert!(
5360 !ops.operators.iter().any(|o| o.as_str() == ")"),
5361 "the closing `)` must not be a separate operator; operators were {:?}",
5362 ops.operators
5363 );
5364 }
5365
5366 /// Guard for #768. Several `get_op_type` impls (Cpp/C/Objc/Mozcpp/
5367 /// Tcl/iRules/Php/Elixir/Ruby) classify a grammar's *second-alias*
5368 /// opener — `LPAREN2`, and for Elixir/Ruby `LBRACK2`/`LBRACK3` — as a
5369 /// Halstead operator alongside the base `LPAREN`/`LBRACK`. #768 worried
5370 /// that an alias opener would reach `compute_halstead` with a kind_id
5371 /// distinct from the base, inflating n1 (a second `()` entry) and
5372 /// rendering a bare `"("` instead of the folded `"()"`.
5373 ///
5374 /// That cannot happen: tree-sitter's runtime collapses each alias to
5375 /// its base via the grammar's `public_symbol_map` *before*
5376 /// `Node::kind_id()` (`ts_node_symbol`) ever returns. So the alias
5377 /// kind_id is unobservable to the metric layer and the alias match arms
5378 /// are defensive — they only fire if a future grammar bump drops that
5379 /// collapse. This test pins the invariant: parsing the exact
5380 /// constructs each grammar produces the alias for internally
5381 /// (pp-conditional `defined(...)` for Cpp; call arg-list / subscript /
5382 /// constant-array-pattern for Ruby) must yield **no** node carrying the
5383 /// alias kind_id, and the balanced opener must count once and render as
5384 /// the pair glyph. If a grammar bump makes an alias id observable, this
5385 /// goes red and signals that the alias arms must additionally fold to
5386 /// the base in `get_operator_id_as_str` (the fix #768 proposed).
5387 #[cfg(all(feature = "c", feature = "cpp", feature = "elixir", feature = "ruby"))]
5388 #[test]
5389 fn second_alias_opener_collapses_to_base_kind_id() {
5390 #[cfg(any(feature = "c", feature = "cpp", feature = "elixir", feature = "ruby"))]
5391 fn assert_no_alias<T: crate::ParserTrait>(
5392 source: &str,
5393 file: &str,
5394 alias_id: u16,
5395 alias_name: &str,
5396 ) {
5397 let path = PathBuf::from(file);
5398 let parser = T::new(source.as_bytes().to_vec(), &path, None);
5399 let mut stack = vec![parser.root()];
5400 while let Some(node) = stack.pop() {
5401 assert_ne!(
5402 node.kind_id(),
5403 alias_id,
5404 "{alias_name} (kind_id {alias_id}) must never reach kind_id() \
5405 for `{source}`; the runtime public_symbol_map should have \
5406 collapsed it to the base opener. If this fires after a \
5407 grammar bump, fold {alias_name} to its pair glyph in \
5408 get_operator_id_as_str (issue #768)."
5409 );
5410 for child in node.children() {
5411 stack.push(child);
5412 }
5413 }
5414 }
5415
5416 // Balanced openers must count once and render folded (no bare
5417 // `(`/`[`, no n1 inflation) — the property #768 feared was broken.
5418 #[cfg(any(feature = "c", feature = "cpp", feature = "elixir", feature = "ruby"))]
5419 fn assert_folded_openers<T: crate::MetricSuite>(source: &str, file: &str) {
5420 let path = PathBuf::from(file);
5421 let parser = T::new(source.as_bytes().to_vec(), &path, None);
5422 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
5423 assert!(
5424 !ops.operators.iter().any(|o| o.as_str() == "("),
5425 "no bare `(` operator (must fold to `()`); operators were {:?}",
5426 ops.operators
5427 );
5428 assert!(
5429 !ops.operators.iter().any(|o| o.as_str() == "["),
5430 "no bare `[` operator (must fold to `[]`); operators were {:?}",
5431 ops.operators
5432 );
5433 // Each pair glyph appears at most once — the alias does not add
5434 // a second `()`/`[]` entry to n1.
5435 assert!(
5436 ops.operators.iter().filter(|o| o.as_str() == "()").count() <= 1,
5437 "`()` must be a single n1 entry; operators were {:?}",
5438 ops.operators
5439 );
5440 assert!(
5441 ops.operators.iter().filter(|o| o.as_str() == "[]").count() <= 1,
5442 "`[]` must be a single n1 entry; operators were {:?}",
5443 ops.operators
5444 );
5445 }
5446
5447 // Cpp/C/Mozcpp: LPAREN2 = 20. The grammar emits it internally only
5448 // inside preprocessor-conditional expressions (`#if defined(FOO)`).
5449 assert_no_alias::<crate::CppParser>(
5450 "#if defined(FOO)\n#endif\n",
5451 "a.cpp",
5452 20,
5453 "Cpp::LPAREN2",
5454 );
5455 assert_no_alias::<crate::CParser>("#if defined(FOO)\n#endif\n", "a.c", 20, "C::LPAREN2");
5456
5457 // Ruby: LPAREN2 = 47 (call arg-list), LBRACK3 = 155 (element-
5458 // reference subscript), LBRACK2 = 46 (constant array pattern).
5459 assert_no_alias::<crate::RubyParser>("f(1)\n", "a.rb", 47, "Ruby::LPAREN2");
5460 assert_no_alias::<crate::RubyParser>("a[0]\n", "a.rb", 155, "Ruby::LBRACK3");
5461 assert_no_alias::<crate::RubyParser>(
5462 "case p\nin Point[1, 2] then 1\nend\n",
5463 "a.rb",
5464 46,
5465 "Ruby::LBRACK2",
5466 );
5467
5468 // Elixir: LPAREN2 = 95 (immediate call paren), LBRACK2 = 96
5469 // (access / subscript).
5470 assert_no_alias::<crate::ElixirParser>("f(1)\n", "a.ex", 95, "Elixir::LPAREN2");
5471 assert_no_alias::<crate::ElixirParser>("x[0]\n", "a.ex", 96, "Elixir::LBRACK2");
5472
5473 assert_folded_openers::<crate::CppParser>("int main(){ int a[3]; return a[0]; }", "b.cpp");
5474 assert_folded_openers::<crate::RubyParser>("f(1)\nb = [1]\nb[0]\n", "b.rb");
5475 }
5476
5477 #[cfg(feature = "kotlin")]
5478 #[test]
5479 fn kotlin_halstead_basic() {
5480 check_metrics::<KotlinParser>(
5481 "fun add(a: Int, b: Int): Int {
5482 val result = a + b
5483 return result
5484 }",
5485 "foo.kt",
5486 |metric| {
5487 insta::assert_json_snapshot!(
5488 metric.halstead,
5489 @r#"
5490 {
5491 "unique_operators": 9,
5492 "total_operators": 11,
5493 "unique_operands": 5,
5494 "total_operands": 10,
5495 "length": 21,
5496 "estimated_program_length": 40.13896548741762,
5497 "purity_ratio": 1.9113793089246487,
5498 "vocabulary": 14,
5499 "volume": 79.9544533632097,
5500 "difficulty": 9.0,
5501 "level": 0.1111111111111111,
5502 "effort": 719.5900802688873,
5503 "time": 39.97722668160485,
5504 "bugs": 0.026767153565498338
5505 }
5506 "#
5507 );
5508 },
5509 );
5510 }
5511
5512 #[cfg(feature = "kotlin")]
5513 #[test]
5514 fn kotlin_string_template_no_double_count() {
5515 // Re-anchored for issue #454. The pre-#454 comment claimed
5516 // kotlin-ng emits an `identifier` node for the short `$name`
5517 // form whose bytes include the leading `$`. That is factually
5518 // false: AST dump shows the short form produces bare
5519 // `string_content` tokens (`$`, then `name`) with **no**
5520 // structured node. The old assertion (u_operands = 4, N2 = 5)
5521 // passed for the wrong reason (lesson 6): the wrapping literal
5522 // was counted (+1) and the inner `name` was dropped (-1), and
5523 // the two errors cancelled. The `$name!` it used also defeats
5524 // recovery because the grammar glues the trailing `!` onto the
5525 // name token.
5526 //
5527 // Correct mechanism (clean end-of-segment short form):
5528 // `fun greet(name: String): String {\n return "Hi $name"\n}\n`
5529 // operators: fun, (, ), :, {}, return → as classified.
5530 // operands by token text:
5531 // `greet` × 1, `name` × 2 (param + recovered short-interp),
5532 // `String` × 2 (param type + return type).
5533 // The wrapping `"Hi $name"` literal is suppressed and the
5534 // inner `name` recovered → u_operands = 3 (`greet`, `name`,
5535 // `String`), N2 = 5. Pre-#454: wrapper counted, inner dropped
5536 // → u_operands = 4, N2 = 6.
5537 check_metrics::<KotlinParser>(
5538 "fun greet(name: String): String {\n return \"Hi $name\"\n}\n",
5539 "foo.kt",
5540 |metric| {
5541 assert_eq!(metric.halstead.unique_operands(), 3);
5542 assert_eq!(metric.halstead.total_operands(), 5);
5543 },
5544 );
5545 // Lesson 4: the ops store agrees on n2 and the exact operand set
5546 // (inner `name` present, wrapper absent).
5547 assert_ops_operands::<KotlinParser>(
5548 "fun greet(name: String): String {\n return \"Hi $name\"\n}\n",
5549 "foo.kt",
5550 3,
5551 vec!["greet", "name", "String"],
5552 );
5553 }
5554
5555 #[cfg(feature = "kotlin")]
5556 #[test]
5557 fn kotlin_short_interpolation_counts_inner_not_wrapper() {
5558 // Issue #454: the short `$name` template — distinct from the
5559 // long `${expr}` form, which the kotlin-ng grammar gives a
5560 // structured `interpolation` node (see
5561 // `kotlin_string_template_long_form_no_double_count`). The short
5562 // form has no such node; the variable arrives as a bare
5563 // `string_content` token preceded by a `$` `string_content`.
5564 // The fix recovers the clean-identifier variable as an operand
5565 // and suppresses the opaque wrapper.
5566 //
5567 // `fun f() { val x = 1; println("v=$x") }\n`
5568 // operands by token text: `f`, `x` × 2 (decl + recovered),
5569 // `println`, `1`. The wrapping `"v=$x"` is suppressed →
5570 // u_operands = 4 (`f`, `x`, `println`, `1`), N2 = 5.
5571 // Pre-#454 the wrapper `"v=$x"` counted and the inner `x` was
5572 // dropped → u_operands = 4 but the wrapper, not `x`, was the
5573 // fourth operand, and N2 = 5 with the wrong member — the ops
5574 // assertion below pins the exact set so the cancellation cannot
5575 // hide it.
5576 let src = "fun f() { val x = 1; println(\"v=$x\") }\n";
5577 check_metrics::<KotlinParser>(src, "foo.kt", |metric| {
5578 assert_eq!(metric.halstead.unique_operands(), 4);
5579 assert_eq!(metric.halstead.total_operands(), 5);
5580 });
5581 assert_ops_operands::<KotlinParser>(src, "foo.kt", 4, vec!["f", "x", "println", "1"]);
5582 }
5583
5584 #[cfg(feature = "kotlin")]
5585 #[test]
5586 fn kotlin_short_interpolation_space_separated() {
5587 // Issue #454 follow-up: tree-sitter-kotlin-ng splits the literal
5588 // only at each `$`, so a `$name` segment's name token absorbs any
5589 // trailing inter-segment text into its byte range. For `"$a $b"`
5590 // the token after the first `$` is `"a "` (with the trailing
5591 // space). Pre-fix `kotlin_is_identifier("a ")` returned false and
5592 // the leading variable `a` was silently dropped, yielding
5593 // operands `{b, f, s}` (verified: `a` missing) — breaking parity
5594 // with the long form `"${a} ${b}"`, which recovers `{a, b, f, s}`.
5595 //
5596 // The fix takes the maximal leading-identifier prefix of the name
5597 // token, recovering `a` and keying it as the bare `"a"` (not
5598 // `"a "`). Short and long forms must now agree exactly.
5599 //
5600 // `fun f() { val s = "$a $b" }\n`
5601 // operands by token text: `f`, `s`, `a` (recovered), `b`
5602 // (recovered). Wrapper suppressed → u_operands = 4, N2 = 4.
5603 let short = "fun f() { val s = \"$a $b\" }\n";
5604 let long = "fun f() { val s = \"${a} ${b}\" }\n";
5605 check_metrics::<KotlinParser>(short, "foo.kt", |metric| {
5606 assert_eq!(metric.halstead.unique_operands(), 4);
5607 assert_eq!(metric.halstead.total_operands(), 4);
5608 });
5609 // Both `a` and `b` present, wrapper absent, n2 == dedupe(operands).
5610 assert_ops_operands::<KotlinParser>(short, "foo.kt", 4, vec!["f", "s", "a", "b"]);
5611 // Exact parity with the long `${a} ${b}` form.
5612 assert_ops_operands::<KotlinParser>(long, "foo.kt", 4, vec!["f", "s", "a", "b"]);
5613
5614 // Comma after the name (`"$a, $b"`): the first name token is
5615 // `"a, "`; its leading identifier prefix is `a`.
5616 let comma = "fun f() { val s = \"$a, $b\" }\n";
5617 assert_ops_operands::<KotlinParser>(comma, "foo.kt", 4, vec!["f", "s", "a", "b"]);
5618
5619 // Name preceded by literal text and at end-of-segment (`"x=$a"`):
5620 // the `a` token has no trailing text, so recovery is unchanged.
5621 let prefixed = "fun f() { val s = \"x=$a\" }\n";
5622 assert_ops_operands::<KotlinParser>(prefixed, "foo.kt", 3, vec!["f", "s", "a"]);
5623
5624 // Mid-prose `"$x is "`: the name token is `"x is "`. The leading
5625 // identifier prefix is `x`, matching the long form `"${x} is "`,
5626 // which also recovers `x` and treats `" is "` as literal text.
5627 let prose_short = "fun f() { val s = \"$x is \" }\n";
5628 let prose_long = "fun f() { val s = \"${x} is \" }\n";
5629 assert_ops_operands::<KotlinParser>(prose_short, "foo.kt", 3, vec!["f", "s", "x"]);
5630 assert_ops_operands::<KotlinParser>(prose_long, "foo.kt", 3, vec!["f", "s", "x"]);
5631 }
5632
5633 #[cfg(feature = "kotlin")]
5634 #[test]
5635 fn kotlin_dollar_non_identifier_stays_literal() {
5636 // Issue #454 boundary: a `$` not followed by a clean identifier
5637 // is literal text, not an interpolation. `"price: $5"` (digit
5638 // after `$`) must keep the wrapping literal as a single operand
5639 // and recover nothing.
5640 //
5641 // `fun f() { val a = "price: $5" }\n`
5642 // operands: `f`, `a`, `"price: $5"` → u_operands = 3, N2 = 3.
5643 let src = "fun f() { val a = \"price: $5\" }\n";
5644 check_metrics::<KotlinParser>(src, "foo.kt", |metric| {
5645 assert_eq!(metric.halstead.unique_operands(), 3);
5646 assert_eq!(metric.halstead.total_operands(), 3);
5647 });
5648 assert_ops_operands::<KotlinParser>(src, "foo.kt", 3, vec!["f", "a", "\"price: $5\""]);
5649 }
5650
5651 #[cfg(feature = "kotlin")]
5652 #[test]
5653 fn kotlin_string_template_long_form_no_double_count() {
5654 // The `${expr}` long form of a Kotlin string template also
5655 // produces an `Interpolation` child. The fix must apply to it
5656 // identically.
5657 //
5658 // Source: `fun f(x: Int): String { return "v=${x}" }\n`
5659 // Operands by source-byte key:
5660 // `f` × 1, `x` × 2 (param + inside `${x}`),
5661 // `Int` × 1, `String` × 1.
5662 // With the fix u_operands = 4 (`f`, `x`, `Int`, `String`),
5663 // N2 = 5. Without the fix the wrapping `"v=${x}"` would also
5664 // count → u_operands = 5, N2 = 6.
5665 check_metrics::<KotlinParser>(
5666 "fun f(x: Int): String { return \"v=${x}\" }\n",
5667 "foo.kt",
5668 |metric| {
5669 assert_eq!(metric.halstead.unique_operands(), 4);
5670 assert_eq!(metric.halstead.total_operands(), 5);
5671 },
5672 );
5673 }
5674
5675 #[cfg(feature = "kotlin")]
5676 #[test]
5677 fn kotlin_plain_string_still_operand() {
5678 // The fix for #191 only skips wrapping templates that contain
5679 // an `Interpolation` child; a plain `"hello"` (no `$` interp)
5680 // must still contribute exactly one operand.
5681 //
5682 // Source: `fun f(): String { return "hello" }\n`
5683 // Operands: `f` × 1, `String` × 1, `"hello"` × 1 →
5684 // u_operands = 3, N2 = 3.
5685 check_metrics::<KotlinParser>(
5686 "fun f(): String { return \"hello\" }\n",
5687 "foo.kt",
5688 |metric| {
5689 assert_eq!(metric.halstead.unique_operands(), 3);
5690 assert_eq!(metric.halstead.total_operands(), 3);
5691 },
5692 );
5693 }
5694
5695 // Kotlin half of #1380 — see
5696 // `java_self_and_super_references_are_operands` for the structural
5697 // argument.
5698 #[cfg(feature = "kotlin")]
5699 #[test]
5700 fn kotlin_self_and_super_references_are_operands() {
5701 let source = "class T {\n fun f() = this.x + super.y\n}";
5702 // Operators (n1=7, N1=8): class, {}, fun, (), =, . x2, +
5703 // Operands (n2=6, N2=6): T, f, this, x, super, y
5704 // Before the fix this read (9, 10, 4, 4).
5705 assert_halstead_counts::<KotlinParser>(
5706 source,
5707 "foo.kt",
5708 [7, 8, 6, 6],
5709 "kotlin self/super references",
5710 );
5711 assert_keywords_are_operands_only::<KotlinParser>(source, "foo.kt", &["this", "super"]);
5712 }
5713
5714 // The label-qualified spellings are their own leaf kinds — `this@`
5715 // (106) and `super@` (107) — carrying no `this` / `super` leaf, so
5716 // listing only the two bare kinds would score every `this@Outer`
5717 // reference zero (grammar-dispatch section 1). The labels `Outer`
5718 // and `Inner` stay separate operands: the `this_expression` wrapper
5719 // whose span would have swallowed them is deliberately unclassified
5720 // (section 5).
5721 #[cfg(feature = "kotlin")]
5722 #[test]
5723 fn kotlin_labelled_self_and_super_references_are_operands() {
5724 let source = "class Outer {\n inner class Inner : A() {\n \
5725 fun f() = this@Outer.x + super@Inner.y\n }\n}";
5726 assert_keywords_are_operands_only::<KotlinParser>(source, "foo.kt", &["this@", "super@"]);
5727 assert_ops_operands::<KotlinParser>(
5728 source,
5729 "foo.kt",
5730 8,
5731 vec!["A", "Inner", "Outer", "f", "super@", "this@", "x", "y"],
5732 );
5733 // The metrics store is a second, independent walk — it keys
5734 // operands by `get_operand_id` where `ops_inner` keys by text —
5735 // so `assert_ops_operands` on its own pins the ops store against
5736 // a literal rather than against the metric (lesson 4).
5737 //
5738 // `N2` is what makes the "the labels stay separate operands"
5739 // claim above testable. Both labels duplicate an enclosing class
5740 // name, so dropping their contribution entirely leaves `n2` at 8
5741 // with the same eight strings; only `N2` falls, 10 to 8.
5742 check_metrics::<KotlinParser>(source, "foo.kt", |metric| {
5743 assert_eq!(metric.halstead.unique_operands(), 8);
5744 assert_eq!(metric.halstead.total_operands(), 10);
5745 });
5746 }
5747
5748 // The keeper question of grammar-dispatch section 6: a
5749 // `constructor_delegation_call` emits a bare `this` / `super` leaf
5750 // with no `this_expression` / `super_expression` wrapper around it,
5751 // so classifying the wrapper instead would score constructor
5752 // delegation zero. This is the only spelling that can tell the two
5753 // choices apart — every other `this` carries both nodes — so it is
5754 // the independent path grammar-dispatch section 11 asks for.
5755 #[cfg(feature = "kotlin")]
5756 #[test]
5757 fn kotlin_constructor_delegation_self_reference_is_an_operand() {
5758 let source = "class C(val n: Int) {\n constructor() : this(0)\n}";
5759 assert_keywords_are_operands_only::<KotlinParser>(source, "foo.kt", &["this"]);
5760 assert_ops_operands::<KotlinParser>(
5761 source,
5762 "foo.kt",
5763 5,
5764 vec!["0", "C", "Int", "n", "this"],
5765 );
5766 check_metrics::<KotlinParser>(source, "foo.kt", |metric| {
5767 assert_eq!(metric.halstead.unique_operands(), 5);
5768 assert_eq!(metric.halstead.total_operands(), 5);
5769 });
5770 }
5771
5772 /// One `this` / `super` per container the pinned
5773 /// `tree-sitter-kotlin-ng` can put them in: a bare expression, a
5774 /// call argument, a navigation receiver, a type-argument-qualified
5775 /// `super<P>`, both label-qualified forms, and the
5776 /// `constructor_delegation_call` that carries no wrapper.
5777 #[cfg(feature = "kotlin")]
5778 const KOTLIN_SELF_POSITIONS: &str = "class P {
5779 fun h() = 1
5780 }
5781 class Outer : P() {
5782 val x = 1
5783 fun c() = super.h()
5784 fun d() = this
5785 fun e(o: Any): Any = e(this)
5786 inner class Inner : P() {
5787 fun a() = this@Outer.x
5788 fun b() = this.hashCode()
5789 fun f() = super@Inner.h()
5790 fun g() = super<P>.h()
5791 }
5792 }
5793 class Del(val n: Int) {
5794 constructor() : this(0)
5795 }
5796";
5797
5798 #[cfg(feature = "kotlin")]
5799 #[test]
5800 fn kotlin_self_and_super_leaves_are_unaliased_and_unconditional() {
5801 assert_self_reference_leaves::<KotlinCode>(
5802 KOTLIN_SELF_POSITIONS,
5803 &[
5804 ("this", Kotlin::This as u16, 4),
5805 ("super", Kotlin::Super as u16, 2),
5806 ("this@", Kotlin::ThisAT as u16, 1),
5807 ("super@", Kotlin::SuperAT as u16, 1),
5808 ],
5809 "kotlin",
5810 );
5811 }
5812
5813 #[cfg(feature = "python")]
5814 #[test]
5815 fn python_fstring_no_double_count() {
5816 // Regression: issue #191. A Python f-string (`f"Hi {name}!"`)
5817 // wraps an `Interpolation` child whose inner identifier
5818 // `name` is walked and counted as its own operand. Without
5819 // the `is_child(Interpolation)` guard the wrapping `String`
5820 // would also count, double-counting `name`'s contribution to
5821 // `N2`. Same pattern as #180 (Bash/Elixir) and #184 (PHP).
5822 //
5823 // Source: `def greet(name):\n return f"Hi {name}!"\n`
5824 // Operands by source-byte key:
5825 // `greet` × 1, `name` × 2 (param + inside `{name}`).
5826 // With the fix the wrapping `f"Hi {name}!"` is skipped →
5827 // u_operands = 2 (`greet`, `name`), N2 = 3. Without the fix
5828 // the wrapping literal would also count → u_operands = 3,
5829 // N2 = 4.
5830 check_metrics::<PythonParser>(
5831 "def greet(name):\n return f\"Hi {name}!\"\n",
5832 "foo.py",
5833 |metric| {
5834 assert_eq!(metric.halstead.unique_operands(), 2);
5835 assert_eq!(metric.halstead.total_operands(), 3);
5836 },
5837 );
5838 }
5839
5840 #[cfg(feature = "python")]
5841 #[test]
5842 fn python_plain_string_still_operand() {
5843 // The fix for #191 only skips wrapping `String` nodes that
5844 // contain an `Interpolation` child; a plain `"hi"` must still
5845 // contribute exactly one operand.
5846 //
5847 // Source: `def f():\n return "hi"\n`
5848 // Operands: `f` × 1, `"hi"` × 1 → u_operands = 2, N2 = 2.
5849 // (The previous documentation-string filter is preserved:
5850 // a bare `"hi"` as a top-level `expression_statement` would
5851 // be skipped, but here it appears as `return "hi"`.)
5852 check_metrics::<PythonParser>("def f():\n return \"hi\"\n", "foo.py", |metric| {
5853 assert_eq!(metric.halstead.unique_operands(), 2);
5854 assert_eq!(metric.halstead.total_operands(), 2);
5855 });
5856 }
5857
5858 #[cfg(feature = "python")]
5859 #[test]
5860 fn python_concatenated_docstring_suppressed() {
5861 // Regression for #695. An implicit-concatenation docstring
5862 // (`"""doc""" "more"`) parses as `expression_statement >
5863 // concatenated_string > [string, string]`. The single-literal
5864 // docstring guard (`parent == expression_statement &&
5865 // child_count == 1`) never fired here, so each fragment counted
5866 // as a separate operand and the docstring's N2 contribution
5867 // depended on how many literals it was split into. With the fix,
5868 // every fragment of such a docstring is suppressed.
5869 //
5870 // Source: `def f():\n """doc""" "more"\n return 1\n`
5871 // Operands: `f`, `1` only — both docstring fragments suppressed →
5872 // u_operands = 2, N2 = 2.
5873 check_metrics::<PythonParser>(
5874 "def f():\n \"\"\"doc\"\"\" \"more\"\n return 1\n",
5875 "foo.py",
5876 |metric| {
5877 assert_eq!(metric.halstead.unique_operands(), 2);
5878 assert_eq!(metric.halstead.total_operands(), 2);
5879 },
5880 );
5881 }
5882
5883 #[cfg(feature = "python")]
5884 #[test]
5885 fn python_concatenated_non_docstring_still_counts() {
5886 // The #695 fix must only suppress concatenated literals in the
5887 // *docstring* position (sole child of an `expression_statement`).
5888 // A concatenated string used as a value (`x = "a" "b"`) is not a
5889 // docstring — its `concatenated_string` parent's grandparent is
5890 // an assignment, not a single-child statement — so both fragments
5891 // must still be operands.
5892 //
5893 // Source: `def f():\n x = "a" "b"\n return x\n`
5894 // Operands: `f`, `x` (twice: assign + return), `"a"`, `"b"` →
5895 // u_operands = 4, N2 = 5.
5896 check_metrics::<PythonParser>(
5897 "def f():\n x = \"a\" \"b\"\n return x\n",
5898 "foo.py",
5899 |metric| {
5900 assert_eq!(metric.halstead.unique_operands(), 4);
5901 assert_eq!(metric.halstead.total_operands(), 5);
5902 },
5903 );
5904 }
5905
5906 #[cfg(feature = "python")]
5907 #[test]
5908 fn python_bare_tuple_of_strings_is_not_a_docstring() {
5909 // `"a", "b"` as a statement is a discarded tuple with two
5910 // expression children, so the docstring predicate's
5911 // `child_count == 1` gate keeps both literals as operands.
5912 //
5913 // Source: `def f():\n "a", "b"\n return 1\n`
5914 // Operands: `f`, `"a"`, `"b"`, `1` → u_operands = 4, N2 = 4.
5915 check_metrics::<PythonParser>(
5916 "def f():\n \"a\", \"b\"\n return 1\n",
5917 "foo.py",
5918 |metric| {
5919 assert_eq!(metric.halstead.unique_operands(), 4);
5920 assert_eq!(metric.halstead.total_operands(), 4);
5921 },
5922 );
5923 }
5924
5925 #[cfg(feature = "python")]
5926 #[test]
5927 fn python_parenthesized_docstring_suppressed() {
5928 // CPython drops the parentheses around a docstring (#1610), so
5929 // `("""doc""")` must bill what `"""doc"""` bills: neither the
5930 // string nor its `(`. Operators: `def`, `(` of the parameter list,
5931 // `:`, `return`; operands: `f`, `1`.
5932 let sources: [&[u8]; 4] = [
5933 b"def f():\n \"\"\"doc\"\"\"\n return 1\n",
5934 b"def f():\n (\"\"\"doc\"\"\")\n return 1\n",
5935 b"def f():\n ((\"doc\"))\n return 1\n",
5936 b"def f():\n (\n # c\n \"doc\" \"more\"\n )\n return 1\n",
5937 ];
5938 for source in sources {
5939 let h = crate::test_support::metrics_verbatim(
5940 crate::LANG::Python,
5941 source,
5942 crate::MetricsOptions::default(),
5943 )
5944 .halstead;
5945 assert_eq!(
5946 [
5947 h.unique_operators(),
5948 h.total_operators(),
5949 h.unique_operands(),
5950 h.total_operands()
5951 ],
5952 [4, 4, 2, 2],
5953 "{}",
5954 String::from_utf8_lossy(source)
5955 );
5956 }
5957 // A parenthesized value keeps its `(` and its operand, and so does
5958 // a parenthesized name statement. Operators: `=`, `(`; operands:
5959 // `x`, `"a"`.
5960 check_metrics::<PythonParser>("x = (\"a\")\n", "foo.py", |metric| {
5961 let h = &metric.halstead;
5962 assert_eq!([h.total_operators(), h.total_operands()], [2, 2]);
5963 });
5964 // Operators: `(`; operands: `x`.
5965 check_metrics::<PythonParser>("(x)\n", "foo.py", |metric| {
5966 let h = &metric.halstead;
5967 assert_eq!([h.total_operators(), h.total_operands()], [1, 1]);
5968 });
5969 // A tuple led by a parenthesized string is a value, not a bare
5970 // string. Operators: `(`, `,`; operands: `"a"`, `"b"`.
5971 check_metrics::<PythonParser>("(\"a\"), \"b\"\n", "foo.py", |metric| {
5972 let h = &metric.halstead;
5973 assert_eq!([h.total_operators(), h.total_operands()], [2, 2]);
5974 });
5975 }
5976
5977 #[cfg(feature = "python")]
5978 #[test]
5979 fn python_empty_file_halstead() {
5980 check_metrics::<PythonParser>("", "empty.py", |metric| {
5981 let h = &metric.halstead;
5982 assert_eq!(h.unique_operators(), 0);
5983 assert_eq!(h.total_operands(), 0);
5984 assert_eq!(h.estimated_program_length(), 0.0);
5985 assert_eq!(h.purity_ratio(), 0.0);
5986 assert_eq!(h.volume(), 0.0);
5987 assert_eq!(h.difficulty(), 0.0);
5988 assert_eq!(h.level(), 0.0);
5989 assert_eq!(h.effort(), 0.0);
5990 assert_eq!(h.time(), 0.0);
5991 assert_eq!(h.bugs(), 0.0);
5992 });
5993 }
5994
5995 /// Regression #413, sub-fix (1): `await` was double-counted because the
5996 /// operator arm listed both the await-expression node (Await=237) and the
5997 /// nested `await` keyword token (Await2=95). Only the node should count,
5998 /// mirroring how `yield` counts only the Yield node.
5999 #[cfg(feature = "python")]
6000 #[test]
6001 fn python_await_counted_once_per_use() {
6002 check_metrics::<PythonParser>(
6003 "async def f():\n await a()\n await b()\n await c()\n",
6004 "foo.py",
6005 |metric| {
6006 // expected operators: async, def, (), :, await (5 unique)
6007 // N1: async(1) def(1) await(3) = 5, plus the four call
6008 // and parameter-list `(` and the block `:` (#1486) = 10.
6009 // Before #413, Await + Await2 both matched, so `await` was a
6010 // distinct operator twice: one more unique and three more total.
6011 assert_eq!(metric.halstead.unique_operators(), 5);
6012 assert_eq!(metric.halstead.total_operators(), 10);
6013 },
6014 );
6015 }
6016
6017 /// Regression #413, sub-fix (3): `lambda` was dropped entirely. Only the
6018 /// `lambda` keyword token (Lambda3=73) is classified, not the wrapping
6019 /// Lambda/Lambda2 expression nodes, to avoid an await-style double count.
6020 #[cfg(feature = "python")]
6021 #[test]
6022 fn python_lambda_counted_once() {
6023 check_metrics::<PythonParser>("g = lambda x: x + 1\n", "foo.py", |metric| {
6024 // expected operators: =, lambda, :, + (4 unique, each used
6025 // once; the parameter-list `:` since #1486).
6026 // Before #413, lambda was absent: only =, + were counted.
6027 assert_eq!(metric.halstead.unique_operators(), 4);
6028 assert_eq!(metric.halstead.total_operators(), 4);
6029 });
6030 }
6031
6032 /// Regression #413, sub-fix (2): `match` / `case` keyword tokens
6033 /// (Match=26, Case=27) were dropped. Each should now count as an operator,
6034 /// matching the cyclomatic metric which already counts every `case`.
6035 #[cfg(feature = "python")]
6036 #[test]
6037 fn python_match_case_counted() {
6038 check_metrics::<PythonParser>(
6039 "match x:\n case 1:\n pass\n case _:\n pass\n",
6040 "foo.py",
6041 |metric| {
6042 // expected operators: match, case, pass, : (4 unique)
6043 // match(1) + case(2) + pass(2) + one block `:` per
6044 // header (3, #1486) = 8 total occurrences.
6045 // Before #413, neither match nor case was counted (only pass).
6046 assert_eq!(metric.halstead.unique_operators(), 4);
6047 assert_eq!(metric.halstead.total_operators(), 8);
6048 },
6049 );
6050 }
6051
6052 /// Regression #413, sub-fix (2): `nonlocal` (Nonlocal=41) was dropped while
6053 /// `global` was already classified. Both should count, for parity.
6054 #[cfg(feature = "python")]
6055 #[test]
6056 fn python_nonlocal_and_global_counted() {
6057 check_metrics::<PythonParser>(
6058 "def f():\n global a\n nonlocal b\n",
6059 "foo.py",
6060 |metric| {
6061 // expected operators: def, (), :, global, nonlocal
6062 // (5 unique, each once; `()` and `:` since #1486)
6063 // Before #413, nonlocal was absent: only def, global counted.
6064 assert_eq!(metric.halstead.unique_operators(), 5);
6065 assert_eq!(metric.halstead.total_operators(), 5);
6066 },
6067 );
6068 }
6069
6070 /// Regression #413, sub-fix (4): `not in` (Notin=193) and `is not`
6071 /// (Isnot=194) are single compound operators. The parent-guard suppresses
6072 /// the inner Not/In/Is leaves only under those compounds, so standalone
6073 /// `not x`, `a in b`, `a is b`, and `for x in y` still count their leaves.
6074 #[cfg(feature = "python")]
6075 #[test]
6076 fn python_not_in_is_not_counted_as_single_operator() {
6077 check_metrics::<PythonParser>(
6078 "a not in b\na is not b\nnot c\nd in e\nf is g\nfor h in i:\n pass\n",
6079 "foo.py",
6080 |metric| {
6081 // expected operators (8 unique):
6082 // "not in" (compound, once), "is not" (compound, once),
6083 // "not" (standalone `not c`, once),
6084 // "in" (standalone `d in e` + `for h in i` = twice),
6085 // "is" (standalone `f is g`, once),
6086 // "for" (once), "pass" (once), and the `for` header's
6087 // block ":" (once, #1486)
6088 // Total occurrences: 1+1+1+2+1+1+1+1 = 9.
6089 // Before #413, `a not in b` counted not+in (two) and
6090 // `a is not b` counted is+not (two); the compounds were
6091 // never classified.
6092 assert_eq!(metric.halstead.unique_operators(), 8);
6093 assert_eq!(metric.halstead.total_operators(), 9);
6094 },
6095 );
6096 }
6097
6098 /// Asserts `count(halstead)` for each `(label, source, expected)`
6099 /// row, one parse per row so a row's counts are its own.
6100 #[cfg(any(
6101 feature = "c",
6102 feature = "cpp",
6103 feature = "groovy",
6104 feature = "java",
6105 feature = "javascript",
6106 feature = "kotlin",
6107 feature = "mozcpp",
6108 feature = "mozjs",
6109 feature = "objc",
6110 feature = "python",
6111 feature = "rust",
6112 feature = "typescript"
6113 ))]
6114 fn assert_halstead_rows<T: crate::MetricSuite>(
6115 file: &str,
6116 rows: &[(&str, &str, [u64; 2])],
6117 count: fn(&Stats) -> [u64; 2],
6118 ) {
6119 for (label, source, expected) in rows {
6120 crate::test_support::check_func_space_only::<T, _>(
6121 source,
6122 file,
6123 &[crate::Metric::Halstead],
6124 |space| {
6125 assert_eq!(
6126 count(&space.metrics.halstead),
6127 *expected,
6128 "{label}: {source:?}"
6129 );
6130 },
6131 );
6132 }
6133 }
6134
6135 /// Asserts `[n1, N1]` for each row.
6136 #[cfg(any(
6137 feature = "c",
6138 feature = "cpp",
6139 feature = "groovy",
6140 feature = "java",
6141 feature = "javascript",
6142 feature = "kotlin",
6143 feature = "mozcpp",
6144 feature = "mozjs",
6145 feature = "objc",
6146 feature = "python",
6147 feature = "rust",
6148 feature = "typescript"
6149 ))]
6150 fn assert_operator_rows<T: crate::MetricSuite>(file: &str, rows: &[(&str, &str, [u64; 2])]) {
6151 assert_halstead_rows::<T>(file, rows, |h| [h.unique_operators(), h.total_operators()]);
6152 }
6153
6154 /// Asserts `[n2, N2]` for each row.
6155 #[cfg(any(feature = "java", feature = "typescript"))]
6156 fn assert_operand_rows<T: crate::MetricSuite>(file: &str, rows: &[(&str, &str, [u64; 2])]) {
6157 assert_halstead_rows::<T>(file, rows, |h| [h.unique_operands(), h.total_operands()]);
6158 }
6159
6160 /// #1486: Python's bracket openers, `;` and `:` are vocabulary, as
6161 /// they are in every other getter (#1395). Each row isolates one
6162 /// position, so the expected `[n1, N1]` is derivable from the row
6163 /// alone; a bracket pair bills its opener once, as `()` / `[]` / `{}`.
6164 #[cfg(feature = "python")]
6165 #[test]
6166 fn python_punctuation_is_vocabulary_1486() {
6167 const ROWS: &[(&str, &str, [u64; 2])] = &[
6168 // `()`
6169 ("call", "f(x)\n", [1, 1]),
6170 // `()`, `=`
6171 ("keyword argument", "f(a=1)\n", [2, 2]),
6172 // `()`, `*`, `,`, `**`
6173 ("splat arguments", "f(*a, **k)\n", [4, 4]),
6174 // `[]`
6175 ("index", "a[1]\n", [1, 1]),
6176 // `[]`, `:` twice
6177 ("slice", "a[1:2:3]\n", [2, 3]),
6178 // `{}`, `:`
6179 ("dict", "{k: v}\n", [2, 2]),
6180 // `{}`, `,`
6181 ("set", "{1, 2}\n", [2, 2]),
6182 // `[]`, `,` twice, `()`
6183 ("list and tuple", "[1, (2, 3)]\n", [3, 4]),
6184 // `()`, `for`, `in`
6185 ("generator", "(x for x in y)\n", [3, 3]),
6186 // `{}`, `:`, `for`, `in`
6187 ("dict comprehension", "{k: v for k in y}\n", [4, 4]),
6188 // `()`, `:=` — the walrus is its own token, not a `:`
6189 ("walrus", "(n := 3)\n", [2, 2]),
6190 // `lambda`, `:`
6191 ("lambda", "lambda x: x\n", [2, 2]),
6192 // `def`, `()`, `:` twice, `->`, `pass`
6193 ("annotations", "def f(x: int) -> int:\n pass\n", [5, 6]),
6194 // `:`, `=`
6195 ("annotated assignment", "x: int = 1\n", [2, 2]),
6196 // `@`, `def`, `()`, `:`, `pass`
6197 ("decorator", "@d\ndef f():\n pass\n", [5, 5]),
6198 // `if`, `:`, `pass`
6199 ("if block", "if x:\n pass\n", [3, 3]),
6200 // `while`, `:`, `pass`
6201 ("while block", "while x:\n pass\n", [3, 3]),
6202 // `for`, `in`, `:`, `pass`
6203 ("for block", "for i in y:\n pass\n", [4, 4]),
6204 // `with`, `:`, `pass`
6205 ("with block", "with o:\n pass\n", [3, 3]),
6206 // `try`, `except`, `:` twice, `pass` twice
6207 ("try block", "try:\n pass\nexcept E:\n pass\n", [4, 6]),
6208 // `class`, `:`, `pass` — the keyword is billed like `def`
6209 // (#1550)
6210 ("class block", "class C:\n pass\n", [3, 3]),
6211 // `class` twice, `()`, `:` twice, `pass` — one `class` per
6212 // keyword, nothing for the `ClassDefinition` wrapper
6213 (
6214 "nested class",
6215 "class C(B):\n class D:\n pass\n",
6216 [4, 6],
6217 ),
6218 // `;`
6219 ("statement separator", "a; b\n", [1, 1]),
6220 ];
6221 assert_operator_rows::<PythonParser>("foo.py", ROWS);
6222 }
6223
6224 /// #1486: an f-string replacement field's `{` and format-spec `:` are
6225 /// spelling, not operations, like every other language's
6226 /// interpolation opener (#1314) — the f-string rows bill nothing.
6227 /// #1551 adds the self-documenting `=` (`f"{z=}"`), a directive of
6228 /// the same field. The guard reads the token's *parent*, so a dict,
6229 /// a keyword argument or a walrus written inside a replacement field
6230 /// keeps its own operators.
6231 #[cfg(feature = "python")]
6232 #[test]
6233 fn python_f_string_delimiters_are_not_operators_1486() {
6234 const ROWS: &[(&str, &str, [u64; 2])] = &[
6235 ("interpolation opener", "f\"{x}\"\n", [0, 0]),
6236 ("format spec colon", "f\"{x:>10}\"\n", [0, 0]),
6237 ("nested format expression", "f\"{a:{w}}\"\n", [0, 0]),
6238 // `{}`, `:`, `[]` of the dict and its subscript, nothing of
6239 // the replacement field around them
6240 ("dict inside a field", "f\"{ {k: v}[k] }\"\n", [3, 3]),
6241 ("self-documenting =", "f\"{z=}\"\n", [0, 0]),
6242 // the `=` keeps its spaces in the output; `!r` and `:` follow it
6243 ("spaced = with conversion", "f\"{z = !r:>10}\"\n", [0, 0]),
6244 // a `=` in a nested format expression (Python 3.12+)
6245 ("nested self-documenting =", "f\"{a:{w=}}\"\n", [0, 0]),
6246 // `()`, `=` — a keyword argument's `=` is a real operator
6247 ("keyword argument inside a field", "f\"{g(a=1)}\"\n", [2, 2]),
6248 // `()`, `:=`
6249 ("walrus inside a field", "f\"{(y := 1)}\"\n", [2, 2]),
6250 ];
6251 assert_operator_rows::<PythonParser>("foo.py", ROWS);
6252 }
6253
6254 /// #1552: a JS-family `class` keyword bills as `function` does, once
6255 /// per keyword. Shared by the four grammars of the macro; each runs
6256 /// its own call so a grammar whose `Class2` stops being the keyword
6257 /// leaf fails alone. Before #1552 every row billed one fewer.
6258 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
6259 const JS_FAMILY_CLASS_ROWS: &[(&str, &str, [u64; 2])] = &[
6260 // `class`, `{}`
6261 ("declaration", "class C {}\n", [2, 2]),
6262 // `const`, `=`, `class`, `{}`, `;` — the class *expression*
6263 // node wraps the keyword and bills nothing of its own
6264 ("expression", "const D = class {};\n", [5, 5]),
6265 // `class` twice, `extends`, `{}` twice
6266 ("two classes", "class A {}\nclass B extends A {}\n", [3, 5]),
6267 ];
6268
6269 #[cfg(any(feature = "javascript", feature = "mozjs"))]
6270 #[test]
6271 fn js_class_keyword_is_an_operator_1552() {
6272 #[cfg(feature = "javascript")]
6273 assert_operator_rows::<JavascriptParser>("foo.js", JS_FAMILY_CLASS_ROWS);
6274 #[cfg(feature = "mozjs")]
6275 assert_operator_rows::<MozjsParser>("foo.js", JS_FAMILY_CLASS_ROWS);
6276 }
6277
6278 /// #1554: a function *expression* bills its `function` keyword once,
6279 /// as a declaration does — the `function_expression` wrapper bills
6280 /// nothing of its own. Before #1554 the plain and `async` rows billed
6281 /// one extra unique and total operator, and the nested row one extra
6282 /// unique and two extra totals. The declaration and generator rows
6283 /// are controls: neither node was ever in the operator arm.
6284 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
6285 const JS_FAMILY_FUNCTION_ROWS: &[(&str, &str, [u64; 2])] = &[
6286 // `function`, `()`, `{}`
6287 ("declaration", "function h() {}\n", [3, 3]),
6288 // `const`, `=`, `function`, `()`, `{}`, `;`
6289 ("expression", "const f = function () {};\n", [6, 6]),
6290 // `const`, `=`, `async`, `function`, `()`, `{}`, `;`
6291 (
6292 "async expression",
6293 "const f = async function () {};\n",
6294 [7, 7],
6295 ),
6296 // `const`, `=`, `function`, `*`, `()`, `{}`, `;` — a generator
6297 // expression is its own `generator_function` node, never billed
6298 (
6299 "generator expression",
6300 "const f = function* () {};\n",
6301 [7, 7],
6302 ),
6303 // `const`, `=`, `return`, `;` x2, and `function`, `()`, `{}`
6304 // twice each
6305 (
6306 "nested expressions",
6307 "const f = function () { return function () {}; };\n",
6308 [7, 11],
6309 ),
6310 ];
6311
6312 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
6313 #[test]
6314 fn js_family_function_expression_bills_function_once_1554() {
6315 #[cfg(feature = "javascript")]
6316 assert_operator_rows::<JavascriptParser>("foo.js", JS_FAMILY_FUNCTION_ROWS);
6317 #[cfg(feature = "mozjs")]
6318 assert_operator_rows::<MozjsParser>("foo.js", JS_FAMILY_FUNCTION_ROWS);
6319 #[cfg(feature = "typescript")]
6320 {
6321 assert_operator_rows::<TypescriptParser>("foo.ts", JS_FAMILY_FUNCTION_ROWS);
6322 assert_operator_rows::<TsxParser>("foo.tsx", JS_FAMILY_FUNCTION_ROWS);
6323 }
6324 }
6325
6326 /// #1552 for the two TypeScript grammars: the shared `class` rows,
6327 /// plus their own `interface` and `enum` declaration keywords.
6328 #[cfg(feature = "typescript")]
6329 #[test]
6330 fn typescript_declaration_keywords_are_operators_1552() {
6331 const ROWS: &[(&str, &str, [u64; 2])] = &[
6332 // `interface`, `{}`
6333 ("interface", "interface I {}\n", [2, 2]),
6334 // `enum`, `{}`
6335 ("enum", "enum E { A }\n", [2, 2]),
6336 ];
6337 assert_operator_rows::<TypescriptParser>("foo.ts", JS_FAMILY_CLASS_ROWS);
6338 assert_operator_rows::<TypescriptParser>("foo.ts", ROWS);
6339 assert_operator_rows::<TsxParser>("foo.tsx", JS_FAMILY_CLASS_ROWS);
6340 assert_operator_rows::<TsxParser>("foo.tsx", ROWS);
6341 }
6342
6343 /// #1552: Java's type-declaration keywords are operators, as C#'s
6344 /// are. The class-literal row pins the guard: the `class` tail of
6345 /// `C.class` is an operand (#1566), not a declaration keyword.
6346 #[cfg(feature = "java")]
6347 #[test]
6348 fn java_declaration_keywords_are_operators_1552() {
6349 const ROWS: &[(&str, &str, [u64; 2])] = &[
6350 // `class`, `{}`
6351 ("class", "class C {}\n", [2, 2]),
6352 // `interface`, `{}`
6353 ("interface", "interface I {}\n", [2, 2]),
6354 // `enum`, `{}`
6355 ("enum", "enum E { A }\n", [2, 2]),
6356 // `record`, `()`, `int`, `{}`
6357 ("record", "record R(int x) {}\n", [4, 4]),
6358 // `@interface`, `{}`
6359 ("annotation type", "@interface N {}\n", [2, 2]),
6360 // `class`, `{}`, `=`, `.`, `;` — one `class`: the literal's
6361 // tail is an operand (#1566)
6362 ("class literal", "class U { Object o = C.class; }\n", [5, 5]),
6363 ];
6364 assert_operator_rows::<JavaParser>("Foo.java", ROWS);
6365 }
6366
6367 /// #1560: a Java type name is an operand, as in C#, Rust, the C
6368 /// family, Go, Groovy and TypeScript. Each row isolates one position
6369 /// of the single `type_identifier` kind. The class-literal rows pin
6370 /// that a type inside `Foo.class` bills like any other (#1566),
6371 /// whether it sits there directly or under an array or scoped type,
6372 /// beside the literal's `class` tail; the `var` rows, that the inferred-type
6373 /// keyword bills nothing while a variable named `var` stays an
6374 /// operand.
6375 #[cfg(feature = "java")]
6376 #[test]
6377 fn java_type_names_are_operands_1560() {
6378 const ROWS: &[(&str, &str, [u64; 2])] = &[
6379 // `C`, `I` x2, `Foo`, `f`
6380 (
6381 "field type and implements",
6382 "class C implements I { Foo f; }\ninterface I {}\n",
6383 [4, 5],
6384 ),
6385 // `C`, `java`, `util`, `List`, `Foo`, `l`
6386 (
6387 "scoped and generic",
6388 "class C { java.util.List<Foo> l; }\n",
6389 [6, 6],
6390 ),
6391 // `C`, `T` x2, `Comparable`
6392 (
6393 "type parameter",
6394 "class C<T extends Comparable<T>> {}\n",
6395 [3, 4],
6396 ),
6397 // `C`, `m`, `Ex`
6398 ("throws", "class C { void m() throws Ex {} }\n", [3, 3]),
6399 // `C`, `Object`, `a`, `Foo`, `class`
6400 (
6401 "class literal",
6402 "class C { Object a = Foo.class; }\n",
6403 [5, 5],
6404 ),
6405 // `C`, `Object`, `a`, `Foo`, `class`
6406 (
6407 "array class literal",
6408 "class C { Object a = Foo[].class; }\n",
6409 [5, 5],
6410 ),
6411 // `C`, `Object`, `a`, `java`, `util`, `List`, `class`
6412 (
6413 "scoped class literal",
6414 "class C { Object a = java.util.List.class; }\n",
6415 [7, 7],
6416 ),
6417 // `C`, `m`, `x`, `1` — no `var`
6418 (
6419 "inferred type",
6420 "class C { void m() { var x = 1; } }\n",
6421 [4, 4],
6422 ),
6423 // `C`, `m`, `var`, `1` — the variable's name
6424 (
6425 "variable named var",
6426 "class C { void m() { int var = 1; } }\n",
6427 [4, 4],
6428 ),
6429 ];
6430 assert_operand_rows::<JavaParser>("Foo.java", ROWS);
6431 }
6432
6433 /// One class literal's own vocabulary: the source spelling, then the
6434 /// operators and operands it adds to the fixture's frame.
6435 #[cfg(any(feature = "groovy", feature = "java"))]
6436 type ClassLiteralParts = (
6437 &'static str,
6438 &'static [&'static str],
6439 &'static [&'static str],
6440 );
6441
6442 /// #1566: the class-literal shapes Java and Groovy spell alike. The
6443 /// expected parts are what Groovy — untouched by #1566 — already
6444 /// billed for `Foo.class`: the type name an operand, the `.` and
6445 /// `[]` operators, and the `class` tail an operand. A primitive or
6446 /// `void` receiver is an operator keyed by its spelling, as Java's
6447 /// keyword is; Groovy joined that half in #1568. Both tests below
6448 /// read this one table, so the two languages cannot drift apart.
6449 #[cfg(any(feature = "groovy", feature = "java"))]
6450 const SHARED_CLASS_LITERALS: &[ClassLiteralParts] = &[
6451 ("Foo.class", &["."], &["Foo", "class"]),
6452 ("Foo[].class", &[".", "[]"], &["Foo", "class"]),
6453 (
6454 "java.util.List.class",
6455 &["."],
6456 &["List", "class", "java", "util"],
6457 ),
6458 ("int.class", &[".", "int"], &["class"]),
6459 ("int[].class", &[".", "[]", "int"], &["class"]),
6460 ("void.class", &[".", "void"], &["class"]),
6461 ];
6462
6463 /// Asserts each literal's operator and operand vocabularies inside
6464 /// `frame`, which spells the fixture around it and contributes
6465 /// `frame_ops` / `frame_operands` of its own.
6466 #[cfg(any(feature = "groovy", feature = "java"))]
6467 #[track_caller]
6468 fn assert_class_literal_parts<T: crate::MetricSuite>(
6469 file: &str,
6470 frame: fn(&str) -> String,
6471 frame_ops: &[&str],
6472 frame_operands: &[&str],
6473 rows: &[ClassLiteralParts],
6474 ) {
6475 let merged = |frame: &[&str], parts: &[&str]| {
6476 let mut all: Vec<String> = frame.iter().chain(parts).map(|s| (*s).to_owned()).collect();
6477 all.sort_unstable();
6478 all.dedup();
6479 all
6480 };
6481 for (literal, ops, operands) in rows {
6482 let got = ops_of::<T>(&frame(literal), file);
6483 assert_eq!(
6484 got.operators,
6485 merged(frame_ops, ops),
6486 "operators of {literal}"
6487 );
6488 assert_eq!(
6489 got.operands,
6490 merged(frame_operands, operands),
6491 "operands of {literal}"
6492 );
6493 }
6494 }
6495
6496 /// #1566: a Java class literal bills its parts. JLS §15.8.2 makes it
6497 /// a primary expression rather than a literal (§3.10 lists none), so
6498 /// the type name is an operand as anywhere else, `.` and `[]` are
6499 /// operators, and the `class` tail is the operand they act on — the
6500 /// split Kotlin's and PHP's `Foo::class` and Groovy's `Foo.class`
6501 /// already made. Before the fix the literal billed as one operand
6502 /// spelled by its whole text while `.`, `[]` and a primitive inside
6503 /// it also billed as operators. The shared rows take their expected
6504 /// values from Groovy, whose primitive and `void` rows joined them
6505 /// in #1568.
6506 #[cfg(feature = "java")]
6507 #[test]
6508 fn java_class_literal_bills_its_parts_1566() {
6509 const FRAME_OPS: &[&str] = &[";", "=", "class", "{}"];
6510 const FRAME_OPERANDS: &[&str] = &["C", "Object", "c"];
6511 let frame = |literal: &str| format!("class C {{ Object c = {literal}; }}\n");
6512 assert_class_literal_parts::<JavaParser>(
6513 "Foo.java",
6514 frame,
6515 FRAME_OPS,
6516 FRAME_OPERANDS,
6517 SHARED_CLASS_LITERALS,
6518 );
6519 // The totals pin the multiplicities a vocabulary cannot: the frame
6520 // bills four operators (`class`, `{}`, `=`, `;`) and three operands
6521 // (`C`, `Object`, `c`), and each literal adds its parts once —
6522 // `java.util.List.class` its three `.` and four names.
6523 assert_halstead_rows::<JavaParser>(
6524 "Foo.java",
6525 &[
6526 ("int", "class C { Object c = int.class; }\n", [6, 4]),
6527 ("array", "class C { Object c = Foo[].class; }\n", [6, 5]),
6528 (
6529 "scoped",
6530 "class C { Object c = java.util.List.class; }\n",
6531 [7, 7],
6532 ),
6533 ("void", "class C { Object c = void.class; }\n", [6, 4]),
6534 ],
6535 |h| [h.total_operators(), h.total_operands()],
6536 );
6537 }
6538
6539 /// #1566's oracle: Groovy bills the shared class-literal shapes the
6540 /// way Java now does. A Groovy field needs no `;`.
6541 #[cfg(feature = "groovy")]
6542 #[test]
6543 fn groovy_class_literal_bills_its_parts_1566() {
6544 assert_class_literal_parts::<GroovyParser>(
6545 "foo.groovy",
6546 |literal| format!("class C {{ Object c = {literal} }}\n"),
6547 &["=", "class", "{}"],
6548 &["C", "Object", "c"],
6549 SHARED_CLASS_LITERALS,
6550 );
6551 }
6552
6553 /// #1568: a Groovy primitive type bills as the operator Java's
6554 /// keyword does, keyed by its spelling, in every position the
6555 /// grammar gives it: a `type_identifier` where a type is declared,
6556 /// a plain `identifier` where the grammar misreads an uninitialised
6557 /// local (`int t` parses as two expression statements). Each row's
6558 /// operators are what the Java twin of the issue fixture bills, which
6559 /// `groovy_primitive_type_matches_java_1568` checks directly.
6560 #[cfg(feature = "groovy")]
6561 #[test]
6562 fn groovy_primitive_types_are_operators_1568() {
6563 const ROWS: &[(&str, &[&str], &[&str])] = &[
6564 (
6565 "class A { boolean m() { int x = 1; } }\n",
6566 &["()", ";", "=", "boolean", "class", "int", "{}"],
6567 &["1", "A", "m", "x"],
6568 ),
6569 (
6570 "void m(long a, char b) {}\n",
6571 &["()", ",", "char", "long", "void", "{}"],
6572 &["a", "b", "m"],
6573 ),
6574 (
6575 "def m() { short s\n double d }\n",
6576 &["()", "def", "double", "short", "{}"],
6577 &["d", "m", "s"],
6578 ),
6579 (
6580 "byte[] a = (float) b as byte\n",
6581 &["()", "=", "[]", "as", "byte", "float"],
6582 &["a", "b"],
6583 ),
6584 // A user type's name stays an operand, however close its
6585 // spelling: only the exact keyword bills.
6586 (
6587 "Integer a\nInt b\ninte c\n",
6588 &[],
6589 &["Int", "Integer", "a", "b", "c", "inte"],
6590 ),
6591 ];
6592 for (source, operators, operands) in ROWS {
6593 let ops = ops_of::<GroovyParser>(source, "foo.groovy");
6594 assert_eq!(ops.operators, *operators, "operators of {source:?}");
6595 assert_eq!(ops.operands, *operands, "operands of {source:?}");
6596 }
6597 }
6598
6599 /// #1568's oracle: the issue's fixture scores the same in Groovy and
6600 /// Java, vocabulary and multiplicities alike.
6601 #[cfg(all(feature = "groovy", feature = "java"))]
6602 #[test]
6603 fn groovy_primitive_type_matches_java_1568() {
6604 let source = "class A { boolean m() { int x = 1; } }\n";
6605 let groovy = ops_of::<GroovyParser>(source, "foo.groovy");
6606 let java = ops_of::<JavaParser>(source, "Foo.java");
6607 assert_eq!(groovy.operators, java.operators);
6608 assert_eq!(groovy.operands, java.operands);
6609 // `()`, `;`, `=`, `boolean`, `class`, `int`, `{}` x2 / `A`, `m`,
6610 // `x`, `1`
6611 assert_halstead_rows::<GroovyParser>(
6612 "foo.groovy",
6613 &[("issue fixture", source, [8, 4])],
6614 |h| [h.total_operators(), h.total_operands()],
6615 );
6616 assert_halstead_rows::<JavaParser>("Foo.java", &[("issue fixture", source, [8, 4])], |h| {
6617 [h.total_operators(), h.total_operands()]
6618 });
6619 }
6620
6621 /// #1568: Groovy reserves the primitive type names, so one can name
6622 /// something only as a member selector after a `.`-family operator
6623 /// or as a map or named-argument label. Each position stays an
6624 /// operand; each row is its own source so a selector kind dropped
6625 /// from the gate fails by name.
6626 #[cfg(feature = "groovy")]
6627 #[test]
6628 fn groovy_primitive_spelled_names_stay_operands_1568() {
6629 for source in [
6630 "x.int\n",
6631 "x?.int\n",
6632 "x??.int\n",
6633 "x*.int\n",
6634 "x.@int\n",
6635 "x.&int\n",
6636 "x::int\n",
6637 "def m = [int: 1]\n",
6638 "f(int: 1)\n",
6639 ] {
6640 assert_keywords_are_operands_only::<GroovyParser>(source, "foo.groovy", &["int"]);
6641 }
6642 }
6643
6644 /// #1552: Groovy's type-declaration keywords are operators, as
6645 /// Java's are. `String.class` is a `field_access` whose `class` is a
6646 /// plain identifier, so it stays an operand with no guard needed.
6647 #[cfg(feature = "groovy")]
6648 #[test]
6649 fn groovy_declaration_keywords_are_operators_1552() {
6650 const ROWS: &[(&str, &str, [u64; 2])] = &[
6651 // `class`, `{}`
6652 ("class", "class C {}\n", [2, 2]),
6653 // `interface`, `{}`
6654 ("interface", "interface I {}\n", [2, 2]),
6655 // `trait`, `{}`
6656 ("trait", "trait T {}\n", [2, 2]),
6657 // `enum`, `{}`
6658 ("enum", "enum E { A }\n", [2, 2]),
6659 // `record`, `()`, `int`, `{}` — as Java's row, since #1568
6660 ("record", "record R(int x) {}\n", [4, 4]),
6661 // `def`, `=`, `.` — no `class`
6662 ("class reference", "def a = String.class\n", [3, 3]),
6663 // `=` twice: a script-level `record` / `trait` used as a name
6664 // falls into error recovery with the keyword leaf, which is
6665 // not a declaration and bills nothing
6666 ("contextual names", "record = 3\ntrait = 4\n", [1, 2]),
6667 ];
6668 assert_operator_rows::<GroovyParser>("foo.groovy", ROWS);
6669 }
6670
6671 /// #1565: Java bills every modifier, as C# and Kotlin do, each as
6672 /// its keyword leaf and never as the `modifiers` wrapper. Before the
6673 /// fix only `abstract`, `final`, `transient`, `synchronized` and
6674 /// `default` billed. The rows from "import static" on are controls:
6675 /// a directive's `static` and the contextual words used as names add
6676 /// no operator.
6677 #[cfg(feature = "java")]
6678 #[test]
6679 fn java_modifiers_are_operators_1565() {
6680 const ROWS: &[(&str, &str, [u64; 2])] = &[
6681 // `class`, `{}`, `public`, `private`, `protected`, `int` x3,
6682 // `;` x3
6683 (
6684 "visibility",
6685 "class C { public int a; private int b; protected int c; }\n",
6686 [7, 11],
6687 ),
6688 // `class`, `{}`, `static`, `final`, `int` x2, `=`, `;` x2,
6689 // `transient`, `volatile`
6690 (
6691 "field modifiers",
6692 "class C { static final int a = 1; transient volatile int b; }\n",
6693 [9, 11],
6694 ),
6695 // `class`, `{}` x3, `native`, `synchronized`, `strictfp`,
6696 // `void` x3, `()` x3, `;`
6697 (
6698 "method modifiers",
6699 "class C { native void n(); synchronized void s() {} strictfp void f() {} }\n",
6700 [8, 14],
6701 ),
6702 // `sealed`, `class` x2, `permits`, `{}` x2, `non-sealed`,
6703 // `extends`
6704 (
6705 "sealed hierarchy",
6706 "sealed class S permits T {}\nnon-sealed class T extends S {}\n",
6707 [6, 8],
6708 ),
6709 // `class`, `{}` x2, `static`: the initializer's `static`
6710 ("static initializer", "class C { static {} }\n", [3, 4]),
6711 // `class`, `{}` x2, `public`, `()`, `return`, `;`: the
6712 // annotation beside the modifier bills nothing of its own
6713 (
6714 "annotation in modifiers",
6715 "class C { @Override public String toString() { return s; } }\n",
6716 [6, 7],
6717 ),
6718 // `.` x3, `;`, `class`, `{}`: no `static`
6719 (
6720 "import static",
6721 "import static java.lang.Math.max;\nclass C {}\n",
6722 [4, 6],
6723 ),
6724 // `{}`, `;`: no `static`, and `transitive` stays unbilled
6725 // with the rest of the module directive
6726 (
6727 "requires static",
6728 "module m { requires static transitive a; }\n",
6729 [2, 2],
6730 ),
6731 // `class`, `{}` x2, `void`, `()`, `=` x3, `;` x3, `int`
6732 (
6733 "names",
6734 "class C { void m() { var sealed = 1; int permits = 2; sealed = permits; } }\n",
6735 [7, 12],
6736 ),
6737 // `class`, `{}`, `int` x2, `,`, `;` x2, `=`, `-`
6738 (
6739 "non-sealed as a subtraction",
6740 "class C { int non, sealed; int z = non-sealed; }\n",
6741 [7, 9],
6742 ),
6743 ];
6744 assert_operator_rows::<JavaParser>("Foo.java", ROWS);
6745 }
6746
6747 /// #1565: the issue's fixture, by vocabulary. Each modifier is
6748 /// listed by its own word, never by `modifiers`, and the contextual
6749 /// words used as names are operands only. No getter edit makes that
6750 /// second half fail, since Java's arms carry no guard on those
6751 /// words: it pins the grammar emitting an `identifier` for a name,
6752 /// so a bump that routes one to the keyword kind trips it.
6753 #[cfg(feature = "java")]
6754 #[test]
6755 fn java_modifier_vocabulary_1565() {
6756 let ops = ops_of::<JavaParser>(
6757 "public abstract class C { private static final int a = 1; \
6758 protected transient int b; public synchronized void m() {} }\n",
6759 "Foo.java",
6760 );
6761 assert_eq!(
6762 ops.operators,
6763 [
6764 "()",
6765 ";",
6766 "=",
6767 "abstract",
6768 "class",
6769 "final",
6770 "int",
6771 "private",
6772 "protected",
6773 "public",
6774 "static",
6775 "synchronized",
6776 "transient",
6777 "void",
6778 "{}",
6779 ]
6780 );
6781 assert_eq!(ops.operands, ["1", "C", "a", "b", "m"]);
6782
6783 assert_keywords_are_operands_only::<JavaParser>(
6784 "class C { int sealed() { return permits; } void m() { sealed(); } }\n",
6785 "Foo.java",
6786 &["sealed", "permits"],
6787 );
6788 }
6789
6790 /// #1565: Groovy bills every modifier, as Java now does. Its grammar
6791 /// has no `modifiers` wrapper, so each leaf sits directly under its
6792 /// declaration. The rows declare with `def`, so the only primitive
6793 /// type counted is `void`, an operator since #1568. The last two
6794 /// rows are controls.
6795 #[cfg(feature = "groovy")]
6796 #[test]
6797 fn groovy_modifiers_are_operators_1565() {
6798 const ROWS: &[(&str, &str, [u64; 2])] = &[
6799 // `class`, `{}`, `public`, `private`, `protected`, `def` x3
6800 (
6801 "visibility",
6802 "class C {\n public def a\n private def b\n protected def c\n}\n",
6803 [6, 8],
6804 ),
6805 // `class`, `{}`, `static`, `final`, `def` x2, `=`,
6806 // `transient`, `volatile`
6807 (
6808 "field modifiers",
6809 "class C {\n static final def a = 1\n transient volatile def b\n}\n",
6810 [8, 9],
6811 ),
6812 // `class`, `{}` x3, `native`, `synchronized`, `strictfp`,
6813 // `void` x3 (#1568), `()` x3
6814 (
6815 "method modifiers",
6816 "class C {\n native void n()\n synchronized void s() {}\n \
6817 strictfp void f() {}\n}\n",
6818 [7, 13],
6819 ),
6820 // `sealed`, `class` x2, `permits`, `{}` x2, `non-sealed`,
6821 // `extends`
6822 (
6823 "sealed hierarchy",
6824 "sealed class S permits T {}\nnon-sealed class T extends S {}\n",
6825 [6, 8],
6826 ),
6827 // `sealed` x2, `interface`, `trait`, `permits` x2, `{}` x2
6828 (
6829 "sealed interface and trait",
6830 "sealed interface I permits A {}\nsealed trait T permits A {}\n",
6831 [5, 8],
6832 ),
6833 // `class`, `{}` x3, `static` x2, `void` (#1568), `()`: an
6834 // initializer and a script-level method
6835 (
6836 "static initializer and method",
6837 "class C {\n static {}\n}\nstatic void top() {}\n",
6838 [5, 8],
6839 ),
6840 // `.` x3: no `static`, as `import` bills nothing
6841 (
6842 "import static",
6843 "import static java.lang.Math.max\n",
6844 [1, 3],
6845 ),
6846 // `=` x2, `.`, `()` x2, `def`: a statement opening with
6847 // `sealed` as a name fails into an `ERROR` holding the
6848 // keyword leaf, which bills nothing
6849 (
6850 "contextual names",
6851 "sealed = 1\nsealed.call()\ndef permits = 2\nfoo(permits)\n",
6852 [4, 6],
6853 ),
6854 ];
6855 assert_operator_rows::<GroovyParser>("foo.groovy", ROWS);
6856 }
6857
6858 /// #1565: the issue's Groovy fixture, by vocabulary, plus the
6859 /// modifier words in positions the grammar parses as a name. Unlike
6860 /// Java, Groovy accepts even its hard keywords as a map key, a named
6861 /// argument and a member name. No getter edit makes that half fail:
6862 /// it pins the grammar emitting an `identifier` there, so a bump
6863 /// that routes one to the keyword kind trips it.
6864 #[cfg(feature = "groovy")]
6865 #[test]
6866 fn groovy_modifier_vocabulary_1565() {
6867 let ops = ops_of::<GroovyParser>(
6868 "public abstract class C { private static final def a = 1; protected def m() {} }\n",
6869 "foo.groovy",
6870 );
6871 assert_eq!(
6872 ops.operators,
6873 [
6874 "()",
6875 ";",
6876 "=",
6877 "abstract",
6878 "class",
6879 "def",
6880 "final",
6881 "private",
6882 "protected",
6883 "public",
6884 "static",
6885 "{}",
6886 ]
6887 );
6888 assert_eq!(ops.operands, ["1", "C", "a", "m"]);
6889
6890 assert_keywords_are_operands_only::<GroovyParser>(
6891 "x = sealed\nfoo(permits)\ndef m = [static: 1, public: 2]\n\
6892 foo(native: 1, volatile: 2)\ny = obj.private\n",
6893 "foo.groovy",
6894 &[
6895 "sealed", "permits", "static", "public", "native", "volatile", "private",
6896 ],
6897 );
6898 }
6899
6900 /// #1565's oracle: Groovy accepts Java's modifier-heavy fixture
6901 /// verbatim, and the two languages bill it alike, vocabulary and
6902 /// multiplicities. Neither side's expectation is typed by the fix
6903 /// alone: a modifier one getter drops shows as a disagreement.
6904 #[cfg(all(feature = "groovy", feature = "java"))]
6905 #[test]
6906 fn groovy_modifiers_match_java_1565() {
6907 let source = "public abstract class C { private static final int a = 1; \
6908 protected transient int b; public synchronized void m() {} }\n";
6909 let groovy = ops_of::<GroovyParser>(source, "foo.groovy");
6910 let java = ops_of::<JavaParser>(source, "Foo.java");
6911 assert_eq!(groovy.operators, java.operators);
6912 assert_eq!(groovy.operands, java.operands);
6913 // `public` x2, `abstract`, `class`, `{}` x2, `private`, `static`,
6914 // `final`, `int` x2, `=`, `;` x2, `protected`, `transient`,
6915 // `synchronized`, `void`, `()`
6916 let rows = [("modifier fixture", source, [15, 19])];
6917 assert_operator_rows::<GroovyParser>("foo.groovy", &rows);
6918 assert_operator_rows::<JavaParser>("Foo.java", &rows);
6919 }
6920
6921 /// #1552 rows shared by the two C++ clones. The template-parameter
6922 /// rows pin the synonym: `class T` is `typename T`, and the two
6923 /// spellings score alike. Both were unbilled until #1557 billed
6924 /// `typename`, and the parameter `class` with it.
6925 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
6926 const CPP_DECLARATION_KEYWORD_ROWS: &[(&str, &str, [u64; 2])] = &[
6927 // `class`, `{}`, `;`
6928 ("class", "class C {};\n", [3, 3]),
6929 // `struct`, `{}`, `;`
6930 ("struct", "struct S {};\n", [3, 3]),
6931 // `union`, `{}`, `int`, `;` twice
6932 ("union", "union U { int a; };\n", [4, 5]),
6933 // `enum`, `{}`, `;`
6934 ("enum", "enum E { A };\n", [3, 3]),
6935 // `enum`, `class`, `{}`, `;`
6936 ("enum class", "enum class F { B };\n", [4, 4]),
6937 // `struct`, `;` — an elaborated type bills its keyword
6938 ("elaborated type", "struct S s;\n", [2, 2]),
6939 // `template`, `<>`, `class`, `struct`, `{}`, `;` — the parameter
6940 // list's brackets are one pair (#1559)
6941 (
6942 "class parameter",
6943 "template <class T> struct W {};\n",
6944 [6, 6],
6945 ),
6946 // as above, `typename` in place of `class`
6947 (
6948 "typename parameter",
6949 "template <typename T> struct W {};\n",
6950 [6, 6],
6951 ),
6952 // `template`, `<>`, `class`, `=`, `int`, `struct`, `{}`, `;`
6953 (
6954 "optional class parameter",
6955 "template <class T = int> struct W {};\n",
6956 [8, 8],
6957 ),
6958 // `template`, `<>`, `class`, `struct`, `{}`, `;` — `...` is an
6959 // operand
6960 (
6961 "variadic class parameter",
6962 "template <class... T> struct W {};\n",
6963 [6, 6],
6964 ),
6965 ];
6966
6967 /// One call per clone, so reverting either arm fails its own row;
6968 /// Mozcpp owns no file extension, so this is its only coverage.
6969 #[cfg(all(feature = "cpp", feature = "mozcpp"))]
6970 #[test]
6971 fn cpp_declaration_keywords_are_operators_1552() {
6972 assert_operator_rows::<CppParser>("foo.cpp", CPP_DECLARATION_KEYWORD_ROWS);
6973 assert_operator_rows::<MozcppParser>("foo.cpp", CPP_DECLARATION_KEYWORD_ROWS);
6974 }
6975
6976 /// #1552: C bills `struct` / `union` / `enum` as C++ does, so a
6977 /// header scores the same whichever grammar parses it.
6978 #[cfg(feature = "c")]
6979 #[test]
6980 fn c_declaration_keywords_are_operators_1552() {
6981 const ROWS: &[(&str, &str, [u64; 2])] = &[
6982 // `struct`, `{}`, `int`, `;` twice
6983 ("struct", "struct S { int a; };\n", [4, 5]),
6984 // `union`, `{}`, `int`, `;` twice
6985 ("union", "union U { int a; };\n", [4, 5]),
6986 // `enum`, `{}`, `;`
6987 ("enum", "enum E { A };\n", [3, 3]),
6988 // `struct`, `;`
6989 ("elaborated type", "struct S s;\n", [2, 2]),
6990 ];
6991 assert_operator_rows::<CParser>("foo.c", ROWS);
6992 }
6993
6994 /// #1578: C bills each storage class, function specifier and type
6995 /// qualifier as its keyword leaf, never as the
6996 /// `storage_class_specifier` / `type_qualifier` wrapper. Before the
6997 /// fix none of them billed.
6998 #[cfg(feature = "c")]
6999 #[test]
7000 fn c_modifiers_are_operators_1578() {
7001 const ROWS: &[(&str, &str, [u64; 2])] = &[
7002 // `static`, `extern`, `register`, `auto`, `int` x4, `;` x4
7003 (
7004 "storage classes",
7005 "static int a;\nextern int b;\nregister int c;\nauto int d;\n",
7006 [6, 12],
7007 ),
7008 // `const`, `volatile`, `int` x3, `;` x3, `*`, `restrict`,
7009 // `_Atomic`
7010 (
7011 "qualifiers",
7012 "const volatile int a;\nint * restrict p;\n_Atomic int g;\n",
7013 [7, 11],
7014 ),
7015 // `inline`, `int`, `()` x2, `void` x3, `;` x2, `_Noreturn`
7016 (
7017 "function specifiers",
7018 "inline int f(void);\n_Noreturn void g(void);\n",
7019 [6, 10],
7020 ),
7021 // `__inline`, `int` x3, `()`, `void`, `;` x3, `__thread`, `*`,
7022 // `__restrict__`: each vendor spelling is its own operator
7023 (
7024 "vendor spellings",
7025 "__inline int f(void);\n__thread int t;\nint * __restrict__ q;\n",
7026 [8, 12],
7027 ),
7028 // `int`, `*`, `const`, `=`, `;`: a pointer's own qualifier
7029 ("pointer qualifier", "int * const p = 0;\n", [5, 5]),
7030 // `_Alignas`, `()`, `int`, `;`
7031 ("alignas", "_Alignas(8) int m;\n", [4, 4]),
7032 // `int`, `=`, `__extension__`, `;`: the expression prefix
7033 ("extension expression", "int q = __extension__ 1;\n", [4, 4]),
7034 ];
7035 assert_operator_rows::<CParser>("foo.c", ROWS);
7036 }
7037
7038 /// #1578: the issue's C fixture, by vocabulary.
7039 #[cfg(feature = "c")]
7040 #[test]
7041 fn c_modifier_vocabulary_1578() {
7042 let ops = ops_of::<CParser>(
7043 "static inline const volatile int a = 1;\nextern int b;\nregister int c;\n",
7044 "foo.c",
7045 );
7046 assert_eq!(
7047 ops.operators,
7048 [
7049 ";", "=", "const", "extern", "inline", "int", "register", "static", "volatile",
7050 ]
7051 );
7052 assert_eq!(ops.operands, ["1", "a", "b", "c"]);
7053 }
7054
7055 /// #1578 rows shared by the two C++ clones: C's modifiers plus C++'s
7056 /// own. Each bills as its keyword leaf, never as the
7057 /// `access_specifier` / `virtual_specifier` / `type_qualifier` /
7058 /// `explicit_function_specifier` wrapper.
7059 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
7060 const CPP_MODIFIER_ROWS: &[(&str, &str, [u64; 2])] = &[
7061 // `class`, `{}`, `public`, `protected`, `private`, `:` x3,
7062 // `int` x3, `;` x4
7063 (
7064 "access labels",
7065 "class C { public: int a; protected: int b; private: int c; };\n",
7066 [8, 15],
7067 ),
7068 // `struct`, `{}`, `static`, `int` x2, `;` x4, `mutable`,
7069 // `virtual`, `void`, `()`, `const`, `override`, `final`
7070 (
7071 "member modifiers",
7072 "struct S { static int a; mutable int b; virtual void f() const override final; };\n",
7073 [12, 16],
7074 ),
7075 // `struct`, `{}`, `friend`, `class`, `;` x3, `explicit`, `()`,
7076 // `int`
7077 (
7078 "friend and explicit",
7079 "struct S { friend class D; explicit S(int); };\n",
7080 [8, 10],
7081 ),
7082 // `extern`, `inline`, `constexpr`, `int`, `()`, `{}`, `return`,
7083 // `;`: the `"C"` is an operand
7084 (
7085 "linkage specification",
7086 "extern \"C\" inline constexpr int g() { return 1; }\n",
7087 [8, 8],
7088 ),
7089 // `consteval`, `int` x2, `()`, `;` x2, `constinit`,
7090 // `thread_local`, `=`
7091 (
7092 "C++20 specifiers",
7093 "consteval int h();\nconstinit thread_local int t = 1;\n",
7094 [7, 9],
7095 ),
7096 // `void` x2, `()` x3, `noexcept` x2, `;` x2: the leaf bills, the
7097 // `noexcept` wrapper does not
7098 (
7099 "exception specification",
7100 "void k() noexcept;\nvoid k2() noexcept(true);\n",
7101 [4, 9],
7102 ),
7103 // `struct`, `final`, `{}` x3, `;` x3, `class` x2, `:` x2,
7104 // `public`, `private`, `virtual`
7105 (
7106 "base clauses",
7107 "struct B final {};\nclass E : public B {};\nclass F : private virtual B {};\n",
7108 [9, 15],
7109 ),
7110 // `void`, `()` x2, `{}` x2, `if`, `constexpr`
7111 (
7112 "if constexpr",
7113 "void f() { if constexpr (true) {} }\n",
7114 [5, 7],
7115 ),
7116 // `inline`, `namespace`, `{}`
7117 ("inline namespace", "inline namespace v1 {}\n", [3, 3]),
7118 // `int` x2, `=` x2, `;` x2: the contextual words as names add
7119 // no operator
7120 (
7121 "contextual names",
7122 "int override = 1;\nint final = override;\n",
7123 [3, 6],
7124 ),
7125 ];
7126
7127 /// One call per clone, so reverting either arm fails its own row;
7128 /// Mozcpp owns no file extension, so this is its only lib coverage.
7129 #[cfg(all(feature = "cpp", feature = "mozcpp"))]
7130 #[test]
7131 fn cpp_modifiers_are_operators_1578() {
7132 assert_operator_rows::<CppParser>("foo.cpp", CPP_MODIFIER_ROWS);
7133 assert_operator_rows::<MozcppParser>("foo.cpp", CPP_MODIFIER_ROWS);
7134 }
7135
7136 /// #1578: the issue's C++ fixture, by vocabulary, in both clones,
7137 /// plus `override` and `final` used as names. No getter edit makes
7138 /// that second half fail, since the arms carry no guard on those
7139 /// words: it pins the grammar emitting an `identifier` for a name,
7140 /// so a bump that routes one to the keyword kind trips it.
7141 #[cfg(all(feature = "cpp", feature = "mozcpp"))]
7142 #[test]
7143 fn cpp_modifier_vocabulary_1578() {
7144 const SOURCE: &str = "class C { public: static int a; \
7145 virtual void f() const override final; mutable int b; };\n\
7146 extern \"C\" inline constexpr int g() { return 1; }\n";
7147 const NAMES: &str = "int override = 1;\nint final() { return override; }\n";
7148 let expected_operators = [
7149 "()",
7150 ":",
7151 ";",
7152 "class",
7153 "const",
7154 "constexpr",
7155 "extern",
7156 "final",
7157 "inline",
7158 "int",
7159 "mutable",
7160 "override",
7161 "public",
7162 "return",
7163 "static",
7164 "virtual",
7165 "void",
7166 "{}",
7167 ];
7168 let expected_operands = ["\"C\"", "1", "C", "a", "b", "f", "g"];
7169
7170 let cpp = ops_of::<CppParser>(SOURCE, "foo.cpp");
7171 assert_eq!(cpp.operators, expected_operators);
7172 assert_eq!(cpp.operands, expected_operands);
7173 let mozcpp = ops_of::<MozcppParser>(SOURCE, "foo.cpp");
7174 assert_eq!(mozcpp.operators, expected_operators);
7175 assert_eq!(mozcpp.operands, expected_operands);
7176
7177 assert_keywords_are_operands_only::<CppParser>(NAMES, "foo.cpp", &["override", "final"]);
7178 assert_keywords_are_operands_only::<MozcppParser>(NAMES, "foo.cpp", &["override", "final"]);
7179 }
7180
7181 /// #1578: every modifier spelling the arms list bills under its own
7182 /// name, one fixture line apiece, so dropping any one kind from an
7183 /// arm fails its own entry. The row tests above pin the counts; this
7184 /// one pins the spellings they leave out.
7185 #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
7186 #[track_caller]
7187 fn assert_modifier_spellings_bill<T: crate::MetricSuite>(file: &str, rows: &[(&str, &str)]) {
7188 for (spelling, source) in rows {
7189 let ops = ops_of::<T>(source, file);
7190 assert!(
7191 ops.operators.iter().any(|o| o == spelling),
7192 "{file}: `{spelling}` must be an operator in {source:?}; operators were {:?}",
7193 ops.operators
7194 );
7195 assert!(
7196 !ops.operands.iter().any(|o| o == spelling),
7197 "{file}: `{spelling}` must not be an operand; operands were {:?}",
7198 ops.operands
7199 );
7200 }
7201 }
7202
7203 /// The spellings C and C++ share, each in a declaration both
7204 /// grammars parse without error.
7205 #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
7206 const C_FAMILY_MODIFIER_SPELLINGS: &[(&str, &str)] = &[
7207 ("static", "static int a;\n"),
7208 ("extern", "extern int a;\n"),
7209 ("register", "register int a;\n"),
7210 ("inline", "inline int f(void);\n"),
7211 ("__inline", "__inline int f(void);\n"),
7212 ("__inline__", "__inline__ int f(void);\n"),
7213 ("__forceinline", "__forceinline int f(void);\n"),
7214 ("thread_local", "thread_local int a;\n"),
7215 ("__thread", "__thread int a;\n"),
7216 ("const", "const int a = 1;\n"),
7217 ("constexpr", "constexpr int a = 1;\n"),
7218 ("volatile", "volatile int a;\n"),
7219 ("restrict", "int * restrict p;\n"),
7220 ("__restrict__", "int * __restrict__ p;\n"),
7221 ("_Atomic", "_Atomic int a;\n"),
7222 ("_Noreturn", "_Noreturn void f(void);\n"),
7223 ("noreturn", "noreturn void f(void);\n"),
7224 ("_Nonnull", "int * _Nonnull p;\n"),
7225 ("alignas", "alignas(8) int a;\n"),
7226 ("_Alignas", "_Alignas(8) int a;\n"),
7227 ("__extension__", "__extension__ int a;\n"),
7228 // The one place the grammar emits `__volatile__`: as a GNU `asm`
7229 // qualifier, where the plain spelling bills too.
7230 (
7231 "__volatile__",
7232 "void f(void) { __asm__ __volatile__(\"nop\"); }\n",
7233 ),
7234 ("volatile", "void f(void) { asm volatile(\"nop\"); }\n"),
7235 ];
7236
7237 #[cfg(feature = "c")]
7238 #[test]
7239 fn c_modifier_spellings_bill_1578() {
7240 assert_modifier_spellings_bill::<CParser>("foo.c", C_FAMILY_MODIFIER_SPELLINGS);
7241 // C's storage class `auto`; C++ has none (see the Cpp arm).
7242 assert_modifier_spellings_bill::<CParser>("foo.c", &[("auto", "auto int a;\n")]);
7243 }
7244
7245 #[cfg(all(feature = "cpp", feature = "mozcpp"))]
7246 #[test]
7247 fn cpp_modifier_spellings_bill_1578() {
7248 const CPP_ONLY: &[(&str, &str)] = &[
7249 ("virtual", "struct S { virtual void f(); };\n"),
7250 ("mutable", "struct S { mutable int a; };\n"),
7251 ("constinit", "constinit int a = 1;\n"),
7252 ("consteval", "consteval int f() { return 1; }\n"),
7253 ("explicit", "struct S { explicit S(int); };\n"),
7254 ("friend", "struct S { friend class D; };\n"),
7255 ("public", "class C { public: int a; };\n"),
7256 ("private", "class C { private: int a; };\n"),
7257 ("protected", "class C { protected: int a; };\n"),
7258 ("override", "struct S { void f() override; };\n"),
7259 ("final", "struct S final {};\n"),
7260 ("noexcept", "void f() noexcept;\n"),
7261 ];
7262 for rows in [C_FAMILY_MODIFIER_SPELLINGS, CPP_ONLY] {
7263 assert_modifier_spellings_bill::<CppParser>("foo.cpp", rows);
7264 assert_modifier_spellings_bill::<MozcppParser>("foo.cpp", rows);
7265 }
7266 }
7267
7268 /// #1552: ObjC bills C's `struct` and its own class and protocol
7269 /// declaration keywords. `@class` already billed its `@` and stays
7270 /// as it was.
7271 #[cfg(feature = "objc")]
7272 #[test]
7273 fn objc_declaration_keywords_are_operators_1552() {
7274 const ROWS: &[(&str, &str, [u64; 2])] = &[
7275 // `@interface`, `:`
7276 ("interface", "@interface C : NSObject\n@end\n", [2, 2]),
7277 // `@implementation`
7278 ("implementation", "@implementation C\n@end\n", [1, 1]),
7279 // `@protocol`
7280 ("protocol", "@protocol P\n@end\n", [1, 1]),
7281 // `struct`, `{}`, `int`, `;` twice
7282 ("struct", "struct S { int a; };\n", [4, 5]),
7283 // `@`, `;` — unchanged by #1552
7284 ("forward class", "@class Fwd;\n", [2, 2]),
7285 ];
7286 assert_operator_rows::<ObjcParser>("foo.m", ROWS);
7287 }
7288
7289 /// #1579: ObjC bills C's declaration modifiers as `c.rs` does
7290 /// (#1578), and its own: ivar visibility labels, property attribute
7291 /// words, ARC ownership and nullability qualifiers, protocol section
7292 /// labels, method-type qualifiers and generic variance. Before the
7293 /// fix none of them billed.
7294 #[cfg(feature = "objc")]
7295 #[test]
7296 fn objc_modifiers_are_operators_1579() {
7297 const ROWS: &[(&str, &str, [u64; 2])] = &[
7298 // `static`, `const`, `int`, `=`, `;`: the issue's first line
7299 ("C modifiers", "static const int a = 1;\n", [5, 5]),
7300 // `@interface`, `{}`, `@public`, `@private`, `int` x2, `;` x2
7301 (
7302 "ivar visibility",
7303 "@interface C { @public int x; @private int y; }\n@end\n",
7304 [6, 8],
7305 ),
7306 // `@interface`, `()`, `nonatomic`, `,` x2, `strong`,
7307 // `getter`, `=`, `;`: `isY` is an operand, `id` and
7308 // `@property` bill nothing
7309 (
7310 "property attributes",
7311 "@interface C\n@property (nonatomic, strong, getter=isY) id y;\n@end\n",
7312 [8, 9],
7313 ),
7314 // `__weak`, `__strong`, `__block`, `int`, `;` x3
7315 (
7316 "ownership qualifiers",
7317 "__weak id a;\n__strong id b;\n__block int c;\n",
7318 [5, 7],
7319 ),
7320 // `*` x2, `_Nullable`, `nullable`, `;` x2
7321 (
7322 "nullability qualifiers",
7323 "NSString * _Nullable f;\nNSString * nullable k;\n",
7324 [4, 6],
7325 ),
7326 // `@protocol`, `@optional`, `@required`, `-` x2, `()` x3,
7327 // `void` x2, `;` x2, `oneway`, `:`, `out`, `*`
7328 (
7329 "protocol sections and method qualifiers",
7330 "@protocol P\n@optional\n- (void)m;\n@required\n\
7331 - (oneway void)n:(out id *)b;\n@end\n",
7332 [11, 16],
7333 ),
7334 // `@interface`, `<>`, `__covariant`, `:`
7335 (
7336 "generic variance",
7337 "@interface G<__covariant T> : NSObject\n@end\n",
7338 [4, 4],
7339 ),
7340 ];
7341 assert_operator_rows::<ObjcParser>("foo.m", ROWS);
7342 }
7343
7344 /// #1579: the `nullable` qualifier and the `nullable` property
7345 /// attribute are one operator. The attribute has no keyword kind, so
7346 /// it is keyed by text, and keying the qualifier by kind would list
7347 /// the one word twice in `n1`.
7348 #[cfg(feature = "objc")]
7349 #[test]
7350 fn objc_nullable_is_one_operator_1579() {
7351 const SOURCE: &str = "@interface C\n@property (nullable) id y;\n- (nullable id)m;\n@end\n";
7352 // `@interface`, `()` x2, `nullable` x2, `-`, `;` x2
7353 assert_operator_rows::<ObjcParser>("foo.m", &[("nullable", SOURCE, [5, 8])]);
7354 let ops = ops_of::<ObjcParser>(SOURCE, "foo.m");
7355 assert_eq!(ops.operators, ["()", "-", ";", "@interface", "nullable"]);
7356 assert_eq!(ops.operands, ["C", "m", "y"]);
7357 }
7358
7359 /// #1579's oracle: a `.m` is a C translation unit, so C's
7360 /// modifier-heavy declarations score alike in both grammars,
7361 /// vocabulary and multiplicities. Neither expectation is typed by
7362 /// the fix: a modifier one getter drops shows as a disagreement.
7363 #[cfg(all(feature = "c", feature = "objc"))]
7364 #[test]
7365 fn objc_c_modifiers_match_c_1579() {
7366 let source = "static inline const volatile int a = 1;\nextern int b;\n\
7367 register int c;\nauto int d;\nint * restrict p;\n_Atomic int g;\n\
7368 __thread int t;\n_Noreturn void h(void);\n_Alignas(8) int m;\n";
7369 let objc = ops_of::<ObjcParser>(source, "foo.m");
7370 let c = ops_of::<CParser>(source, "foo.c");
7371 assert_eq!(objc.operators, c.operators);
7372 assert_eq!(objc.operands, c.operands);
7373 // `static`, `inline`, `const`, `volatile`, `int` x8, `=`, `;` x9,
7374 // `extern`, `register`, `auto`, `*`, `restrict`, `_Atomic`,
7375 // `__thread`, `_Noreturn`, `void` x2, `()` x2, `_Alignas`
7376 let rows = [("C modifiers", source, [18, 35])];
7377 assert_operator_rows::<ObjcParser>("foo.m", &rows);
7378 assert_operator_rows::<CParser>("foo.c", &rows);
7379 }
7380
7381 /// #1579: every modifier word the ObjC arm lists bills under its own
7382 /// name, one fixture apiece, so dropping any kind from the arm fails
7383 /// its own entry. The spellings C shares with C++ run through the
7384 /// #1578 table; `OBJC_ONLY` holds the rest, C's `auto` included, plus
7385 /// a sample of the text-keyed property attributes.
7386 #[cfg(feature = "objc")]
7387 #[test]
7388 fn objc_modifier_spellings_bill_1579() {
7389 const OBJC_ONLY: &[(&str, &str)] = &[
7390 ("auto", "auto int a;\n"),
7391 ("__const", "__const int a;\n"),
7392 ("_Complex", "double _Complex z;\n"),
7393 ("__complex", "double __complex z;\n"),
7394 ("nullable", "NSString * nullable p;\n"),
7395 ("_Nullable", "NSString * _Nullable p;\n"),
7396 ("__nullable", "NSString * __nullable p;\n"),
7397 ("_Nullable_result", "NSString * _Nullable_result p;\n"),
7398 ("_Null_unspecified", "NSString * _Null_unspecified p;\n"),
7399 ("__nonnull", "NSString * __nonnull p;\n"),
7400 ("__autoreleasing", "__autoreleasing id a;\n"),
7401 ("__block", "__block int a;\n"),
7402 ("__strong", "__strong id a;\n"),
7403 ("__unsafe_unretained", "__unsafe_unretained id a;\n"),
7404 ("__weak", "__weak id a;\n"),
7405 ("__bridge", "void f(void) { id x = (__bridge id)p; }\n"),
7406 (
7407 "__bridge_retained",
7408 "void f(void) { void *x = (__bridge_retained void *)p; }\n",
7409 ),
7410 (
7411 "__bridge_transfer",
7412 "void f(void) { id x = (__bridge_transfer id)p; }\n",
7413 ),
7414 ("__kindof", "__kindof NSView *v;\n"),
7415 (
7416 "__covariant",
7417 "@interface G<__covariant T> : NSObject\n@end\n",
7418 ),
7419 (
7420 "__contravariant",
7421 "@interface G<__contravariant T> : NSObject\n@end\n",
7422 ),
7423 ("in", "@protocol P\n- (void)m:(in id)a;\n@end\n"),
7424 ("out", "@protocol P\n- (void)m:(out id *)a;\n@end\n"),
7425 ("inout", "@protocol P\n- (void)m:(inout id *)a;\n@end\n"),
7426 ("bycopy", "@protocol P\n- (void)m:(bycopy id)a;\n@end\n"),
7427 ("byref", "@protocol P\n- (void)m:(byref id)a;\n@end\n"),
7428 ("oneway", "@protocol P\n- (oneway void)m;\n@end\n"),
7429 ("@public", "@interface C { @public int x; }\n@end\n"),
7430 ("@private", "@interface C { @private int x; }\n@end\n"),
7431 ("@protected", "@interface C { @protected int x; }\n@end\n"),
7432 ("@package", "@interface C { @package int x; }\n@end\n"),
7433 ("@optional", "@protocol P\n@optional\n- (void)m;\n@end\n"),
7434 ("@required", "@protocol P\n@required\n- (void)m;\n@end\n"),
7435 ("copy", "@interface C\n@property (copy) id y;\n@end\n"),
7436 (
7437 "readonly",
7438 "@interface C\n@property (readonly) id y;\n@end\n",
7439 ),
7440 ("class", "@interface C\n@property (class) id y;\n@end\n"),
7441 (
7442 "setter",
7443 "@interface C\n@property (setter=sY:) id y;\n@end\n",
7444 ),
7445 ];
7446 assert_modifier_spellings_bill::<ObjcParser>("foo.m", C_FAMILY_MODIFIER_SPELLINGS);
7447 assert_modifier_spellings_bill::<ObjcParser>("foo.m", OBJC_ONLY);
7448 }
7449
7450 /// #1579: the plain-word modifiers stay operands where they name a
7451 /// variable, and a property attribute's `getter=` value is the
7452 /// selector it names, not a modifier. The arms carry no guard on
7453 /// these words, so this pins the grammar: it emits an `identifier`
7454 /// for each name, and only the first word of a `property_attribute`
7455 /// bills.
7456 #[cfg(feature = "objc")]
7457 #[test]
7458 fn objc_modifier_words_as_names_stay_operands_1579() {
7459 assert_keywords_are_operands_only::<ObjcParser>(
7460 "int out = 1;\nint copy = 2;\nint oneway = 3;\nint strong = out + copy;\n",
7461 "foo.m",
7462 &["out", "copy", "oneway", "strong"],
7463 );
7464 assert_keywords_are_operands_only::<ObjcParser>(
7465 "@interface C\n@property (getter=atomic, setter=weak:) id y;\n@end\n",
7466 "foo.m",
7467 &["atomic", "weak"],
7468 );
7469 }
7470
7471 /// #1579: the Apple SDK macros the grammar files as storage classes
7472 /// and qualifiers bill nothing, as Mozcpp's `MOZ_*` macros do: each
7473 /// annotates a declaration, and a `.c` parses the spelling as a name.
7474 #[cfg(feature = "objc")]
7475 #[test]
7476 fn objc_sdk_macros_bill_nothing_1579() {
7477 // `int` x4, `;` x4, `()`, `void`, `{}`, `return`: the macros add
7478 // nothing, and the declared names and `1` are the operands
7479 const SOURCE: &str = "NS_INLINE int f(void) { return 1; }\nFOUNDATION_EXPORT int g;\n\
7480 IBOutlet int h;\n__unused int u;\n";
7481 let ops = ops_of::<ObjcParser>(SOURCE, "foo.m");
7482 assert_eq!(ops.operators, ["()", ";", "int", "return", "void", "{}"]);
7483 assert_eq!(ops.operands, ["1", "f", "g", "h", "u"]);
7484 }
7485
7486 /// #1556: Kotlin's `interface` bills as its `class` and `object`
7487 /// do. The modifier rows score one above the bare keyword since
7488 /// #1558: `enum` in `enum class` is a `class_modifier` like `data`
7489 /// and `sealed`, and Kotlin bills its modifiers as C# does.
7490 #[cfg(feature = "kotlin")]
7491 #[test]
7492 fn kotlin_declaration_keywords_are_operators_1556() {
7493 const ROWS: &[(&str, &str, [u64; 2])] = &[
7494 // `class`, `{}`
7495 ("class", "class C {}\n", [2, 2]),
7496 // `interface`, `{}`
7497 ("interface", "interface I {}\n", [2, 2]),
7498 // `object`, `{}`
7499 ("object", "object O {}\n", [2, 2]),
7500 // `enum`, `class`, `{}`
7501 ("enum class", "enum class E { A }\n", [3, 3]),
7502 // `data`, `class`, `{}`
7503 ("data class", "data class D {}\n", [3, 3]),
7504 // `sealed`, `interface`, `{}`
7505 ("sealed interface", "sealed interface S {}\n", [3, 3]),
7506 // `interface`, `class`, `:`, `{}` twice
7507 (
7508 "implementing class",
7509 "interface J {}\nclass K : J {}\n",
7510 [4, 5],
7511 ),
7512 ];
7513 assert_operator_rows::<KotlinParser>("foo.kt", ROWS);
7514 }
7515
7516 /// #1558: Kotlin bills its modifier keywords as C# bills its own,
7517 /// one family per row, each keyword the leaf and never
7518 /// its `*_modifier` wrapper. The "names" row is the control: the same
7519 /// words used as names are operands, and add no operator. The rows
7520 /// after it are names the grammar fails to parse, whose recovery keeps
7521 /// the keyword leaf, and their control.
7522 #[cfg(feature = "kotlin")]
7523 #[test]
7524 fn kotlin_modifiers_are_operators_1558() {
7525 const ROWS: &[(&str, &str, [u64; 2])] = &[
7526 // `public`, `private`, `protected`, `internal`, `val` x4,
7527 // `=` x4, `class`, `{}`
7528 (
7529 "visibility",
7530 "class C {\n public val a = 1\n private val b = 1\n \
7531 protected val c = 1\n internal val d = 1\n}\n",
7532 [8, 14],
7533 ),
7534 // `abstract` x2, `open`, `final`, `override`, `class`, `{}` x2,
7535 // `fun` x3, `()` x3, `=`
7536 (
7537 "inheritance and member",
7538 "abstract class A {\n abstract fun f()\n open fun g() {}\n \
7539 final override fun toString() = \"\"\n}\n",
7540 [9, 15],
7541 ),
7542 // `inner`, `value`, `annotation`, `class` x3, `()`, `val`, `:`
7543 (
7544 "class",
7545 "inner class I\nvalue class V(val x: Int)\nannotation class N\n",
7546 [7, 9],
7547 ),
7548 // `lateinit`, `companion`, `const`, `class`, `object`, `var`,
7549 // `val`, `:`, `=`, `{}` x2
7550 (
7551 "property and companion",
7552 "class C {\n lateinit var a: String\n companion object {\n \
7553 const val b = 1\n }\n}\n",
7554 [10, 11],
7555 ),
7556 // six modifiers, `fun` x6, `()` x6, `{}` x5
7557 (
7558 "function",
7559 "tailrec fun a() {}\noperator fun b() {}\ninfix fun c() {}\n\
7560 inline fun d() {}\nexternal fun e()\nsuspend fun f() {}\n",
7561 [9, 23],
7562 ),
7563 // `inline`, `noinline`, `crossinline`, `vararg`, `fun`,
7564 // `()` x3, `:` x3, `->` x2, `,` x2, `{}`
7565 (
7566 "parameter",
7567 "inline fun f(noinline a: () -> Unit, crossinline b: () -> Unit, \
7568 vararg c: Int) {}\n",
7569 [10, 16],
7570 ),
7571 // `inline`, `reified`, `in`, `out`, `fun`, `class`, `<>` x2,
7572 // `,`, `()`, `{}`
7573 (
7574 "type parameter",
7575 "inline fun <reified T> f() {}\nclass Box<in A, out B>\n",
7576 [10, 11],
7577 ),
7578 // `expect`, `actual`, `fun` x2, `()` x2, `{}`
7579 ("platform", "expect fun f()\nactual fun g() {}\n", [5, 7]),
7580 // `fun`, `()`, `{}`: an `@N` use is the `annotation` node, not
7581 // the keyword, so it bills no `annotation` operator
7582 ("annotation use", "@N fun k() {}\n", [3, 3]),
7583 // `val` x3, `=` x3: `open`, `out` and `value` are names here
7584 (
7585 "names",
7586 "val open = 1\nval out = 2\nval value = 3\n",
7587 [2, 6],
7588 ),
7589 // The rows below are names the grammar fails to parse. Its
7590 // recovery keeps the keyword leaf, which must bill nothing;
7591 // each expected value is what the source scored before #1558.
7592 // `class`, `{}` x2, `var`, `=` x2, `fun`, `()`: `open = !open`
7593 // recovers into an `ERROR` holding `inheritance_modifier`.
7594 (
7595 "name stranded in an ERROR",
7596 "class Drawer {\n var open = false\n fun toggle() { open = !open }\n}\n",
7597 [6, 8],
7598 ),
7599 // `val`: `suspend` never parses as a name.
7600 ("suspend as a name", "val suspend = 1\n", [1, 1]),
7601 // `fun`, `()`, `:` x2, `=`: a modifier list followed by an
7602 // inserted zero-width parameter name.
7603 (
7604 "parameter named vararg",
7605 "fun f(vararg: Int): Int = vararg\n",
7606 [4, 5],
7607 ),
7608 // The same row with a comment before the inserted name: an
7609 // extra, so the name is still the next token. It scored
7610 // [5, 6], a phantom `vararg` operator.
7611 (
7612 "parameter named vararg, commented",
7613 "fun f(vararg /*note*/: Int): Int = vararg\n",
7614 [4, 5],
7615 ),
7616 // Control: a real `vararg` before a commented parameter keeps
7617 // its operator. `vararg`, `fun`, `()`, `:`, `{}`.
7618 (
7619 "vararg modifier, commented",
7620 "fun f(vararg /*note*/ x: Int) {}\n",
7621 [5, 5],
7622 ),
7623 // `class`, `()`, `:`
7624 ("class parameter named open", "class C(open: Int)\n", [3, 3]),
7625 // `val`, `=`, `{}`, `:`
7626 (
7627 "lambda parameter named open",
7628 "val l = { open: Int -> open }\n",
7629 [4, 4],
7630 ),
7631 // Control for the gate: a real `suspend` in a function type's
7632 // `type_modifiers` and in a declaration's `modifiers` bills
7633 // twice. `val`, `:`, `suspend` x2, `()` x2, `->`, `=`, `{}` x2,
7634 // `fun`.
7635 (
7636 "suspend modifiers",
7637 "val f: suspend () -> Unit = {}\nsuspend fun g() {}\n",
7638 [8, 11],
7639 ),
7640 ];
7641 assert_operator_rows::<KotlinParser>("foo.kt", ROWS);
7642 }
7643
7644 /// #1558: `reified` is the one Kotlin modifier spelled as an aliased
7645 /// named kind. `KotlinCode::is_primitive` keys it by its text, so
7646 /// `bca ops` lists the word written, not the grammar's
7647 /// `reification_modifier`. `n1` cannot see the difference — the kind
7648 /// has one spelling either way — so the vocabulary is the only place
7649 /// this is observable.
7650 #[cfg(feature = "kotlin")]
7651 #[test]
7652 fn kotlin_reified_is_listed_by_its_text_1558() {
7653 let ops = ops_of::<KotlinParser>("fun <reified T> f() {}\n", "foo.kt");
7654 assert!(
7655 ops.operators.iter().any(|op| op == "reified"),
7656 "{:?}",
7657 ops.operators
7658 );
7659 assert!(
7660 !ops.operators.iter().any(|op| op == "reification_modifier"),
7661 "{:?}",
7662 ops.operators
7663 );
7664 }
7665
7666 /// #1564: `typealias`, `init`, `where`, `by`, `get` and `set` bill
7667 /// as Kotlin's other declaration keywords do, and as C# bills its
7668 /// `get` / `set` / `init` / `where`. Each is the keyword leaf, never
7669 /// its wrapper. The rows after "accessors" are controls: the
7670 /// `@get:` / `@set:` use-site targets and the words used as names
7671 /// add no operator.
7672 #[cfg(feature = "kotlin")]
7673 #[test]
7674 fn kotlin_declaration_keywords_are_operators_1564() {
7675 const ROWS: &[(&str, &str, [u64; 2])] = &[
7676 // `typealias`, `=`
7677 ("typealias", "typealias A = Int\n", [2, 2]),
7678 // `class`, `init`, `{}` x2
7679 ("init", "class C {\n init {}\n}\n", [3, 4]),
7680 // `class`, `<>`, `where`, `:`
7681 ("class where", "class C<T> where T : Any\n", [4, 4]),
7682 // `fun`, `<>`, `()`, `where`, `:`, `{}`
7683 ("function where", "fun <T> f() where T : Any {}\n", [6, 6]),
7684 // `val`, `by`, `{}`
7685 ("property delegate", "val d by lazy { 1 }\n", [3, 3]),
7686 // `class`, `()`, `:` x2, `by`
7687 ("class delegation", "class D(d: I) : I by d\n", [4, 5]),
7688 // `var`, `:`, `=` x3, `get`, `()` x2, `set`, `{}`
7689 (
7690 "accessors",
7691 "var p: Int = 0\n get() = field\n set(v) { field = v }\n",
7692 [7, 10],
7693 ),
7694 // `var`, `=`, `:` x2: the use-site targets bill nothing, as
7695 // `@field:` does
7696 ("use-site targets", "@get:A @set:B var q = 0\n", [3, 4]),
7697 // `val`, `=`
7698 (
7699 "names",
7700 "val get = 1\nval set = 2\nval by = 3\nval where = 4\nval init = 5\n",
7701 [2, 10],
7702 ),
7703 // `fun`, `()` x4, `:`, `{}`, `.` x2
7704 (
7705 "calls",
7706 "fun g(x: X) {\n x.get(by)\n x.set(where)\n init(x)\n}\n",
7707 [5, 9],
7708 ),
7709 // `fun`, `()` x2, `{}`, `val`, `=`: valid Kotlin that kotlin-ng
7710 // misreads, leaving the call's `get` leaf in an `ERROR`
7711 (
7712 "call after a local val",
7713 "fun g() {\n val f = 6\n get(1)\n}\n",
7714 [5, 6],
7715 ),
7716 ];
7717 assert_operator_rows::<KotlinParser>("foo.kt", ROWS);
7718 }
7719
7720 /// #1564: the six words are listed by their own spelling, and the
7721 /// accessor body's `field` stays an operand like any variable.
7722 #[cfg(feature = "kotlin")]
7723 #[test]
7724 fn kotlin_declaration_keyword_vocabulary_1564() {
7725 let ops = ops_of::<KotlinParser>(
7726 "typealias A = Int\nclass C<T> where T : Any {\n init {}\n \
7727 val d by lazy { 1 }\n var p: Int = 0\n get() = field\n \
7728 set(v) { field = v }\n}\n",
7729 "foo.kt",
7730 );
7731 for word in ["typealias", "init", "where", "by", "get", "set"] {
7732 assert!(
7733 ops.operators.iter().any(|op| op == word),
7734 "{word}: {:?}",
7735 ops.operators
7736 );
7737 }
7738 assert!(
7739 ops.operands.iter().any(|op| op == "field"),
7740 "{:?}",
7741 ops.operands
7742 );
7743
7744 // The words used as names are operands, not merely non-operators:
7745 // the operator rows above cannot see a name that bills nothing.
7746 let names = ops_of::<KotlinParser>(
7747 "fun get(x: X) {\n val set = 1\n val by = 2\n x.init(where(set, by))\n}\n",
7748 "foo.kt",
7749 );
7750 for word in ["init", "where", "by", "get", "set"] {
7751 assert!(
7752 names.operands.iter().any(|op| op == word),
7753 "{word}: {:?}",
7754 names.operands
7755 );
7756 assert!(
7757 !names.operators.iter().any(|op| op == word),
7758 "{word}: {:?}",
7759 names.operators
7760 );
7761 }
7762 }
7763
7764 /// #1557 rows shared by the two C++ clones: the declaration keywords
7765 /// #1552 left out, one per row, plus the two disambiguator positions
7766 /// (`typename T::x`, `x.template f`) that bill the same leaf.
7767 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
7768 const CPP_MORE_DECLARATION_KEYWORD_ROWS: &[(&str, &str, [u64; 2])] = &[
7769 // `namespace`, `{}`
7770 ("namespace", "namespace N {}\n", [2, 2]),
7771 // `typedef`, `int`, `;`
7772 ("typedef", "typedef int T;\n", [3, 3]),
7773 // `using`, `=`, `int`, `;`
7774 ("alias declaration", "using U = int;\n", [4, 4]),
7775 // `using`, `namespace`, `;`
7776 ("using directive", "using namespace std;\n", [3, 3]),
7777 // `template`, `<>`, `typename`, `;` and the `struct` of `S`
7778 (
7779 "template declaration",
7780 "template <typename T> struct S;\n",
7781 [5, 5],
7782 ),
7783 // `typename`, `::`, `;`
7784 ("dependent type", "typename T::type x;\n", [3, 3]),
7785 // `void`, `()` twice, `{}`, `.`, `template`, `<>`, `int`, `;`
7786 (
7787 "template disambiguator",
7788 "void g() { x.template f<int>(); }\n",
7789 [8, 9],
7790 ),
7791 ];
7792
7793 /// One call per clone, as for the #1552 rows.
7794 #[cfg(all(feature = "cpp", feature = "mozcpp"))]
7795 #[test]
7796 fn cpp_more_declaration_keywords_are_operators_1557() {
7797 assert_operator_rows::<CppParser>("foo.cpp", CPP_MORE_DECLARATION_KEYWORD_ROWS);
7798 assert_operator_rows::<MozcppParser>("foo.cpp", CPP_MORE_DECLARATION_KEYWORD_ROWS);
7799 }
7800
7801 /// #1557: `typedef` is a C keyword, so C and ObjC bill it as C++
7802 /// does — a header scores alike whichever grammar parses it.
7803 #[cfg(any(feature = "c", feature = "objc"))]
7804 const C_TYPEDEF_ROWS: &[(&str, &str, [u64; 2])] = &[
7805 // `typedef`, `int`, `;`
7806 ("typedef", "typedef int T;\n", [3, 3]),
7807 // `typedef`, `struct`, `{}`, `int`, `;` twice
7808 ("typedef struct", "typedef struct S { int a; } S;\n", [5, 6]),
7809 ];
7810
7811 #[cfg(any(feature = "c", feature = "objc"))]
7812 #[test]
7813 fn c_family_typedef_is_an_operator_1557() {
7814 #[cfg(feature = "c")]
7815 assert_operator_rows::<CParser>("foo.c", C_TYPEDEF_ROWS);
7816 #[cfg(feature = "objc")]
7817 assert_operator_rows::<ObjcParser>("foo.m", C_TYPEDEF_ROWS);
7818 }
7819
7820 /// #1557: the TypeScript declaration keywords #1552 left out. The
7821 /// "keywords as names" row pins that each word, used as a *name*,
7822 /// stays an operand: the grammar emits an `identifier` there. The two
7823 /// exported-binding rows are the one place it emits the `type` leaf
7824 /// for a name, which must bill nothing; the last two rows are their
7825 /// controls, a type-only specifier and re-export whose `type` still
7826 /// bills.
7827 #[cfg(feature = "typescript")]
7828 const TS_DECLARATION_KEYWORD_ROWS: &[(&str, &str, [u64; 2])] = &[
7829 // `type`, `=`, `number`, `;`
7830 ("type alias", "type T = number;\n", [4, 4]),
7831 // `namespace`, `{}`
7832 ("namespace", "namespace N {}\n", [2, 2]),
7833 // `module`, `{}`. Billing the `module` declaration node instead
7834 // of its keyword leaf would score this row the same: every
7835 // spelling probed (bodiless, quoted, dotted, exported, ERROR
7836 // recovery) emits the two together, so only the getter's
7837 // choice of the leaf, not this row, keeps them from both billing.
7838 ("module", "module M {}\n", [2, 2]),
7839 // `declare`, `module`, `{}`
7840 ("ambient module", "declare module \"m\" {}\n", [3, 3]),
7841 // `declare`, `global`, `{}`
7842 ("global augmentation", "declare global {}\n", [3, 3]),
7843 // `import`, `type`, `{}`, `from`, `;`
7844 (
7845 "type-only import",
7846 "import type { A } from \"a\";\n",
7847 [5, 5],
7848 ),
7849 // `let`, `=` twice, `;` twice, `.` — no keyword
7850 (
7851 "keywords as names",
7852 "let type = 1;\nmodule.exports = declare;\n",
7853 [4, 6],
7854 ),
7855 // `const`, `=`, `;` twice, `export`, `{}` — no keyword. The
7856 // grammar recovers an exported binding named `type` as the
7857 // keyword leaf, under an `ERROR` or beside one in the specifier.
7858 (
7859 "exported binding named type",
7860 "const type = 1;\nexport { type };\n",
7861 [5, 6],
7862 ),
7863 (
7864 "renamed exported binding named type",
7865 "const type = 1;\nexport { type as kind };\n",
7866 [5, 6],
7867 ),
7868 // `export`, `{}`, `type`, `from`, `;` — a type-only specifier
7869 // parses cleanly, so its keyword still bills.
7870 (
7871 "type-only export specifier",
7872 "export { type Foo } from \"m\";\n",
7873 [5, 5],
7874 ),
7875 // `export`, `type`, `*`, `from`, `;` — the pinned grammar fails
7876 // TS 5.0's type-only re-export into an `ERROR` that hangs from the
7877 // statement, not from an export clause, and its keyword bills.
7878 (
7879 "type-only re-export",
7880 "export type * from \"./types\";\n",
7881 [5, 5],
7882 ),
7883 ];
7884
7885 /// #1557: a TypeScript type name is an operand, as C#, Rust and the
7886 /// C family bill theirs. Each row's names are `type_identifier`
7887 /// leaves except where noted, and before #1557 none of them billed.
7888 #[cfg(feature = "typescript")]
7889 const TS_TYPE_NAME_OPERAND_ROWS: &[(&str, &str, [u64; 2])] = &[
7890 // `T`
7891 ("alias name", "type T = number;\n", [1, 1]),
7892 // `I`
7893 ("interface name", "interface I {}\n", [1, 1]),
7894 // `C`
7895 ("class name", "class C {}\n", [1, 1]),
7896 // `x` (an identifier), `Foo`
7897 ("annotation", "let x: Foo;\n", [2, 2]),
7898 // `m`, `Map`, `K` twice
7899 ("generic", "let m: Map<K, K>;\n", [3, 4]),
7900 // `f`, `a` (identifiers), `U` twice
7901 ("type parameter", "function f<U>(a: U) {}\n", [3, 4]),
7902 // `q`, `ns` (identifiers), `T` — the `nested_type_identifier`
7903 // wrapper is unlisted, so its text is not a fourth operand
7904 ("qualified type", "let q: ns.T;\n", [3, 3]),
7905 // `C`, `I`
7906 ("implements", "class C implements I {}\n", [2, 2]),
7907 ];
7908
7909 #[cfg(feature = "typescript")]
7910 #[test]
7911 fn typescript_declaration_keywords_and_type_names_1557() {
7912 assert_operator_rows::<TypescriptParser>("foo.ts", TS_DECLARATION_KEYWORD_ROWS);
7913 assert_operator_rows::<TsxParser>("foo.tsx", TS_DECLARATION_KEYWORD_ROWS);
7914 assert_operand_rows::<TypescriptParser>("foo.ts", TS_TYPE_NAME_OPERAND_ROWS);
7915 assert_operand_rows::<TsxParser>("foo.tsx", TS_TYPE_NAME_OPERAND_ROWS);
7916 }
7917
7918 /// #1561: TypeScript's type-level operator keywords bill as their
7919 /// value-level siblings `as`, `typeof` and `extends` do, each as its
7920 /// keyword leaf under an unbilled wrapper. The `asserts a is Baz` row
7921 /// pins that the `asserts` node and its keyword token bill once
7922 /// between them, and the last row that the same words used as names
7923 /// are operands. Both TypeScript grammars carry the change.
7924 #[cfg(feature = "typescript")]
7925 #[test]
7926 fn typescript_type_operator_keywords_1561() {
7927 const ROWS: &[(&str, &str, [u64; 2])] = &[
7928 // `let`, `=`, `satisfies`, `;`
7929 ("satisfies", "let s = w satisfies Sat;\n", [4, 4]),
7930 // `type`, `=`, `keyof`, `;`
7931 ("keyof", "type K = keyof T;\n", [4, 4]),
7932 // `type`, `<>` x2, `=`, `extends`, `infer`, `?`, `:`, `;`
7933 (
7934 "infer",
7935 "type I<X> = X extends Array<infer E> ? E : X;\n",
7936 [8, 9],
7937 ),
7938 // `function`, `()`, `:` x2, `unknown`, `is`, `{}`, `return`, `;`
7939 (
7940 "type predicate",
7941 "function g(a: unknown): a is Baz { return true; }\n",
7942 [8, 9],
7943 ),
7944 // `function`, `()`, `:` x2, `unknown`, `asserts`, `is`, `{}`
7945 (
7946 "asserts predicate",
7947 "function h(a: unknown): asserts a is Baz {}\n",
7948 [7, 8],
7949 ),
7950 // `function`, `()`, `:` x2, `unknown`, `asserts`, `{}`
7951 (
7952 "bare asserts",
7953 "function k(a: unknown): asserts a {}\n",
7954 [6, 7],
7955 ),
7956 // `const`, `=` and `;` five times each: the words are names
7957 (
7958 "names",
7959 "const keyof = 1;\nconst infer = 2;\nconst is = 3;\nconst asserts = 4;\n\
7960 const satisfies = 5;\n",
7961 [3, 15],
7962 ),
7963 ];
7964 assert_operator_rows::<TypescriptParser>("foo.ts", ROWS);
7965 assert_operator_rows::<TsxParser>("foo.tsx", ROWS);
7966 }
7967
7968 /// #1563: TypeScript bills its member modifiers as C# and Kotlin bill
7969 /// theirs, each as the keyword leaf and never as the
7970 /// `accessibility_modifier` / `override_modifier` wrapper. The rows
7971 /// after the class members are the same leaves outside a class; the
7972 /// last two are the words used as names, which add no operator.
7973 #[cfg(feature = "typescript")]
7974 const TS_MEMBER_MODIFIER_ROWS: &[(&str, &str, [u64; 2])] = &[
7975 // `class`, `{}`, `public`, `private`, `protected`, `=` x3, `;` x3
7976 (
7977 "visibility",
7978 "class C {\n public a = 1;\n private b = 2;\n protected c = 3;\n}\n",
7979 [7, 11],
7980 ),
7981 // `class`, `extends`, `{}`, `static`, `readonly`, `override`,
7982 // `=` x2, `;` x2
7983 (
7984 "static, readonly, override",
7985 "class D extends B {\n static readonly x = 1;\n override y = 2;\n}\n",
7986 [8, 10],
7987 ),
7988 // `abstract` x2, `class`, `{}`, `()`, `:`, `void`, `;`
7989 (
7990 "abstract",
7991 "abstract class A {\n abstract m(): void;\n}\n",
7992 [7, 8],
7993 ),
7994 // `class`, `{}`, `accessor`, `=`, `;`
7995 ("accessor", "class E {\n accessor q = 1;\n}\n", [5, 5]),
7996 // `class`, `{}` x3, `static` x2, `()`
7997 (
7998 "static block and method",
7999 "class F {\n static {}\n static m() {}\n}\n",
8000 [4, 7],
8001 ),
8002 // `class`, `{}` x2, `()`, `private`, `readonly`, `:` x2, `number`,
8003 // `,`, `public`, `string`
8004 (
8005 "parameter properties",
8006 "class G {\n constructor(private readonly p: number, public o: string) {}\n}\n",
8007 [10, 12],
8008 ),
8009 // `type`, `=`, `readonly`, `string`, `[]`, `;`
8010 ("readonly type", "type R = readonly string[];\n", [6, 6]),
8011 // `type`, `=`, `{}`, `-`, `readonly`, `[]`, `in`, `:`, `;`
8012 (
8013 "mapped type modifier",
8014 "type M = { -readonly [k in K]: T };\n",
8015 [9, 9],
8016 ),
8017 // `type`, `=`, `abstract`, `new`, `()`, `=>`, `void`, `;`
8018 (
8019 "abstract constructor type",
8020 "type A = abstract new () => void;\n",
8021 [8, 8],
8022 ),
8023 // `let`, `const` x2, `=` x3, `;` x4, `.`, `()`, `{}`, `:` x5,
8024 // `,` x4 — no modifier
8025 (
8026 "names",
8027 "let readonly = 1;\nobj.static;\nconst a = abstract();\n\
8028 const o = { public: 1, private: 2, protected: 3, override: 4, accessor: 5 };\n",
8029 [9, 22],
8030 ),
8031 // `class`, `{}` x2, `static`, `=`, `;` x2, `()` — the modifier
8032 // bills once; the field named `static`, the method named
8033 // `readonly` and the field named `public` bill none
8034 (
8035 "member names",
8036 "class K {\n static static = 1;\n readonly() {}\n public;\n}\n",
8037 [6, 8],
8038 ),
8039 ];
8040
8041 #[cfg(feature = "typescript")]
8042 #[test]
8043 fn typescript_member_modifiers_are_operators_1563() {
8044 assert_operator_rows::<TypescriptParser>("foo.ts", TS_MEMBER_MODIFIER_ROWS);
8045 assert_operator_rows::<TsxParser>("foo.tsx", TS_MEMBER_MODIFIER_ROWS);
8046 }
8047
8048 /// #1563: the issue's own fixture, by vocabulary. Every modifier
8049 /// reaches the operator list once, by its word and never by its
8050 /// wrapper's kind name, and the names stay the only operands.
8051 #[cfg(feature = "typescript")]
8052 #[test]
8053 fn typescript_member_modifier_vocabulary_1563() {
8054 const SOURCE: &str = "abstract class C {\n declare x: number;\n \
8055 public static readonly y = 1;\n private z = 2;\n \
8056 protected override w = 3;\n abstract m(): void;\n \
8057 accessor q = 4;\n}\n";
8058 const OPERATORS: &[&str] = &[
8059 "()",
8060 ":",
8061 ";",
8062 "=",
8063 "abstract",
8064 "accessor",
8065 "class",
8066 "declare",
8067 "number",
8068 "override",
8069 "private",
8070 "protected",
8071 "public",
8072 "readonly",
8073 "static",
8074 "void",
8075 "{}",
8076 ];
8077 const OPERANDS: &[&str] = &["1", "2", "3", "4", "C", "m", "q", "w", "x", "y", "z"];
8078 for (file, ops) in [
8079 ("foo.ts", ops_of::<TypescriptParser>(SOURCE, "foo.ts")),
8080 ("foo.tsx", ops_of::<TsxParser>(SOURCE, "foo.tsx")),
8081 ] {
8082 assert_eq!(ops.operators, OPERATORS, "{file}");
8083 assert_eq!(ops.operands, OPERANDS, "{file}");
8084 }
8085 }
8086
8087 /// #1563: the modifier words used as names are operands and never
8088 /// operators, in an expression, a member access, a call, an object
8089 /// key and a class member name. No getter edit makes this fail: the
8090 /// modifier arms key on the keyword kinds, and the grammar emits an
8091 /// identifier for each name. It pins that grammar property, so a
8092 /// grammar bump or a text-keyed arm that bills a name trips it.
8093 #[cfg(feature = "typescript")]
8094 #[test]
8095 fn typescript_modifier_words_as_names_are_operands_1563() {
8096 const SOURCE: &str = "let readonly = 1;\nobj.static;\nconst a = abstract();\n\
8097 const o = { public: 1, private: 2, protected: 3, override: 4, \
8098 accessor: 5 };\nclass K { static() {} }\n";
8099 const KEYWORDS: &[&str] = &[
8100 "readonly",
8101 "static",
8102 "abstract",
8103 "public",
8104 "private",
8105 "protected",
8106 "override",
8107 "accessor",
8108 ];
8109 assert_keywords_are_operands_only::<TypescriptParser>(SOURCE, "foo.ts", KEYWORDS);
8110 assert_keywords_are_operands_only::<TsxParser>(SOURCE, "foo.tsx", KEYWORDS);
8111 }
8112
8113 /// #1563: JavaScript's `static` bills as TypeScript's does, in a
8114 /// field, a method and a `static { … }` block. `static get` with the
8115 /// name on the next line is one token, so it is one operator. The
8116 /// last row is the word used as names, which adds no operator.
8117 #[cfg(any(feature = "javascript", feature = "mozjs"))]
8118 const JS_STATIC_ROWS: &[(&str, &str, [u64; 2])] = &[
8119 // `class`, `{}`, `static`, `=`, `;`
8120 ("static field", "class K {\n static x = 1;\n}\n", [5, 5]),
8121 // `class`, `{}` x3, `static` x2, `()`
8122 (
8123 "static method and block",
8124 "class K {\n static m() {}\n static {}\n}\n",
8125 [4, 7],
8126 ),
8127 // `class`, `{}` x2, `static`, `get`, `()`
8128 (
8129 "static getter",
8130 "class K {\n static get y() {}\n}\n",
8131 [5, 6],
8132 ),
8133 // `class`, `{}` x2, `static get`, `()`
8134 (
8135 "static get before a newline",
8136 "class K {\n static get\n y() {}\n}\n",
8137 [4, 5],
8138 ),
8139 // `.`, `;` x3, `()`, `var`, `=`, `{}`, `:` — no `static`
8140 (
8141 "names",
8142 "obj.static;\nstatic();\nvar o = { static: 1 };\n",
8143 [7, 9],
8144 ),
8145 ];
8146
8147 #[cfg(any(feature = "javascript", feature = "mozjs"))]
8148 #[test]
8149 fn javascript_static_is_an_operator_1563() {
8150 const NAMES: &str = "obj.static;\nstatic();\nvar o = { static: 1 };\n\
8151 class K { static static = 1; }\n";
8152 #[cfg(feature = "javascript")]
8153 {
8154 assert_operator_rows::<JavascriptParser>("foo.js", JS_STATIC_ROWS);
8155 let ops = ops_of::<JavascriptParser>(NAMES, "foo.js");
8156 assert!(ops.operands.iter().any(|o| o == "static"), "{ops:?}");
8157 assert!(ops.operators.iter().any(|o| o == "static"), "{ops:?}");
8158 }
8159 #[cfg(feature = "mozjs")]
8160 {
8161 assert_operator_rows::<MozjsParser>("foo.js", JS_STATIC_ROWS);
8162 let ops = ops_of::<MozjsParser>(NAMES, "foo.js");
8163 assert!(ops.operands.iter().any(|o| o == "static"), "{ops:?}");
8164 assert!(ops.operators.iter().any(|o| o == "static"), "{ops:?}");
8165 }
8166 }
8167
8168 #[cfg(feature = "bash")]
8169 #[test]
8170 fn bash_operators_and_operands() {
8171 check_metrics::<BashParser>(
8172 "#!/bin/bash
8173f() {
8174 local x=1
8175 if [ $x -eq 1 ]; then
8176 echo 'one'
8177 fi
8178}",
8179 "foo.sh",
8180 |metric| {
8181 // Operators (9 unique, 9 occurrences): the opening
8182 // delimiters `()`/`{}`/`[]` (each folded to one glyph and
8183 // counted once per balanced pair, #695 — the closers no
8184 // longer add a second operator), `local`, `=`, `if`,
8185 // `then`, `fi`, `;`.
8186 // Operands (6 unique, 7 occurrences): `f`, `x` (the
8187 // assignment LHS `variable_name`, kind 160), `1` (twice:
8188 // `=1` and `-eq 1`), `$x` (the `simple_expansion` — its
8189 // inner `variable_name` leaf is now suppressed so `$x`
8190 // counts once, #695), `echo`, `'one'`.
8191 // N2 was 8 before #1351: `echo` counted twice, once as the
8192 // `command_name` wrapper and once as the `word` it wraps.
8193 assert_eq!(metric.halstead.unique_operators(), 9);
8194 assert_eq!(metric.halstead.total_operators(), 9);
8195 assert_eq!(metric.halstead.unique_operands(), 6);
8196 assert_eq!(metric.halstead.total_operands(), 7);
8197 insta::assert_json_snapshot!(metric.halstead);
8198 },
8199 );
8200 }
8201
8202 #[cfg(feature = "bash")]
8203 #[test]
8204 fn bash_interpolated_string_no_double_count() {
8205 // Regression: issue #180. A double-quoted Bash string containing
8206 // `$name`, `${name[…]}`, or `$(cmd)` used to be classified as a
8207 // Halstead operand AND have its inner `simple_expansion` /
8208 // `expansion` / `command_substitution` children classified as
8209 // operands too. We now skip the wrapping literal when it has an
8210 // expansion child so only the inner expansion contributes.
8211 //
8212 // expected: operands across `a="plain"\nb="$x"\n` —
8213 // line 1: variable_name `a`, plain string `"plain"` (no
8214 // expansion, still operand) → 2.
8215 // line 2: variable_name `b`, wrapping `"$x"` skipped (has
8216 // expansion), `simple_expansion` `$x` (its inner
8217 // variable_name `x` leaf is suppressed under #695) → 2.
8218 // Total unique operands: 4 (`a`, `b`, `"plain"`, `$x`), each
8219 // appearing once → N2 = 4. Before #695 the inner `x` leaf of
8220 // `$x` was also counted (u_operands = 5, N2 = 5); before the
8221 // earlier #180 fix the wrapping `"$x"` literal was counted too.
8222 // The `=` is the only operator; appears twice (N1 = 2, n1 = 1).
8223 check_metrics::<BashParser>("a=\"plain\"\nb=\"$x\"\n", "foo.sh", |metric| {
8224 assert_eq!(metric.halstead.unique_operators(), 1);
8225 assert_eq!(metric.halstead.total_operators(), 2);
8226 assert_eq!(metric.halstead.unique_operands(), 4);
8227 assert_eq!(metric.halstead.total_operands(), 4);
8228 insta::assert_json_snapshot!(metric.halstead);
8229 });
8230 }
8231
8232 #[cfg(feature = "elixir")]
8233 #[test]
8234 fn elixir_interpolated_string_no_double_count() {
8235 // Regression: issue #180. Without the fix, an interpolated
8236 // Elixir `String` was classified as a single operand while its
8237 // inner `interpolation` identifier was also walked and
8238 // classified as its own operand — double-counting the
8239 // interpolated identifier's contribution to `N2`.
8240 //
8241 // expected: operand contributions for
8242 // `def greet(name) do\n msg = "Hi #{name}"\nend\n` —
8243 // `def`, `greet`, `name` (param), `msg`, and the inner `name`
8244 // (inside `#{...}`). With the fix, the wrapping
8245 // `"Hi #{name}"` literal is skipped (has `Interpolation`
8246 // child), so `name` is the only repeated operand:
8247 // u_operands = 4 (def, greet, name, msg), N2 = 5. Without the
8248 // fix, the wrapping literal would also count → u_operands = 5,
8249 // N2 = 6. Operators: `do`, `end`, `(`, `=` → u = N = 4.
8250 // Only the *opening* delimiters count after #695, so the `)`
8251 // and the `}` interpolation closer add no operator; #1314 then
8252 // dropped the `#{` opener too, on the rule that an
8253 // interpolation opener is spelling rather than an operation
8254 // (was 5 here, and 7 before #695).
8255 check_metrics::<ElixirParser>(
8256 "def greet(name) do\n msg = \"Hi #{name}\"\nend\n",
8257 "foo.ex",
8258 |metric| {
8259 assert_eq!(metric.halstead.unique_operators(), 4);
8260 assert_eq!(metric.halstead.total_operators(), 4);
8261 assert_eq!(metric.halstead.unique_operands(), 4);
8262 assert_eq!(metric.halstead.total_operands(), 5);
8263 insta::assert_json_snapshot!(metric.halstead);
8264 },
8265 );
8266 }
8267
8268 #[cfg(feature = "elixir")]
8269 #[test]
8270 fn elixir_plain_string_still_operand() {
8271 // The fix for #180 only skips wrapping literals that contain
8272 // interpolation; a plain `"hello"` must still contribute exactly
8273 // one operand. expected: `def`, `f`, `"hello"` → 3 unique
8274 // operands (n2 = 3), each appearing once (N2 = 3).
8275 check_metrics::<ElixirParser>("def f do\n \"hello\"\nend\n", "foo.ex", |metric| {
8276 assert_eq!(metric.halstead.unique_operands(), 3);
8277 assert_eq!(metric.halstead.total_operands(), 3);
8278 });
8279 }
8280
8281 #[cfg(feature = "elixir")]
8282 #[test]
8283 fn elixir_boolean_and_nil_literals_count_once() {
8284 // Regression: issue #1253. `boolean: choice("true", "false")`
8285 // and `nil: "nil"` each wrap a keyword leaf, and both the
8286 // wrapper and the leaf sat in the operand arm — so every
8287 // literal occurrence added +1 to N2. Operands are keyed by
8288 // source text, so the duplicate collapsed into the same
8289 // vocabulary entry and n2 stayed correct, which is why nothing
8290 // caught it.
8291 //
8292 // Source is the issue's reproducer plus a repeat of `true` and
8293 // `nil`, so N2 exceeds n2 and the assertions can tell "counted
8294 // once per occurrence" from "deduplicated into the vocabulary".
8295 // All three keywords appear, so restoring any one of `True`,
8296 // `False`, or `Nil2` to the operand arm trips this test.
8297 //
8298 // Operands by text key: `x`, `y`, `z`, `w`, `v`, `true` × 2,
8299 // `nil` × 2, `false` ⇒ n2 = 8, N2 = 10. Before the fix each of
8300 // the five literals counted twice ⇒ N2 = 15.
8301 //
8302 // This also guards the drift in the other direction. Elixir
8303 // classifies the wrapper and drops the leaf outright rather
8304 // than parent-guarding it, so a grammar bump that stopped
8305 // emitting `boolean` / `nil` would leave the leaves unclassified
8306 // and the literals would vanish from N2 entirely (⇒ 5 / 5)
8307 // rather than merely being miscounted.
8308 check_metrics::<ElixirParser>(
8309 "x = true\ny = nil\nz = false\nw = true\nv = nil\n",
8310 "foo.ex",
8311 |metric| {
8312 assert_eq!(metric.halstead.unique_operands(), 8);
8313 assert_eq!(metric.halstead.total_operands(), 10);
8314 },
8315 );
8316 }
8317
8318 #[cfg(feature = "elixir")]
8319 #[test]
8320 fn elixir_reserved_word_after_a_dot_stays_an_operand() {
8321 // Companion to the test above (#1253). Elixir drops `True` /
8322 // `False` / `Nil2` from the operand arm outright, which is only
8323 // safe because the one grammar position that accepts a reserved
8324 // word outside the `boolean` / `nil` wrapper — the right-hand
8325 // side of a remote dot — aliases it to `identifier`. This pins
8326 // that alias: if a grammar bump emitted the bare keyword there
8327 // instead, `Foo.nil` and `Foo.true` would silently stop
8328 // contributing an operand.
8329 //
8330 // Source: a = Foo.nil / b = Foo.true / c = nil
8331 //
8332 // Operands by text key: `a`, `Foo` × 2, `nil` × 2 (the aliased
8333 // identifier and the real literal, which share a text key),
8334 // `b`, `true`, `c` ⇒ n2 = 6, N2 = 8. Losing the alias drops the
8335 // two dotted references ⇒ N2 = 6.
8336 check_metrics::<ElixirParser>("a = Foo.nil\nb = Foo.true\nc = nil\n", "foo.ex", |metric| {
8337 assert_eq!(metric.halstead.unique_operands(), 6);
8338 assert_eq!(metric.halstead.total_operands(), 8);
8339 });
8340 }
8341
8342 #[cfg(feature = "elixir")]
8343 #[test]
8344 fn elixir_interpolated_sigil_no_double_count() {
8345 // Sigils mirror strings under #180. For `~r/foo#{name}/`, the
8346 // wrapping `Sigil` is skipped, but `SigilName` (`r`) and the
8347 // inner `name` identifier each contribute one operand.
8348 // expected: `def`, `f`, `name` (param), `re`, `r` (sigil name),
8349 // `name` (inside `#{...}`) → u_operands = 5, N2 = 6 (`name`
8350 // twice).
8351 check_metrics::<ElixirParser>(
8352 "def f(name) do\n re = ~r/foo#{name}/\nend\n",
8353 "foo.ex",
8354 |metric| {
8355 assert_eq!(metric.halstead.unique_operands(), 5);
8356 assert_eq!(metric.halstead.total_operands(), 6);
8357 },
8358 );
8359 }
8360
8361 #[cfg(feature = "elixir")]
8362 #[test]
8363 fn elixir_interpolated_charlist_no_double_count() {
8364 // Charlists mirror strings and sigils under #180. The
8365 // `E::String | E::Charlist | E::Sigil` arm in `get_op_type`
8366 // skips any wrapping literal that has an `Interpolation`
8367 // child; this test exercises the `Charlist` branch
8368 // specifically.
8369 //
8370 // expected: for `def f(name) do\n cl = 'Hi #{name}'\nend\n` —
8371 // `def`, `f`, `name` (param), `cl`, and the inner `name`
8372 // (inside `#{...}`). With the fix, the wrapping
8373 // `'Hi #{name}'` is skipped → u_operands = 4 (def, f, name,
8374 // cl), N2 = 5 (`name` twice).
8375 check_metrics::<ElixirParser>(
8376 "def f(name) do\n cl = 'Hi #{name}'\nend\n",
8377 "foo.ex",
8378 |metric| {
8379 assert_eq!(metric.halstead.unique_operands(), 4);
8380 assert_eq!(metric.halstead.total_operands(), 5);
8381 },
8382 );
8383 }
8384
8385 #[cfg(feature = "elixir")]
8386 #[test]
8387 fn elixir_sigil_delimiters_are_not_operators() {
8388 // Regression: issue #1256. Sigil delimiter tokens share their
8389 // kind ids with real operators (`SLASH`, `LPAREN`, `LBRACE`,
8390 // …) and were classified unconditionally, so `~r/abc/`
8391 // fabricated two division operators and the author's delimiter
8392 // choice moved n1/N1. The parent guard suppresses them under
8393 // `Sigil`; `~` stays the single per-sigil operator.
8394 //
8395 // expected: operators `=` × 3 and `~` × 3 → n1 = 2, N1 = 6.
8396 // Without the guard the delimiters added `/` × 2, `(`, `{` →
8397 // n1 = 5, N1 = 10. Operands: `a`, `~r/abc/i`, `r`, `i` (sigil
8398 // modifiers), `b`, `~w(one two)`, `w`, `c`, `~s{hi}`, `s` →
8399 // n2 = N2 = 10.
8400 check_metrics::<ElixirParser>(
8401 "a = ~r/abc/i\nb = ~w(one two)\nc = ~s{hi}\n",
8402 "foo.ex",
8403 |metric| {
8404 assert_eq!(metric.halstead.unique_operators(), 2);
8405 assert_eq!(metric.halstead.total_operators(), 6);
8406 assert_eq!(metric.halstead.unique_operands(), 10);
8407 assert_eq!(metric.halstead.total_operands(), 10);
8408 },
8409 );
8410 }
8411
8412 #[cfg(feature = "elixir")]
8413 #[test]
8414 fn elixir_sigil_delimiter_choice_is_invariant() {
8415 // Companion to the test above (#1256): two sigils differing
8416 // only in delimiter are the same literal, so every delimiter
8417 // choice must produce identical Halstead counts. `(` `[` `{`
8418 // `<` `/` `|` are the operator-kind delimiters the guard
8419 // covers; `"` and `'` never had an operator arm and pin the
8420 // already-correct path.
8421 //
8422 // expected per variant: operators `=`, `~` → n1 = 2, N1 = 2;
8423 // operands `x`, the sigil literal text, `w` (sigil name) →
8424 // n2 = 3, N2 = 3.
8425 for (open, close) in [
8426 ("(", ")"),
8427 ("[", "]"),
8428 ("{", "}"),
8429 ("<", ">"),
8430 ("/", "/"),
8431 ("|", "|"),
8432 ("\"", "\""),
8433 ("'", "'"),
8434 ] {
8435 assert_halstead_counts::<ElixirParser>(
8436 &format!("x = ~w{open}one two{close}\n"),
8437 "foo.ex",
8438 [2, 2, 3, 3],
8439 &format!("delimiter pair {open} {close}"),
8440 );
8441 }
8442 }
8443
8444 #[cfg(feature = "elixir")]
8445 #[test]
8446 fn elixir_standalone_operators_survive_the_sigil_guard() {
8447 // Control for #1256: the guard is parent-scoped, so the same
8448 // token kinds outside a sigil still count. Covers every guarded
8449 // kind standalone: `/` (division), `<` / `>` (comparison), `[`
8450 // and `|` (list cons), `(` (call), `{` (map literal, with its
8451 // `%`).
8452 //
8453 // expected: operators `=` × 6, `/`, `<`, `>`, `[`, `|`, `(`,
8454 // `%`, `{` → n1 = 9, N1 = 14. Operands: `x`, `a`, `b`, `y`,
8455 // `c`, `d`, `z`, `e`, `f`, `q`, `h`, `t`, `p`, `g`, `1`, `m`,
8456 // the `k:` keyword, `2` → n2 = N2 = 18.
8457 check_metrics::<ElixirParser>(
8458 "x = a / b\ny = c < d\nz = e > f\nq = [h | t]\np = g(1)\nm = %{k: 2}\n",
8459 "foo.ex",
8460 |metric| {
8461 assert_eq!(metric.halstead.unique_operators(), 9);
8462 assert_eq!(metric.halstead.total_operators(), 14);
8463 assert_eq!(metric.halstead.unique_operands(), 18);
8464 assert_eq!(metric.halstead.total_operands(), 18);
8465 },
8466 );
8467 }
8468
8469 #[cfg(feature = "elixir")]
8470 #[test]
8471 fn elixir_interpolated_sigil_keeps_inner_nodes_counting() {
8472 // Interpolation inside a sigil after #1256: the `{` delimiter
8473 // is suppressed (its parent is the `Sigil`), while the
8474 // `interpolation` child is a separate node whose inner
8475 // identifier must still count — the guard must not reach past
8476 // the delimiter tokens.
8477 //
8478 // expected: operators `=`, `~` → n1 = N1 = 2. Operands:
8479 // `v`, `s` (sigil name), `b` (interpolated identifier); the
8480 // wrapping sigil is skipped (`Interpolation` child, #180) and
8481 // `quoted_content` is unclassified → n2 = N2 = 3. The `#{`
8482 // marker was a third operator until #1314 dropped it.
8483 check_metrics::<ElixirParser>("v = ~s{a#{b} c}\n", "foo.ex", |metric| {
8484 assert_eq!(metric.halstead.unique_operators(), 2);
8485 assert_eq!(metric.halstead.total_operators(), 2);
8486 assert_eq!(metric.halstead.unique_operands(), 3);
8487 assert_eq!(metric.halstead.total_operands(), 3);
8488 });
8489
8490 // A guarded kind *inside* the interpolation: the `/` in
8491 // `#{a / b}` has `binary_operator` as its parent but the
8492 // `Sigil` as a further ancestor, so this input is the one
8493 // discriminator between the correct parent-scoped guard and a
8494 // wrong ancestor-scoped one that would swallow it.
8495 //
8496 // expected: operators `=`, `~`, `/` → n1 = N1 = 3 (the `#{`
8497 // opener stopped counting with #1314); operands `v`, `s`, `a`,
8498 // `b` → n2 = N2 = 4. The division is what this row is for, and
8499 // it still counts — the ancestor-scoped mutant drops it.
8500 check_metrics::<ElixirParser>("v = ~s{x #{a / b} y}\n", "foo.ex", |metric| {
8501 assert_eq!(metric.halstead.unique_operators(), 3);
8502 assert_eq!(metric.halstead.total_operators(), 3);
8503 assert_eq!(metric.halstead.unique_operands(), 4);
8504 assert_eq!(metric.halstead.total_operands(), 4);
8505 });
8506 }
8507
8508 #[cfg(feature = "bash")]
8509 #[test]
8510 fn bash_all_expansion_kinds_skip_wrapper() {
8511 // Exercises every node kind tested by
8512 // `bash_string_has_expansion`: `simple_expansion` (`$v`),
8513 // `expansion` (`${v[0]}`), `command_substitution` (`$(date)`),
8514 // and `arithmetic_expansion` (`$((1+2))`). A typo replacing
8515 // one kind with an aliased neighbour in `language_bash.rs`
8516 // (e.g., `ExpansionBody` instead of `Expansion`) would leave
8517 // the corresponding wrapping string counted as an operand and
8518 // shift the totals.
8519 //
8520 // expected: operands across the four lines —
8521 // line 1 `a="$v"`: var_name `a`, simple_expansion `$v` (its
8522 // inner var_name `v` leaf is suppressed under #695; wrapper
8523 // skipped) → 2
8524 // line 2 `b="${v[0]}"`: var_name `b`, var_name `v` (inside
8525 // subscript — parent is `expansion`, not `simple_expansion`,
8526 // so it still counts), number `0` (wrapper skipped,
8527 // `expansion` itself is not in the operand list) → 3
8528 // line 3 `c="$(date)"`: var_name `c`, the `word` `date` under
8529 // the `command_name` (wrapper skipped, `command_substitution`
8530 // not in operand list, and since #1351 the `command_name`
8531 // wrapper is not either) → 2
8532 // line 4 `d="$((1+2))"`: var_name `d`, numbers `1` and `2`
8533 // (wrapper skipped, `arithmetic_expansion` not in operand
8534 // list) → 3
8535 // Unique operands: a, b, c, d, $v, v, 0, date, 1, 2 → 10. Total
8536 // occurrences: 10 (`v` appears once — only line 2's subscript
8537 // leaf; line 1's `$v` inner leaf is suppressed — and `date` once,
8538 // as the `word`; before #1351 the `command_name` wrapping it
8539 // added a second `date` and N2 was 11). Operators after
8540 // #695: only the openers `[` (folded `[]`) and `+`, plus `=` four
8541 // times — the `}`/`)`/`))`/`]` closers no longer count.
8542 check_metrics::<BashParser>(
8543 "a=\"$v\"\nb=\"${v[0]}\"\nc=\"$(date)\"\nd=\"$((1+2))\"\n",
8544 "foo.sh",
8545 |metric| {
8546 assert_eq!(metric.halstead.unique_operators(), 3);
8547 assert_eq!(metric.halstead.total_operators(), 6);
8548 assert_eq!(metric.halstead.unique_operands(), 10);
8549 assert_eq!(metric.halstead.total_operands(), 10);
8550 },
8551 );
8552 }
8553
8554 /// Regression for #695. A bare `$x` (outside any string) parses as a
8555 /// `simple_expansion` wrapping a `variable_name` leaf — and `$?` / `$1`
8556 /// as a `simple_expansion` wrapping a `special_variable_name` leaf. Both
8557 /// the wrapper and the inner leaf used to be classified as operands, so
8558 /// each bare variable reference double-counted (the same hazard Tcl
8559 /// guards with its `Id2` exclusion and iRules with a parent check). The
8560 /// `variable_name` / `special_variable_name` arm now yields `Unknown`
8561 /// when its parent is a `simple_expansion`, so `$x` contributes exactly
8562 /// one operand while the assignment LHS `variable_name` (`x` in `x=…`,
8563 /// parent is `variable_assignment`) still counts.
8564 #[cfg(feature = "bash")]
8565 #[test]
8566 fn bash_bare_variable_no_double_count() {
8567 let source = "x=1\necho $x\necho $?\n";
8568 let path = PathBuf::from("foo.sh");
8569 let parser = BashParser::new(source.as_bytes().to_vec(), &path, None);
8570 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
8571 let bare_x = ops.operands.iter().filter(|o| o.as_str() == "$x").count();
8572 let special = ops.operands.iter().filter(|o| o.as_str() == "$?").count();
8573 // Each bare reference is exactly one operand; the inner leaf is not
8574 // double-counted. If the guard regressed, the inner `variable_name`
8575 // `x` would add a second `x` occurrence (text-colliding with the
8576 // assignment LHS) and the inner `special_variable_name` `?` would
8577 // appear as a standalone `?` operand.
8578 assert_eq!(
8579 bare_x, 1,
8580 "bare $x must be one operand; operands were {:?}",
8581 ops.operands
8582 );
8583 assert_eq!(
8584 special, 1,
8585 "bare $? must be one operand; operands were {:?}",
8586 ops.operands
8587 );
8588 assert!(
8589 !ops.operands.iter().any(|o| o.as_str() == "?"),
8590 "the inner special_variable_name `?` leaf must be suppressed; operands were {:?}",
8591 ops.operands
8592 );
8593 // The assignment LHS `variable_name` `x` (parent `variable_assignment`,
8594 // not `simple_expansion`) must still be an operand.
8595 assert!(
8596 ops.operands.iter().any(|o| o.as_str() == "x"),
8597 "assignment LHS `x` must still be an operand; operands were {:?}",
8598 ops.operands
8599 );
8600 }
8601
8602 /// Drives one row of a Bash wrapper/leaf double-count table: a grammar
8603 /// wrapper whose entire contribution to `N2` was its single,
8604 /// already-counted child (`command_name`, #1351; `translated_string`,
8605 /// #1358).
8606 ///
8607 /// `expected` is the post-fix `[n1, N1, n2, N2]`. `(n2_before,
8608 /// N2_before)` is what the same source scored while `wrapper` was still
8609 /// classified as an operand. Both columns are checked against the
8610 /// *current* parse under the model "one spurious operand per wrapper
8611 /// node" — they are not measured against the old code, which no longer
8612 /// exists to run:
8613 ///
8614 /// - `N2_before - N2` must equal the number of `wrapper` nodes. That
8615 /// identity *is* the defect: one spurious operand per wrapper.
8616 /// - `n2_before` must equal the post-fix operand vocabulary unioned
8617 /// with the wrapper spellings. It exceeds `n2` wherever the wrapper's
8618 /// whole text is not already an operand in its own right — either
8619 /// because it differs from its single child's (`"$cmd"`, `${cmd}`,
8620 /// `$"ls"`) or because it spans several (`foo$x`, `$(which ls)`,
8621 /// `{1..3}`).
8622 ///
8623 /// So a mistyped or stale column fails rather than misinforming the next
8624 /// reader — one did, during review of #1351 — but neither check can
8625 /// catch a *production* regression, because both hold whatever
8626 /// `get_op_type` answers. `assert_halstead_counts` below is the only
8627 /// assertion here that fails when the wrapper arm comes back; measured
8628 /// by neutralising it under the pre-fix arm, which leaves both tables
8629 /// green.
8630 #[cfg(feature = "bash")]
8631 #[track_caller]
8632 fn assert_bash_wrapper_sheds_one_operand(
8633 source: &str,
8634 expected: [u64; 4],
8635 (n2_before, total_before): (u64, u64),
8636 wrapper: Bash,
8637 ) {
8638 let name: &'static str = wrapper.clone().into();
8639 let kind = wrapper as u16;
8640 let code = source.as_bytes();
8641 let parser = BashParser::new(code.to_vec(), &PathBuf::from("foo.sh"), None);
8642 let spellings: Vec<&str> = parser
8643 .root()
8644 .preorder()
8645 .filter(|node| node.kind_id() == kind)
8646 .filter_map(|node| node.utf8_text(code))
8647 .collect();
8648 assert!(
8649 !spellings.is_empty(),
8650 "row {source:?} parses without a {name}, so it witnesses nothing",
8651 );
8652 // Phrased as an addition rather than `total_before - expected[3]`: a
8653 // future edit that inverts the two would underflow `u64` and panic
8654 // with a raw overflow message instead of the one below.
8655 assert_eq!(
8656 total_before,
8657 expected[3] + spellings.len() as u64,
8658 "row {source:?} must shed exactly one operand per {name}; \
8659 recorded N2_before {total_before}",
8660 );
8661
8662 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
8663 let mut vocabulary: HashSet<&str> = ops.operands.iter().map(String::as_str).collect();
8664 vocabulary.extend(spellings);
8665 assert_eq!(
8666 vocabulary.len() as u64,
8667 n2_before,
8668 "row {source:?}: n2 before the fix is the post-fix vocabulary \
8669 plus the {name} spellings; got {vocabulary:?}",
8670 );
8671
8672 assert_halstead_counts::<BashParser>(source, "foo.sh", expected, source);
8673 }
8674
8675 /// Regression for #1351, the command-name sibling of #695's bare
8676 /// `$x`. `command_name` is a pure wrapper: the grammar gives it
8677 /// exactly one required child (a `_primary_expression` or a
8678 /// `concatenation`) and it adds no text of its own, so classifying it
8679 /// as an operand *and* letting the walk reach the child counted every
8680 /// command name twice in `N2`.
8681 ///
8682 /// The table is the grammar-dispatch §6 evidence that deleting the arm
8683 /// zeroes nothing: it names every kind `command_name` can wrap, and in
8684 /// every row the command name still contributes at least one operand
8685 /// once the trailing `arg` is discounted.
8686 ///
8687 /// `n2_before` / `N2_before` are what each row scores with the
8688 /// `command_name` arm restored *against today's tree*, not a historical
8689 /// measurement — the `$"ls" arg` row has since lost a
8690 /// `translated_string` wrapper to #1358 as well.
8691 /// `assert_bash_wrapper_sheds_one_operand` re-derives both from the
8692 /// current parse rather than trusting them.
8693 #[cfg(feature = "bash")]
8694 #[test]
8695 fn bash_command_name_wrapper_no_double_count() {
8696 // (source, [n1, N1, n2, N2], (n2_before, N2_before))
8697 let cases: [(&str, [u64; 4], (u64, u64)); 14] = [
8698 // word
8699 ("ls bar\n", [0, 0, 2, 2], (2, 3)),
8700 // number
8701 ("1 arg\n", [0, 0, 2, 2], (2, 3)),
8702 // string, inert
8703 ("\"ls\" arg\n", [0, 0, 2, 2], (2, 3)),
8704 // string wrapping an expansion: the wrapper string is already
8705 // skipped (#180), so before #1351 the `command_name` was the
8706 // only thing counting the quoted spelling — which also planted
8707 // a spurious `"$cmd"` entry in n2 beside `$cmd`.
8708 ("\"$cmd\" arg\n", [0, 0, 2, 2], (3, 3)),
8709 // raw_string
8710 ("'ls' arg\n", [0, 0, 2, 2], (2, 3)),
8711 // ansi_c_string
8712 ("$'ls' arg\n", [0, 0, 2, 2], (2, 3)),
8713 // translated_string. Two wrappers stacked here — `command_name`
8714 // over `translated_string` — so the pre-#1351 column below is
8715 // measured against a tree #1358 has already thinned to
8716 // `"ls"` + `arg`.
8717 ("$\"ls\" arg\n", [0, 0, 2, 2], (3, 3)),
8718 // simple_expansion
8719 ("$cmd arg\n", [0, 0, 2, 2], (2, 3)),
8720 // brace expansion: counts through its inner `variable_name`,
8721 // whose `SimpleExpansion` parent guard does not apply here.
8722 ("${cmd} arg\n", [0, 0, 2, 2], (3, 3)),
8723 // command_substitution: counts through the nested command.
8724 // Two command names here — the outer one and `which`.
8725 ("$(which ls) arg\n", [0, 0, 3, 3], (4, 5)),
8726 // process_substitution, likewise two command names.
8727 ("<(ls) arg\n", [0, 0, 2, 2], (3, 4)),
8728 // arithmetic_expansion
8729 ("$((1+1)) arg\n", [1, 1, 2, 3], (3, 4)),
8730 // brace_expression
8731 ("{1..3} arg\n", [1, 1, 3, 3], (4, 4)),
8732 // concatenation
8733 ("foo$x arg\n", [0, 0, 3, 3], (4, 4)),
8734 ];
8735 for (source, expected, before) in cases {
8736 assert!(
8737 expected[3] > 1,
8738 "row {source:?} must leave the command name at least one \
8739 operand beside `arg`; a zero here means the deleted arm \
8740 was load-bearing for this spelling",
8741 );
8742 assert_bash_wrapper_sheds_one_operand(source, expected, before, Bash::CommandName);
8743 }
8744 }
8745
8746 /// Regression for #1358, the sibling of #1351 in the same `match`.
8747 /// `translated_string: $ => seq('$', $.string)` carries exactly one
8748 /// required `string` child and no text of its own beyond the `$`, so
8749 /// classifying the wrapper *and* letting the walk reach the child
8750 /// counted every `$"…"` twice in `N2`.
8751 ///
8752 /// The rows assert the total, `N2`. `n2` alone cannot witness this
8753 /// double count: the wrapper and its child spell *different* text
8754 /// (`$"x"` against `"x"`), so the pre-fix tree split one operand into
8755 /// two vocabulary entries rather than repeating one — which is why the
8756 /// `n2_before` column is carried too, and checked against the wrapper
8757 /// spellings the row actually parses to.
8758 #[cfg(feature = "bash")]
8759 #[test]
8760 fn bash_translated_string_wrapper_no_double_count() {
8761 // (source, [n1, N1, n2, N2], (n2_before, N2_before))
8762 let cases: [(&str, [u64; 4], (u64, u64)); 7] = [
8763 // Assignment RHS.
8764 ("a=$\"x\"\n", [1, 1, 2, 2], (3, 3)),
8765 // Empty translated string. The grammar makes the inner `string`
8766 // required, so the wrapper is never childless — the condition
8767 // grammar-dispatch §6 warns that deletion would regress.
8768 ("a=$\"\"\n", [1, 1, 2, 2], (3, 3)),
8769 // The one spelling the deleted arm *was* load-bearing for: the
8770 // inner `string` wraps an operandless brace `expansion`, so the
8771 // whole `$"…"` now scores zero. That is #1351's residue one
8772 // level down rather than a new gap — a plain `a="${#}"` already
8773 // scored `[1, 1, 1, 1]` — and
8774 // `bash_translated_string_scores_alike_in_both_positions` pins
8775 // that both positions agree on it.
8776 ("a=$\"${#}\"\n", [1, 1, 1, 1], (2, 2)),
8777 // `${!}` is the same residue plus a `!`, which the operator arm
8778 // counts; it must not turn into an operand along the way.
8779 ("a=$\"${!}\"\n", [2, 2, 1, 1], (2, 2)),
8780 // `case` subject.
8781 ("case $\"y\" in *) ;; esac\n", [4, 4, 1, 1], (2, 2)),
8782 // Command-name position, where #1351's wrapper stacks on top of
8783 // this one; the `_before` columns here price the
8784 // `translated_string` alone.
8785 ("$\"ls\" arg\n", [0, 0, 2, 2], (3, 3)),
8786 // Interpolating form. `bash_string_has_expansion` inspects the
8787 // node's own children, and a `translated_string` has only `$`
8788 // and `string`, so it never saw the `simple_expansion` one level
8789 // down: the wrapper reintroduced exactly the double count #180
8790 // removed, for the `$"…"` spelling alone. Only `$y` and `b` may
8791 // survive.
8792 ("b=$\"$y\"\n", [1, 1, 2, 2], (3, 3)),
8793 ];
8794 for (source, expected, before) in cases {
8795 assert_bash_wrapper_sheds_one_operand(source, expected, before, Bash::TranslatedString);
8796 }
8797 }
8798
8799 /// The load-bearing half of #1358's grammar-dispatch §6 argument, which
8800 /// nothing else asserts. In ordinary argument position the grammar emits
8801 /// no `translated_string` at all — just a `$` token and a `string` — so
8802 /// the `string` is the node present for *every* spelling, and dropping
8803 /// the wrapper makes the wrapper-bearing positions agree with argument
8804 /// position rather than newly disagree. If a grammar bump starts
8805 /// emitting the wrapper here, the deletion has to be re-derived.
8806 #[cfg(feature = "bash")]
8807 #[test]
8808 fn bash_translated_string_scores_alike_in_both_positions() {
8809 // `${#}` carries the residue: it contributes no operand of its own,
8810 // so the second row pins that the deletion left *both* positions at
8811 // zero rather than only the wrapper-bearing one.
8812 //
8813 // Only the command-name half moves under the pre-fix arm; the
8814 // argument-position half is the reference the parity is measured
8815 // against, and `cmd $"…"` is asserted nowhere else. Measured — with
8816 // the argument assertions dropped this test still fails pre-fix,
8817 // with the command-name ones dropped it passes — so do not
8818 // "simplify" it down to the failing half.
8819 for (spelling, expected) in [("$\"hi\"", [0, 0, 2, 2]), ("$\"${#}\"", [0, 0, 1, 1])] {
8820 let as_command_name = format!("{spelling} arg\n");
8821 let as_argument = format!("cmd {spelling}\n");
8822 let parse = |source: &str| {
8823 BashParser::new(source.as_bytes().to_vec(), &PathBuf::from("foo.sh"), None)
8824 };
8825 assert!(
8826 !ast_has_kind_id(&parse(&as_argument), Bash::TranslatedString as u16),
8827 "argument position must still parse {spelling} as a bare `$` \
8828 plus a `string`; a grammar bump that emits the wrapper here \
8829 invalidates the parity this test rests on",
8830 );
8831 assert!(
8832 ast_has_kind_id(&parse(&as_command_name), Bash::TranslatedString as u16),
8833 "command-name position must still emit the wrapper, or the \
8834 pair witnesses nothing",
8835 );
8836 assert_halstead_counts::<BashParser>(
8837 &as_command_name,
8838 "foo.sh",
8839 expected,
8840 &as_command_name,
8841 );
8842 assert_halstead_counts::<BashParser>(&as_argument, "foo.sh", expected, &as_argument);
8843 }
8844 }
8845
8846 /// Pins the one residue of #1351: a brace `expansion` with no
8847 /// `variable_name` leaf. `${#}` (positional-parameter count) and `${!}`
8848 /// (last background PID) hold only anonymous tokens, and
8849 /// `Bash::Expansion` is not an operand, so the whole expansion
8850 /// contributes nothing.
8851 ///
8852 /// That was already true in *argument* position before the fix, which
8853 /// is the reason grammar-dispatch §6's gate-don't-delete rule did not
8854 /// apply to the `command_name` arm: deleting it made command-name
8855 /// position agree with argument position rather than newly disagree.
8856 /// The parity is the load-bearing half of that argument and nothing
8857 /// else asserts it, so if a future arm starts classifying these the
8858 /// two positions have to move together.
8859 #[cfg(feature = "bash")]
8860 #[test]
8861 fn bash_operandless_expansion_scores_alike_in_both_positions() {
8862 // `${!}` carries a `!`, which the operator arm counts; `${#}`'s `#`
8863 // is not an operator. Both positions must agree per spelling.
8864 for (spelling, expected) in [("${#}", [0, 0, 1, 1]), ("${!}", [1, 1, 1, 1])] {
8865 let as_command_name = format!("{spelling} arg\n");
8866 let as_argument = format!("cmd {spelling}\n");
8867 assert_halstead_counts::<BashParser>(
8868 &as_command_name,
8869 "foo.sh",
8870 expected,
8871 &as_command_name,
8872 );
8873 assert_halstead_counts::<BashParser>(&as_argument, "foo.sh", expected, &as_argument);
8874 }
8875 }
8876
8877 /// Drift marker for #1351 (lesson 34 / grammar-dispatch §2). `_concat`
8878 /// (`Bash::Concat`) is a hidden zero-width external token the scanner
8879 /// emits between `concatenation` parts; the parser never surfaces it as
8880 /// a node, and it spells no operand, so `BashCode::get_op_type` lists
8881 /// neither it nor the visible `concatenation` wrapper. If a grammar
8882 /// bump starts emitting it, this fails and the classification must be
8883 /// re-derived rather than assumed still absent.
8884 ///
8885 /// Measured, not assumed: putting `Bash::Concat` back in the operand
8886 /// arm fails no test in the suite, because the token is unreachable.
8887 /// Unreachability is the only coverage such an arm can have, which is
8888 /// why this test asserts it directly instead of asserting a count.
8889 #[cfg(feature = "bash")]
8890 #[test]
8891 fn bash_hidden_concat_token_is_unreachable() {
8892 let source = "a=foo$x\nb=pre\"$y\"post\ncmd bar$z\n";
8893 let path = PathBuf::from("foo.sh");
8894 let parser = BashParser::new(source.as_bytes().to_vec(), &path, None);
8895 // Non-vacuity: the visible `concatenation` this source is written
8896 // to produce must actually be in the parse, so the negative below
8897 // is about `_concat` and not about a source that concatenates
8898 // nothing.
8899 assert!(
8900 ast_has_kind_id(&parser, Bash::Concatenation as u16),
8901 "expected a visible `concatenation` node in the parse",
8902 );
8903 assert!(
8904 !ast_has_kind_id(&parser, Bash::Concat as u16),
8905 "the hidden `_concat` token surfaced; re-derive its \
8906 classification in BashCode::get_op_type against the new grammar",
8907 );
8908 }
8909
8910 #[cfg(feature = "tcl")]
8911 #[test]
8912 fn tcl_operators_and_operands() {
8913 check_metrics::<TclParser>(
8914 "proc f {a b} {
8915 set x [expr {$a + $b}]
8916 if {$x > 0 && $x != 0} {
8917 return $x
8918 }
8919 return 0
8920}",
8921 "foo.tcl",
8922 |metric| {
8923 // Anchored per the snapshot policy in AGENTS.md, which
8924 // this call predates. Operators `proc`, `set`, `[]`,
8925 // `{}`, `expr`, `+`, `if`, `>`, `&&`, `!=` → n1 = 10,
8926 // N1 = 14 (`{}` × 4 for the proc parameter list, the
8927 // proc body, the two `expr`/`if` conditions and the
8928 // `if` body — the `expr` braces are an `Expr`, the
8929 // bodies a `BracedWord`). Operands `f`, `a`, `b`, `x`,
8930 // `$a`, `$b`, `$x`, `0` and `return` → n2 = 9,
8931 // N2 = 14. Before #1354 the proc body and the `if`
8932 // body were operands too → 11 / 16.
8933 assert_eq!(metric.halstead.unique_operators(), 10);
8934 assert_eq!(metric.halstead.total_operators(), 14);
8935 assert_eq!(metric.halstead.unique_operands(), 9);
8936 assert_eq!(metric.halstead.total_operands(), 14);
8937 insta::assert_json_snapshot!(metric.halstead);
8938 },
8939 );
8940 }
8941
8942 #[cfg(feature = "tcl")]
8943 #[test]
8944 fn tcl_bitwise_ternary_string_ops() {
8945 // Exercises operator families not covered by tcl_operators_and_operands:
8946 // bitwise (&, |, ^, ~, <<, >>), ternary (?), and string-comparison (eq, ne, in, ni).
8947 check_metrics::<TclParser>(
8948 "proc f {a b} {
8949 set bits [expr {$a & $b | $a ^ ~$b}]
8950 set sh [expr {$a << 1 | $b >> 1}]
8951 set t [expr {$a > 0 ? $a : $b}]
8952 if {$a eq {x} || $a ne {y}} {
8953 return $a
8954 }
8955 return $b
8956}",
8957 "foo.tcl",
8958 |metric| {
8959 // Anchored per the snapshot policy in AGENTS.md, which
8960 // this call predates. N1 fell 33 → 31 with #1314: the
8961 // `if` condition's `{x}` and `{y}` are braced *words*,
8962 // so their openers stopped fabricating a `{}` operator.
8963 // n1 is unchanged at 18 because the `{}` entry survives
8964 // on the proc body and the `expr` braces — which is
8965 // exactly why the fabrication was invisible in n1 and
8966 // is the reason to assert N1 as well (#1294).
8967 //
8968 // The operand columns fell 17 / 30 → 13 / 26 with
8969 // #1354, and the operator columns did not move: the two
8970 // script bodies (the proc's and the `if`'s) stopped
8971 // being operands, and the `x` / `y` inside the braced
8972 // words `{x}` and `{y}` are now part of the one operand
8973 // each word contributes.
8974 assert_eq!(metric.halstead.unique_operators(), 18);
8975 assert_eq!(metric.halstead.total_operators(), 31);
8976 assert_eq!(metric.halstead.unique_operands(), 13);
8977 assert_eq!(metric.halstead.total_operands(), 26);
8978 insta::assert_json_snapshot!(metric.halstead);
8979 },
8980 );
8981 }
8982
8983 #[cfg(feature = "tcl")]
8984 #[test]
8985 fn tcl_array_reference_bills_the_reference_and_the_index() {
8986 // `$arr($i)` is the reference plus the index Tcl substitutes
8987 // inside the parens, and `arr(k)` as a `set` target is the name
8988 // plus the literal index; the `array_index` wrapper is neither.
8989 // The quoted spelling is deliberate — the vendored grammar
8990 // mis-parses a bare `$arr(k)` in command-word position. Pinned
8991 // per dialect and as a parity in `tests/parity/`, because the
8992 // iRules twin listed the wrapper as a third operand.
8993 assert_ops_operands::<TclParser>(
8994 "set arr(k) 1\nset z \"$arr($i)\"\n",
8995 "foo.tcl",
8996 6,
8997 vec!["arr", "k", "1", "z", "$arr($i)", "$i"],
8998 );
8999 }
9000
9001 #[cfg(feature = "tcl")]
9002 #[test]
9003 fn tcl_bare_variable_operand() {
9004 // Bare `$varname` produces a VariableSubstitution node (already an operand).
9005 // Its anonymous Id2 child must NOT be counted separately; each reference is 1 operand.
9006 check_metrics::<TclParser>(
9007 "proc f {x} {
9008 return $x
9009}",
9010 "foo.tcl",
9011 |metric| {
9012 // Anchored per the snapshot policy in AGENTS.md, which
9013 // this call predates. Operators `proc` and `{}` × 2
9014 // (the parameter list and the body) → n1 = 2, N1 = 3.
9015 // Operands `f`, the parameter `x`, `return` and `$x` —
9016 // one occurrence each, so a re-counted `x` leaf inside
9017 // `$x` would show up in N2 even though it collides with
9018 // the parameter in n2. Before #1354 the proc body was a
9019 // fifth operand.
9020 assert_eq!(metric.halstead.unique_operators(), 2);
9021 assert_eq!(metric.halstead.total_operators(), 3);
9022 assert_eq!(metric.halstead.unique_operands(), 4);
9023 assert_eq!(metric.halstead.total_operands(), 4);
9024 insta::assert_json_snapshot!(metric.halstead);
9025 },
9026 );
9027 }
9028
9029 #[cfg(feature = "tcl")]
9030 #[test]
9031 fn tcl_inert_quoted_word_counts_as_operand() {
9032 // Regression for #277. A `"..."` literal with no `$var` / `[cmd]`
9033 // interpolation must contribute exactly one operand (the wrapping
9034 // `QuotedWord`). The string content `hello world` is exposed as a
9035 // single `_quoted_word_content` token (not itself classified by
9036 // `get_op_type`), so the only operands here are `f`, `s`, and the
9037 // quoted string. `set` is the anonymous `Set2` keyword and is
9038 // classified as an operator, not an operand.
9039 check_metrics::<TclParser>(
9040 "proc f {} {
9041 set s \"hello world\"
9042}",
9043 "foo.tcl",
9044 |metric| {
9045 // Operands: `f`, the `set` target `s`, `"hello world"` —
9046 // 3 unique, 3 total. Before #1294 this read 3/3 for a
9047 // different reason, with `s` missing and the proc-body
9048 // `braced_word` making the count coincidentally
9049 // plausible; #1354 removed that body operand, so the
9050 // three named here are now the whole list. The wrapping
9051 // `QuotedWord` must still contribute exactly one operand
9052 // when it carries no interpolation children; dropping to 2
9053 // would mean the inert case was over-guarded.
9054 assert_eq!(metric.halstead.unique_operands(), 3);
9055 assert_eq!(metric.halstead.total_operands(), 3);
9056 insta::assert_json_snapshot!(metric.halstead);
9057 },
9058 );
9059 }
9060
9061 #[cfg(feature = "tcl")]
9062 #[test]
9063 fn tcl_interpolated_quoted_word_no_double_count() {
9064 // Regression for #277. Before the fix, `"$x is $y"` produced an
9065 // extra operand for the wrapping `QuotedWord` on top of the two
9066 // inner `VariableSubstitution` operands (`$x`, `$y`), giving 7.
9067 // After the fix, the wrapper is `TokenRole::Unknown` whenever
9068 // it carries an interpolation child, so operand attribution
9069 // belongs solely to the inner substitutions.
9070 check_metrics::<TclParser>(
9071 "proc f {x y} {
9072 set s \"$x is $y\"
9073}",
9074 "foo.tcl",
9075 |metric| {
9076 // Operands: `f`, `x`, `y` (proc args), the `set` target
9077 // `s`, `$x`, `$y` — 6 unique, 6 total. The wrapping
9078 // `QuotedWord` contributes nothing, and since #1354
9079 // neither does the proc-body `braced_word`. Before #277
9080 // the wrapper double-counted.
9081 assert_eq!(metric.halstead.unique_operands(), 6);
9082 assert_eq!(metric.halstead.total_operands(), 6);
9083 insta::assert_json_snapshot!(metric.halstead);
9084 },
9085 );
9086 }
9087
9088 #[cfg(feature = "tcl")]
9089 #[test]
9090 fn tcl_command_substitution_quoted_word_no_double_count() {
9091 // Regression for #277. A `"...[cmd]..."` literal exposes the
9092 // bracketed command as a `command_substitution` child whose inner
9093 // identifiers/literals contribute their own operands. The wrapping
9094 // `QuotedWord` must not also be classified as an operand, or the
9095 // command's identifier would be counted alongside a phantom
9096 // wrapper operand.
9097 check_metrics::<TclParser>(
9098 "proc f {} {
9099 set s \"result: [foo]\"
9100}",
9101 "foo.tcl",
9102 |metric| {
9103 // Operands: `f`, the `set` target `s`, `foo` — 3 unique,
9104 // 3 total. The wrapping `QuotedWord` and the inert text
9105 // `result: ` do not contribute extra operands, and since
9106 // #1354 neither does the proc-body `braced_word`. Before
9107 // #277 the wrapper double-counted.
9108 assert_eq!(metric.halstead.unique_operands(), 3);
9109 assert_eq!(metric.halstead.total_operands(), 3);
9110 insta::assert_json_snapshot!(metric.halstead);
9111 },
9112 );
9113 }
9114
9115 /// Regression for #1294. The `set` target parses as the anonymous
9116 /// `id` token (`Tcl::Id2`) — the same kind as the leaf inside a
9117 /// `variable_substitution` — and the getter used to exclude that kind
9118 /// wholesale, so every variable a Tcl script assigned was absent from
9119 /// n2/N2. The guard is now parent-scoped: a target `id` counts, a
9120 /// var-sub leaf does not. Exact occurrence counts distinguish this
9121 /// fix from a regression in either direction: a re-blanketed
9122 /// exclusion drops `s`/`t` (total 2), while losing the guard
9123 /// double-counts the `$s` leaf as a second `s` (total 5).
9124 #[cfg(feature = "tcl")]
9125 #[test]
9126 fn tcl_set_target_is_operand() {
9127 let source = "set s 1\nset t $s\n";
9128 let path = PathBuf::from("foo.tcl");
9129 let parser = TclParser::new(source.as_bytes().to_vec(), &path, None);
9130 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
9131 // expected operands: targets `s` and `t`, literal `1`, reference
9132 // `$s` (wrapper only) — 4 total, each exactly once.
9133 for operand in ["s", "t", "1", "$s"] {
9134 assert_eq!(
9135 ops.operands
9136 .iter()
9137 .filter(|o| o.as_str() == operand)
9138 .count(),
9139 1,
9140 "`{operand}` must be exactly one operand; operands were {:?}",
9141 ops.operands
9142 );
9143 }
9144 assert_eq!(
9145 ops.operands.len(),
9146 4,
9147 "operands must be exactly s, t, 1, $s; got {:?}",
9148 ops.operands
9149 );
9150
9151 check_metrics::<TclParser>(source, "foo.tcl", |metric| {
9152 // expected: n2 = 4 (s, t, 1, $s), N2 = 4; operators are the
9153 // two `set` keywords — n1 = 1, N1 = 2.
9154 assert_eq!(metric.halstead.unique_operands(), 4);
9155 assert_eq!(metric.halstead.total_operands(), 4);
9156 assert_eq!(metric.halstead.unique_operators(), 1);
9157 assert_eq!(metric.halstead.total_operators(), 2);
9158 });
9159 }
9160
9161 /// Drift marker for #1294 (lesson 34 / grammar-dispatch §2): the
9162 /// *named* `id` rule (`Tcl::Id`, kind_id 84) never surfaces at the
9163 /// pinned tree-sitter-tcl — the parser emits the anonymous `Id2` in
9164 /// both positions the getter guards (the `set` target and the
9165 /// var-sub leaf). The `Tcl::Id` arm in `get_op_type` is therefore
9166 /// defensive; if a grammar bump starts emitting 84 this fails and
9167 /// the arm's classification must be re-derived instead of trusted.
9168 #[cfg(feature = "tcl")]
9169 #[test]
9170 fn tcl_named_id_variant_is_unreachable() {
9171 let source = "proc f {x} {\n set s $x\n foreach v {1 2} { puts \"$v\" }\n}\n";
9172 let path = PathBuf::from("foo.tcl");
9173 let parser = TclParser::new(source.as_bytes().to_vec(), &path, None);
9174 // Non-vacuity: the anonymous token must be present in this parse
9175 // (both the `set` target and the `$x` / `$v` leaves emit it).
9176 assert!(
9177 ast_has_kind_id(&parser, Tcl::Id2 as u16),
9178 "expected the anonymous Tcl::Id2 token to appear in the parse",
9179 );
9180 assert!(
9181 !ast_has_kind_id(&parser, Tcl::Id as u16),
9182 "the named Tcl::Id rule surfaced; re-derive the defensive \
9183 `Tcl::Id` arm in TclCode::get_op_type against the new grammar",
9184 );
9185 }
9186
9187 #[cfg(feature = "tcl")]
9188 #[test]
9189 fn tcl_braced_word_delimiter_is_not_an_operator() {
9190 // Regression: issue #1314, the Tcl sibling of Elixir #1256 and
9191 // Ruby/Perl #1312. A braced *word* — a literal value, not a
9192 // script — carries its `{` as an `LBRACE` child, the kind id a
9193 // real block uses, so `set a {braced word}` reported a `{}`
9194 // operator with no block in the source.
9195 //
9196 // expected: operator `set` × 3 → n1 = 1, N1 = 3. Operands
9197 // `a`, `b`, `c`, `$a` and `{braced word}` × 2 → n2 = 5,
9198 // N2 = 6. Before the guard the two openers added `{}` →
9199 // n1 = 2, N1 = 5; before #1354 the inner `braced` / `word`
9200 // counted alongside the word containing them → n2 = 7,
9201 // N2 = 10.
9202 check_metrics::<TclParser>(
9203 "set a {braced word}\nset b {braced word}\nset c $a\n",
9204 "foo.tcl",
9205 |metric| {
9206 assert_eq!(metric.halstead.unique_operators(), 1);
9207 assert_eq!(metric.halstead.total_operators(), 3);
9208 assert_eq!(metric.halstead.unique_operands(), 5);
9209 assert_eq!(metric.halstead.total_operands(), 6);
9210 },
9211 );
9212 }
9213
9214 #[cfg(feature = "tcl")]
9215 #[test]
9216 fn tcl_script_bodies_keep_their_braces() {
9217 // Control for #1314, and the reason a kind-scoped guard is safe
9218 // in Tcl where it would not be elsewhere: the grammar gives the
9219 // literal and the block *different* kinds. A `proc` body, an
9220 // `if` body and an `if` condition are `BracedWord` (88) and
9221 // `Expr` (97); only the value form is `BracedWordSimple` (89).
9222 // This fixture nests a braced word inside a real script body,
9223 // so a guard that keyed on the brace alone would drop the
9224 // block's `{}` and fail here.
9225 //
9226 // expected: operators `proc`, `set` × 2, `if`, `>`, and `{}`
9227 // × 4 (the proc parameter list, the proc body, the `if`
9228 // condition, the `if` body) → n1 = 5, N1 = 9. Operands, all
9229 // distinct → n2 = N2 = 8: `p`, `x`, `a`, `$x`, `1`, `b` and
9230 // the two braced *words* `{v w}` and `{y}`, each one operand
9231 // rather than one per inner word.
9232 //
9233 // Before #1354 this read 13 / 13. The five extra entries were
9234 // the inner words `v`, `w`, `y` and the two *script* bodies,
9235 // which were `BracedWord` operands in their own right — so a
9236 // block counted twice over, once as the operand and once as
9237 // the `{}` operator this test is about. Both halves are gone;
9238 // the operator columns are what this test guards and they did
9239 // not move.
9240 check_metrics::<TclParser>(
9241 "proc p {x} {\n set a {v w}\n if {$x > 1} { set b {y} }\n}\n",
9242 "foo.tcl",
9243 |metric| {
9244 assert_eq!(metric.halstead.unique_operators(), 5);
9245 assert_eq!(metric.halstead.total_operators(), 9);
9246 assert_eq!(metric.halstead.unique_operands(), 8);
9247 assert_eq!(metric.halstead.total_operands(), 8);
9248 },
9249 );
9250 }
9251
9252 #[cfg(feature = "tcl")]
9253 #[test]
9254 fn tcl_braced_word_guard_is_parent_scoped_not_ancestor_scoped() {
9255 // The input that separates the parent-scoped guard from the
9256 // ancestor-scanning mutant. I first recorded this distinction
9257 // as *unobservable* in Tcl, reasoning that a braced word holds
9258 // only simple words and nested braced words. `bca dump` says
9259 // otherwise: the grammar parses a `[…]` command substitution
9260 // inside a braced word, and the `if` inside it brings an `Expr`
9261 // condition and a `BracedWord` body, each with its own `{`,
9262 // both of them non-immediate descendants of the
9263 // `BracedWordSimple`. An ancestor scan swallows both.
9264 //
9265 // (Real Tcl does not substitute inside braces — this is the
9266 // grammar modelling structure it will not evaluate. What the
9267 // classifier sees is what the metric reports, so it is the
9268 // right fixture regardless.)
9269 //
9270 // expected: operators `set`, `[]`, `if`, and `{}` × 2 (the
9271 // `if` condition's `Expr` and its `BracedWord` body; the outer
9272 // value word's own `{` is suppressed) → n1 = 4, N1 = 5.
9273 // Operands `z`, the whole braced word, and `$q` / `puts` / `w`
9274 // from inside the command substitution → n2 = N2 = 5. Under
9275 // the ancestor-scoped mutant both surviving braces vanish:
9276 // n1 = 3, N1 = 3.
9277 //
9278 // #1354 widened the guard from the `{` alone to every direct
9279 // child of the braced word, which is why `x` and `v` are no
9280 // longer operands and the nested script body no longer is
9281 // either. It did not change the *scope*: the three operands
9282 // from inside the command substitution are grandchildren, and
9283 // an ancestor-scoped guard would drop them too.
9284 check_metrics::<TclParser>("set z {x [if {$q} {puts w}] v}\n", "foo.tcl", |metric| {
9285 assert_eq!(metric.halstead.unique_operators(), 4);
9286 assert_eq!(metric.halstead.total_operators(), 5);
9287 assert_eq!(metric.halstead.unique_operands(), 5);
9288 assert_eq!(metric.halstead.total_operands(), 5);
9289 });
9290 }
9291
9292 #[cfg(feature = "irules")]
9293 #[test]
9294 fn irules_braced_word_guard_is_parent_scoped_not_ancestor_scoped() {
9295 // The iRules twin of the test above — the two getters are
9296 // clones, so the mutant must fail in both.
9297 //
9298 // expected: operators `when`, `set`, `[]`, `if`, `{}` × 3 (the
9299 // handler body, the `if` condition and the `if` body) → n1 = 5,
9300 // N1 = 7. Operands `HTTP_REQUEST`, `z`, the whole braced word,
9301 // and `$q` / `log` / `w` from inside the command substitution →
9302 // n2 = N2 = 6 (10 before #1354, which also took the direct
9303 // children `x` / `v` and the two script bodies out of the
9304 // operand set without moving the operator columns this test
9305 // guards).
9306 check_metrics::<IrulesParser>(
9307 "when HTTP_REQUEST {\n set z {x [if {$q} {log w}] v}\n}\n",
9308 "foo.irule",
9309 |metric| {
9310 assert_eq!(metric.halstead.unique_operators(), 5);
9311 assert_eq!(metric.halstead.total_operators(), 7);
9312 assert_eq!(metric.halstead.unique_operands(), 6);
9313 assert_eq!(metric.halstead.total_operands(), 6);
9314 },
9315 );
9316 }
9317
9318 /// How one dialect of the Tcl family spells the braced-word
9319 /// construct. The two grammars are deliberate clones with different
9320 /// id blocks, so #1354's guard is derived once and instantiated
9321 /// twice — and a fix that landed in only one dialect fails the
9322 /// second instantiation rather than going unnoticed.
9323 struct BracedWordKinds {
9324 /// `braced_word_simple`, the literal *value* form the guard
9325 /// keys on.
9326 wrapper: u16,
9327 /// `braced_word`, the *script* form #1354 gated on holding a
9328 /// command: an operand only when it holds none.
9329 script_body: u16,
9330 /// `comment`, the one named child of a script that is not a
9331 /// command and that the gate must not mistake for one.
9332 comment: u16,
9333 /// Every named kind node-types.json admits directly inside
9334 /// `wrapper`. A child outside this set means the grammar moved
9335 /// and the parent-keyed arm has to be re-derived
9336 /// (grammar-dispatch §1).
9337 children: [u16; 6],
9338 /// The `{` / `}` the wrapper also holds. Suppressing the opener
9339 /// was the whole of #1314's narrower guard; the closer has
9340 /// never been classified, since `get_operator_id_as_str` folds
9341 /// the pair to one `{}` glyph.
9342 delimiters: [u16; 2],
9343 /// Of `children`, the three the operand arm classified
9344 /// unconditionally before the guard.
9345 operand_children: [u16; 3],
9346 /// `quoted_word`, which was an operand only when *inert* —
9347 /// `string_operand_type`'s own rule, replicated here so the
9348 /// shed count below is what `N2` actually shed.
9349 quoted_word: u16,
9350 /// The interpolation kinds that decide that.
9351 interpolation: [u16; 2],
9352 }
9353
9354 #[cfg(feature = "tcl")]
9355 const TCL_BRACED_WORD_KINDS: BracedWordKinds = BracedWordKinds {
9356 wrapper: Tcl::BracedWordSimple as u16,
9357 script_body: Tcl::BracedWord as u16,
9358 comment: Tcl::Comment as u16,
9359 children: [
9360 Tcl::SimpleWord as u16,
9361 Tcl::EscapedCharacter as u16,
9362 Tcl::QuotedWord as u16,
9363 Tcl::VariableSubstitution as u16,
9364 Tcl::CommandSubstitution as u16,
9365 Tcl::BracedWordSimple as u16,
9366 ],
9367 delimiters: [Tcl::LBRACE as u16, Tcl::RBRACE as u16],
9368 operand_children: [
9369 Tcl::SimpleWord as u16,
9370 Tcl::VariableSubstitution as u16,
9371 Tcl::BracedWordSimple as u16,
9372 ],
9373 quoted_word: Tcl::QuotedWord as u16,
9374 interpolation: [
9375 Tcl::VariableSubstitution as u16,
9376 Tcl::CommandSubstitution as u16,
9377 ],
9378 };
9379
9380 #[cfg(feature = "irules")]
9381 const IRULES_BRACED_WORD_KINDS: BracedWordKinds = BracedWordKinds {
9382 wrapper: Irules::BracedWordSimple as u16,
9383 script_body: Irules::BracedWord as u16,
9384 comment: Irules::Comment as u16,
9385 children: [
9386 Irules::SimpleWord as u16,
9387 Irules::EscapedCharacter as u16,
9388 Irules::QuotedWord as u16,
9389 Irules::VariableSubstitution as u16,
9390 Irules::CommandSubstitution as u16,
9391 Irules::BracedWordSimple as u16,
9392 ],
9393 delimiters: [Irules::LBRACE as u16, Irules::RBRACE as u16],
9394 operand_children: [
9395 Irules::SimpleWord as u16,
9396 Irules::VariableSubstitution as u16,
9397 Irules::BracedWordSimple as u16,
9398 ],
9399 quoted_word: Irules::QuotedWord as u16,
9400 interpolation: [
9401 Irules::VariableSubstitution as u16,
9402 Irules::CommandSubstitution as u16,
9403 ],
9404 };
9405
9406 /// The operand occurrences #1354 removed from `source`, as their
9407 /// source texts, plus the set of `braced_word_simple` child kinds
9408 /// the fixture witnessed.
9409 ///
9410 /// Walks with `for_each_node_with_chain`, which maintains the
9411 /// ancestor chain exactly as `spaces::compute` does, so "parent"
9412 /// here means what `Ancestors::parent` means inside the guard.
9413 /// Doubles as the grammar-dispatch §1 / §2 drift marker: a child of
9414 /// the wrapper outside `children` ∪ `delimiters` fails on the spot,
9415 /// which is what makes keying the arm on the parent alone — rather
9416 /// than on an enumerated child list — safe to rely on.
9417 #[cfg(any(feature = "irules", feature = "tcl"))]
9418 fn braced_word_shed<L: crate::LanguageInfo>(
9419 source: &str,
9420 kinds: &BracedWordKinds,
9421 ) -> (Vec<String>, HashSet<u16>) {
9422 let code = source.as_bytes();
9423 let mut shed = Vec::new();
9424 let mut witnessed = HashSet::new();
9425 for_each_node_with_chain::<L>(code, |node, chain| {
9426 let text = || {
9427 node.utf8_text(code)
9428 .expect("fixture is valid UTF-8")
9429 .to_owned()
9430 };
9431 // The script form was an operand of its own wherever it
9432 // appeared until #1354, which now gates it on holding a
9433 // command — a named child that is not a comment. This is not
9434 // a child of the wrapper, so it is counted before the parent
9435 // test below.
9436 if node.kind_id() == kinds.script_body
9437 && node
9438 .children()
9439 .any(|child| child.is_named() && child.kind_id() != kinds.comment)
9440 {
9441 shed.push(text());
9442 }
9443 if chain.last().is_none_or(|p| p.kind_id() != kinds.wrapper) {
9444 return;
9445 }
9446 assert!(
9447 kinds.children.contains(&node.kind_id())
9448 || kinds.delimiters.contains(&node.kind_id()),
9449 "`{source}`: a `{}` inside a braced word is a child this guard \
9450 was not derived against; re-read node-types.json before \
9451 trusting it",
9452 node.kind(),
9453 );
9454 if kinds.children.contains(&node.kind_id()) {
9455 witnessed.insert(node.kind_id());
9456 }
9457 let was_operand = kinds.operand_children.contains(&node.kind_id())
9458 || (node.kind_id() == kinds.quoted_word && !node.wraps_any(&kinds.interpolation));
9459 if was_operand {
9460 shed.push(text());
9461 }
9462 });
9463 (shed, witnessed)
9464 }
9465
9466 /// One row of the #1354 tables: a fixture, what it measures now,
9467 /// what it measured before, and the operand texts behind the counts.
9468 struct BracedWordCase {
9469 source: &'static str,
9470 /// `[n1, N1, n2, N2]` with the guard in place.
9471 counts: [u64; 4],
9472 /// `[n2, N2]` without it. Re-derived by the loop rather than
9473 /// trusted, so a stale row fails instead of misinforming.
9474 before: [u64; 2],
9475 operands: &'static [&'static str],
9476 }
9477
9478 /// Every row is measured in *both* dialects, so a fix applied to
9479 /// one getter and not its clone fails here. `braced_word_shed`'s
9480 /// drift assertion likewise runs against both grammars.
9481 #[cfg(any(feature = "irules", feature = "tcl"))]
9482 fn check_braced_word_cases<T: crate::MetricSuite, L: crate::LanguageInfo>(
9483 cases: &[BracedWordCase],
9484 file: &str,
9485 kinds: &BracedWordKinds,
9486 ) -> HashSet<u16> {
9487 let mut witnessed = HashSet::new();
9488 for case in cases {
9489 let (shed, seen) = braced_word_shed::<L>(case.source, kinds);
9490 witnessed.extend(seen);
9491
9492 // Phrased as an addition rather than a subtraction so a
9493 // future edit that inverts the two underflows nothing.
9494 assert_eq!(
9495 case.before[1],
9496 case.counts[3] + shed.len() as u64,
9497 "{file} `{}`: N2 must shed exactly one occurrence per \
9498 previously-billed part; recorded {}, parts {shed:?}",
9499 case.source,
9500 case.before[1],
9501 );
9502 let mut vocabulary: HashSet<&str> = case.operands.iter().copied().collect();
9503 vocabulary.extend(shed.iter().map(String::as_str));
9504 assert_eq!(
9505 vocabulary.len() as u64,
9506 case.before[0],
9507 "{file} `{}`: n2 before the fix is the post-fix vocabulary \
9508 plus those parts; got {vocabulary:?}",
9509 case.source,
9510 );
9511
9512 assert_halstead_counts::<T>(case.source, file, case.counts, case.source);
9513 assert_ops_operands::<T>(
9514 case.source,
9515 file,
9516 case.operands.len(),
9517 case.operands.to_vec(),
9518 );
9519 }
9520 witnessed
9521 }
9522
9523 /// The rows shared by both dialects: one per named child kind
9524 /// `braced_word_simple` admits, the childless spelling, the
9525 /// repeated-value row that separates `n2` from `N2` (#1294), and
9526 /// the braced/quoted parity pair #1317 asks for.
9527 #[cfg(any(feature = "irules", feature = "tcl"))]
9528 const BRACED_WORD_CASES: [BracedWordCase; 11] = [
9529 // simple_word, the reported fixture. Two words inside one
9530 // value scored two operands beside the value itself.
9531 BracedWordCase {
9532 source: "set x {literal here}\n",
9533 counts: [1, 1, 2, 2],
9534 before: [4, 4],
9535 operands: &["x", "{literal here}"],
9536 },
9537 // The childless spelling, and the reason the wrapper is kept
9538 // rather than dropped in favour of its contents
9539 // (grammar-dispatch §6): with nothing inside, the wrapper is
9540 // the empty string's only carrier. Nothing is shed here, so
9541 // this row asserts the two columns agree.
9542 BracedWordCase {
9543 source: "set y {}\n",
9544 counts: [1, 1, 2, 2],
9545 before: [2, 2],
9546 operands: &["y", "{}"],
9547 },
9548 // braced_word_simple inside braced_word_simple: the nesting
9549 // that makes the over-count unbounded in depth. One value
9550 // spelled six vocabulary entries.
9551 BracedWordCase {
9552 source: "set a {x {y z}}\n",
9553 counts: [1, 1, 2, 2],
9554 before: [6, 6],
9555 operands: &["a", "{x {y z}}"],
9556 },
9557 // quoted_word, inert — an operand in its own right elsewhere,
9558 // and shed here.
9559 BracedWordCase {
9560 source: "set a {x \"q w\" v}\n",
9561 counts: [1, 1, 2, 2],
9562 before: [5, 5],
9563 operands: &["a", "{x \"q w\" v}"],
9564 },
9565 // quoted_word carrying an interpolation, which was *not* an
9566 // operand before the guard either (`string_operand_type` had
9567 // already suppressed it) — so only `x` and `v` are shed. Its
9568 // `$q` is a grandchild of the braced word and still counts,
9569 // which is the parent-scoping this arm inherits from #1314.
9570 BracedWordCase {
9571 source: "set a {x \"$q\" v}\n",
9572 counts: [1, 1, 3, 3],
9573 before: [5, 5],
9574 operands: &["a", "$q", "{x \"$q\" v}"],
9575 },
9576 // escaped_character, never classified — so it sheds nothing
9577 // and the row measures the two `simple_word`s around it.
9578 BracedWordCase {
9579 source: "set a {x \\n y}\n",
9580 counts: [1, 1, 2, 2],
9581 before: [4, 4],
9582 operands: &["a", "{x \\n y}"],
9583 },
9584 // variable_substitution. Tcl substitutes nothing between
9585 // braces, so `{$x}` is the two-character string `$x` — the row
9586 // that makes "the wrapper is the value" more than a tie-break.
9587 BracedWordCase {
9588 source: "set a {$x}\n",
9589 counts: [1, 1, 2, 2],
9590 before: [3, 3],
9591 operands: &["a", "{$x}"],
9592 },
9593 // command_substitution, likewise never an operand itself. Its
9594 // interior is, and stays so: `$q`, `puts` and `w` are
9595 // grandchildren. The nested `{puts w}` is a *script* body and
9596 // sheds under the other half of #1354.
9597 BracedWordCase {
9598 source: "set z {x [if {$q} {puts w}] v}\n",
9599 counts: [4, 5, 5, 5],
9600 before: [8, 8],
9601 operands: &["z", "$q", "puts", "w", "{x [if {$q} {puts w}] v}"],
9602 },
9603 // The same value twice: n2 3 against N2 4, so a row that
9604 // asserted only the vocabulary could not tell the two axes
9605 // apart (#1294).
9606 BracedWordCase {
9607 source: "set a {b c}\nset d {b c}\n",
9608 counts: [1, 2, 3, 4],
9609 before: [5, 8],
9610 operands: &["a", "d", "{b c}"],
9611 },
9612 // The parity pair #1317 named: two spellings of one literal
9613 // value must score alike. They did not before — braced 4 / 4
9614 // against quoted 2 / 2 — which is the spelling sensitivity
9615 // #695, #1312 and #1314 each removed elsewhere.
9616 BracedWordCase {
9617 source: "set a {one two}\n",
9618 counts: [1, 1, 2, 2],
9619 before: [4, 4],
9620 operands: &["a", "{one two}"],
9621 },
9622 BracedWordCase {
9623 source: "set a \"one two\"\n",
9624 counts: [1, 1, 2, 2],
9625 before: [2, 2],
9626 operands: &["a", "\"one two\""],
9627 },
9628 ];
9629
9630 /// The script half of #1354, shared by both dialects: a
9631 /// `braced_word` holding commands is a block whose contents the
9632 /// walk already counts, so it is no longer also an operand
9633 /// spanning the whole block.
9634 ///
9635 /// The three childless rows are the gate, and the reason this is a
9636 /// gate and not a deletion (grammar-dispatch §6). `braced_word` is
9637 /// not only the script kind: it is the value slot of every command
9638 /// the grammar does not special-case, where `lappend l {}` is an
9639 /// empty list whose brace pair is its only carrier. Deleting the
9640 /// kind scored that zero while its `lappend l ""` synonym — the
9641 /// last row, the control — scored one. An empty `proc` body is
9642 /// spelled identically and so also keeps an operand; #1318 tells
9643 /// the two roles apart by the enclosing command, but it bills them
9644 /// alike, so this gate is still what keeps the operand.
9645 ///
9646 /// The rows' *operator* columns moved with #1318 wherever the
9647 /// enclosing command takes a value, which is why the `lappend`
9648 /// rows here read `0` where the `proc` and `if` rows read the
9649 /// block's `{}`. The operand columns are untouched by it — #1318
9650 /// revises the brace and nothing else — so every `before` here
9651 /// still describes #1354 alone.
9652 #[cfg(any(feature = "irules", feature = "tcl"))]
9653 const SCRIPT_BODY_CASES: [BracedWordCase; 9] = [
9654 BracedWordCase {
9655 source: "proc p {} { set b 1 }\n",
9656 counts: [3, 4, 3, 3],
9657 before: [4, 4],
9658 operands: &["p", "b", "1"],
9659 },
9660 BracedWordCase {
9661 source: "proc p {} {}\n",
9662 counts: [2, 3, 2, 2],
9663 before: [2, 2],
9664 operands: &["p", "{}"],
9665 },
9666 // A comment is a named child of the script but not a command,
9667 // and no arm bills it, so a comment-only body scores like the
9668 // empty one above rather than like nothing: the block's whole
9669 // text is its one operand. Gating on "any named child" billed
9670 // it zero — adding a comment to an empty block lowered N2.
9671 BracedWordCase {
9672 source: "proc p {} {\n # only a comment\n}\n",
9673 counts: [2, 3, 2, 2],
9674 before: [2, 2],
9675 operands: &["p", "{\n # only a comment\n}"],
9676 },
9677 BracedWordCase {
9678 source: "if {$q} {\n # noop\n}\n",
9679 counts: [2, 3, 2, 2],
9680 before: [2, 2],
9681 operands: &["$q", "{\n # noop\n}"],
9682 },
9683 // The control: a comment *beside* a command changes nothing, so a
9684 // gate keyed on "contains a comment" would fail this row.
9685 BracedWordCase {
9686 source: "proc p {} {\n # c\n set b 1\n}\n",
9687 counts: [3, 4, 3, 3],
9688 before: [4, 4],
9689 operands: &["p", "b", "1"],
9690 },
9691 // The value-role twin: inside a literal string Tcl performs no
9692 // substitution, so `# x` is not a comment at all, and the word
9693 // is its one operand exactly as `lappend l {}` below is. Its
9694 // operator columns are zero since #1318 — `lappend` takes a
9695 // value, so the brace pair is a quote and not a block.
9696 BracedWordCase {
9697 source: "lappend l {\n # x\n}\n",
9698 counts: [0, 0, 3, 3],
9699 before: [3, 3],
9700 operands: &["lappend", "l", "{\n # x\n}"],
9701 },
9702 // A `braced_word` in *value* position — the #1318 role, where
9703 // the same kind carries a literal list. Its interior words
9704 // counted before #1354 and still do; what #1354 removed is the
9705 // whole-block operand that was double-billing them, and what
9706 // #1318 removed is the `{}` operator (`lappend` takes a value,
9707 // so the brace pair quotes rather than opens).
9708 BracedWordCase {
9709 source: "lappend l {a b}\n",
9710 counts: [0, 0, 4, 4],
9711 before: [5, 5],
9712 operands: &["lappend", "l", "a", "b"],
9713 },
9714 // …and the childless spelling of that value, which sheds
9715 // nothing: the brace pair is the empty list's only carrier.
9716 BracedWordCase {
9717 source: "lappend l {}\nreturn {}\n",
9718 counts: [0, 0, 4, 5],
9719 before: [4, 5],
9720 operands: &["lappend", "l", "return", "{}"],
9721 },
9722 // The control the row above is measured against: the quoted
9723 // spelling of the same empty value, which never depended on
9724 // the arm and must keep scoring one operand. Since #1318 the
9725 // two rows agree on all four columns rather than only on the
9726 // operand pair — an empty list is an empty list however it is
9727 // spelled.
9728 BracedWordCase {
9729 source: "lappend l \"\"\nreturn \"\"\n",
9730 counts: [0, 0, 4, 5],
9731 before: [4, 5],
9732 operands: &["lappend", "l", "return", "\"\""],
9733 },
9734 ];
9735
9736 /// The table must exercise every named child kind the grammar
9737 /// admits inside a braced word, and no other — the other half of
9738 /// the drift marker in `braced_word_shed`, which can only police
9739 /// kinds a fixture actually produces.
9740 #[cfg(any(feature = "irules", feature = "tcl"))]
9741 fn assert_braced_word_children_witnessed(
9742 witnessed: &HashSet<u16>,
9743 kinds: &BracedWordKinds,
9744 dialect: &str,
9745 ) {
9746 let mut got: Vec<u16> = witnessed.iter().copied().collect();
9747 got.sort_unstable();
9748 let mut expected = kinds.children;
9749 expected.sort_unstable();
9750 assert_eq!(
9751 got.as_slice(),
9752 expected.as_slice(),
9753 "{dialect}: the fixtures must witness every child kind \
9754 node-types.json admits inside a braced word",
9755 );
9756 }
9757
9758 /// Regression for #1354 and #1317. `braced_word_simple` is a
9759 /// literal value and `braced_word` a script, and both were
9760 /// operands *beside* the content the walk counts anyway: a braced
9761 /// word was billed once per inner word plus once for itself, and a
9762 /// block once for every command in it plus once for its whole
9763 /// text. `set x {literal here}` scored n2 4 / N2 4 for one value.
9764 /// The name is the invariant either way — the value's content is
9765 /// the literal, the script's is its commands, and each is billed
9766 /// once.
9767 ///
9768 /// Each row's `before` column is re-derived by the loop from the
9769 /// current parse rather than trusted. That derivation is a replica
9770 /// of the pre-#1354 arms and could in principle drift from what
9771 /// they did, so every column was also measured directly against a
9772 /// build of the old getters — `lappend l {a b}` at n2 5 / N2 5,
9773 /// `lappend l {}` at 4 / 5, `proc p {} {}` at 2 / 2 — rather than
9774 /// derived only from the model here.
9775 ///
9776 /// The same walk doubles as the drift marker for the parent-keyed
9777 /// arm: it fails if the grammar ever puts a seventh kind directly
9778 /// inside a braced word, and the union assertion below fails if a
9779 /// bump stops emitting one of the six.
9780 #[cfg(feature = "tcl")]
9781 #[test]
9782 fn tcl_braced_word_bills_its_content_once_1354() {
9783 let mut witnessed = check_braced_word_cases::<TclParser, TclCode>(
9784 &BRACED_WORD_CASES,
9785 "foo.tcl",
9786 &TCL_BRACED_WORD_KINDS,
9787 );
9788 witnessed.extend(check_braced_word_cases::<TclParser, TclCode>(
9789 &SCRIPT_BODY_CASES,
9790 "foo.tcl",
9791 &TCL_BRACED_WORD_KINDS,
9792 ));
9793 assert_braced_word_children_witnessed(&witnessed, &TCL_BRACED_WORD_KINDS, "tcl");
9794 }
9795
9796 /// The iRules twin. The tables are shared, so a fix that reached
9797 /// only `big-code-analysis-ast/src/getter/tcl.rs` fails every row
9798 /// here — the two getters are deliberate clones and #1354 names
9799 /// both.
9800 #[cfg(feature = "irules")]
9801 #[test]
9802 fn irules_braced_word_bills_its_content_once_1354() {
9803 let mut witnessed = check_braced_word_cases::<IrulesParser, IrulesCode>(
9804 &BRACED_WORD_CASES,
9805 "foo.irule",
9806 &IRULES_BRACED_WORD_KINDS,
9807 );
9808 witnessed.extend(check_braced_word_cases::<IrulesParser, IrulesCode>(
9809 &SCRIPT_BODY_CASES,
9810 "foo.irule",
9811 &IRULES_BRACED_WORD_KINDS,
9812 ));
9813 // The `when` handler body is the iRules-only spelling of a
9814 // script body, and the largest instance of the defect: its
9815 // operand text was the entire event handler.
9816 let handlers: [BracedWordCase; 3] = [
9817 BracedWordCase {
9818 source: "when HTTP_REQUEST { set x 1 }\n",
9819 counts: [3, 3, 3, 3],
9820 before: [4, 4],
9821 operands: &["HTTP_REQUEST", "x", "1"],
9822 },
9823 BracedWordCase {
9824 source: "when HTTP_REQUEST {}\n",
9825 counts: [2, 2, 2, 2],
9826 before: [2, 2],
9827 operands: &["HTTP_REQUEST", "{}"],
9828 },
9829 // The comment-only twin of the empty handler above; see the
9830 // shared table for why it scores like it.
9831 BracedWordCase {
9832 source: "when HTTP_REQUEST {\n # only a comment\n}\n",
9833 counts: [2, 2, 2, 2],
9834 before: [2, 2],
9835 operands: &["HTTP_REQUEST", "{\n # only a comment\n}"],
9836 },
9837 ];
9838 witnessed.extend(check_braced_word_cases::<IrulesParser, IrulesCode>(
9839 &handlers,
9840 "foo.irule",
9841 &IRULES_BRACED_WORD_KINDS,
9842 ));
9843 assert_braced_word_children_witnessed(&witnessed, &IRULES_BRACED_WORD_KINDS, "irules");
9844 }
9845
9846 /// One row of the #1318 tables: a fixture and the four Halstead
9847 /// columns plus the operand vocabulary behind them.
9848 ///
9849 /// No `before` column, unlike [`BracedWordCase`]: #1318 is not a
9850 /// guard whose absence a walk can model, it is a *classification*.
9851 /// The pre-fix counts are recorded per row in prose instead, and
9852 /// each was measured against a build without the
9853 /// `get_op_type_with_code` overrides.
9854 struct BracedWordValueCase {
9855 source: &'static str,
9856 /// `[n1, N1, n2, N2]`.
9857 counts: [u64; 4],
9858 operands: &'static [&'static str],
9859 }
9860
9861 /// Runs one #1318 table against one dialect. Both dialects run
9862 /// every shared row, so a fix that reached one getter and not its
9863 /// clone fails here.
9864 #[cfg(any(feature = "irules", feature = "tcl"))]
9865 fn check_braced_word_value_cases<T: crate::MetricSuite>(
9866 cases: &[BracedWordValueCase],
9867 file: &str,
9868 ) {
9869 for case in cases {
9870 assert_halstead_counts::<T>(case.source, file, case.counts, case.source);
9871 assert_ops_operands::<T>(
9872 case.source,
9873 file,
9874 case.operands.len(),
9875 case.operands.to_vec(),
9876 );
9877 }
9878 }
9879
9880 /// The #1318 rows both dialects share. Every independent path
9881 /// through the role test gets a row of its own
9882 /// (`.claude/rules/testing.md`, "give one input to each independent
9883 /// path"): the value default, the script-command list, a modelled
9884 /// construct reached through a `word_list`, the two modelled slots
9885 /// that hold values, an unresolvable command name, and both
9886 /// nesting directions.
9887 ///
9888 /// **Every row asserts the operand columns too, and they are the
9889 /// half that must not move.** #1318 revises one operator and
9890 /// nothing else, so a mutant that suppressed a value word's
9891 /// *contents* — the tidier-looking rule, which collapses an
9892 /// `oo::class create C {…}` body into a single operand — fails
9893 /// here rather than passing as an improvement. #1382 keeps to the
9894 /// same discipline: it withdraws further `{}` operators and moves
9895 /// no operand column in this table.
9896 ///
9897 /// The trailing rows are #1382's. The first four are two *pairs*:
9898 /// a braced value slot beside the bare spelling of the same
9899 /// argument, which must score alike. Before #1382 they did not —
9900 /// the braced spelling billed one extra `{}`. The four after them
9901 /// carry no bare partner: each braces a value with a space in it
9902 /// (`proc {my proc}`, `{a b}`, `{code msg}`), which is the whole
9903 /// reason the braces are there. Their pre-#1382 counts are
9904 /// recorded per row instead.
9905 #[cfg(any(feature = "irules", feature = "tcl"))]
9906 const BRACED_WORD_VALUE_CASES: [BracedWordValueCase; 24] = [
9907 // The rule reaches the opener and *only* the opener. A `;`
9908 // separating two commands is a direct child of the
9909 // `braced_word` — `_terminator` is a hidden rule, so it is
9910 // inlined rather than wrapped — and both dialects classify
9911 // `SEMI` as an operator, so a revision keyed on the parent
9912 // kind alone swallowed it too: this row read n1 0 / N1 0 and
9913 // `halstead.effort` 0.0, against a line that is two commands.
9914 // The pairing with the `eval` row below is the point — same
9915 // brace, same separator, one command name apart.
9916 BracedWordValueCase {
9917 source: "lappend l {puts a ; puts b}\n",
9918 counts: [1, 1, 5, 6],
9919 operands: &["lappend", "l", "puts", "a", "b"],
9920 },
9921 BracedWordValueCase {
9922 source: "eval {puts a ; puts b}\n",
9923 counts: [2, 2, 4, 5],
9924 operands: &["eval", "puts", "a", "b"],
9925 },
9926 // The reported fixture. `lappend` takes a list, so `{a b}`
9927 // quotes rather than opens: no operator. Before #1318 this
9928 // read n1 1 / N1 1 — a `{}` block the line does not contain.
9929 BracedWordValueCase {
9930 source: "lappend l {a b}\n",
9931 counts: [0, 0, 4, 4],
9932 operands: &["lappend", "l", "a", "b"],
9933 },
9934 // The issue's second line, and the one showing the defect
9935 // needs no second argument to appear.
9936 BracedWordValueCase {
9937 source: "puts {c d}\n",
9938 counts: [0, 0, 3, 3],
9939 operands: &["puts", "c", "d"],
9940 },
9941 // The headline: one value, two commands, and until #1318 two
9942 // different operator counts — `set` reported none, `lappend` a
9943 // `{}`. `set` is an operator and `lappend` an operand because
9944 // the grammar models one command and not the other; that
9945 // asymmetry is the grammar's, not this rule's.
9946 BracedWordValueCase {
9947 source: "set x {a b}\nlappend y {a b}\n",
9948 counts: [1, 1, 6, 6],
9949 operands: &["lappend", "x", "y", "{a b}", "a", "b"],
9950 },
9951 // The script side of the same shape, and the reason `LBRACE`
9952 // is gated rather than deleted (grammar-dispatch §6): `eval`
9953 // evaluates its argument, so the block is real and keeps its
9954 // `{}`.
9955 BracedWordValueCase {
9956 source: "eval {puts hi}\n",
9957 counts: [1, 1, 3, 3],
9958 operands: &["eval", "puts", "hi"],
9959 },
9960 // The same command named through the global namespace, which
9961 // is the spelling a `namespace eval` body uses to guarantee the
9962 // core command over a local proc shadowing it. `::eval` *is*
9963 // `eval`, so the two rows must agree on the operator column;
9964 // before the leading `::` was stripped this one fell to the
9965 // value default and lost its block.
9966 BracedWordValueCase {
9967 source: "::eval {puts hi}\n",
9968 counts: [1, 1, 3, 3],
9969 operands: &["::eval", "puts", "hi"],
9970 },
9971 // The control for that strip: only the *leading* qualifier is
9972 // a namespace path to the core command. `ns::eval` is a
9973 // different proc living in `ns`, so it takes the value default
9974 // like any other unrecognised name — a strip of every `::`
9975 // segment would have promoted it to a script.
9976 BracedWordValueCase {
9977 source: "ns::eval {a b}\n",
9978 counts: [0, 0, 3, 3],
9979 operands: &["ns::eval", "a", "b"],
9980 },
9981 // The default for an unrecognised command — a user proc here,
9982 // but equally any package command. Nothing distinguishes it
9983 // from `lappend` structurally, so the row exists to pin which
9984 // way the coin lands. Note the words inside are still
9985 // operands: the rule withdraws the block, never the code.
9986 BracedWordValueCase {
9987 source: "myproc {g h}\n",
9988 counts: [0, 0, 3, 3],
9989 operands: &["myproc", "g", "h"],
9990 },
9991 // `namespace` is the one construct either grammar models that
9992 // still reaches its body through a `word_list`, so it is the
9993 // row that fails if the rule stops at the parent instead of
9994 // reading the grandparent. Its body must stay a script.
9995 BracedWordValueCase {
9996 source: "namespace eval ns {\n set a 1\n}\n",
9997 counts: [3, 3, 4, 4],
9998 operands: &["eval", "ns", "a", "1"],
9999 },
10000 // A literal nested in a literal: neither brace opens a block,
10001 // and both words survive.
10002 BracedWordValueCase {
10003 source: "puts {a {b c}}\n",
10004 counts: [0, 0, 4, 4],
10005 operands: &["puts", "a", "b", "c"],
10006 },
10007 // A `set` inside a literal. The `set` keyword and the `[]`
10008 // substitution are real operators of a command the walk
10009 // descends into, and they stay — the rule reaches exactly the
10010 // one brace whose parent it can classify, never a subtree.
10011 // `{x [foo] v}` is a `braced_word_simple`, so #1354 bills it
10012 // whole and its `[foo]` interior separately, unchanged.
10013 BracedWordValueCase {
10014 source: "lappend l {set a {x [foo] v}}\n",
10015 counts: [2, 2, 5, 5],
10016 operands: &["lappend", "l", "a", "{x [foo] v}", "foo"],
10017 },
10018 // A braced word in the command *name* position. Legal Tcl —
10019 // `{puts} hi` invokes `puts` — and the one generic position
10020 // that is not an argument, so it is answered before the
10021 // command lookup. It must also never reach the `switch`-arm
10022 // rescue, which is what the braced-pattern row in the Tcl
10023 // table checks.
10024 BracedWordValueCase {
10025 source: "{puts} hi\n",
10026 counts: [0, 0, 2, 2],
10027 operands: &["puts", "hi"],
10028 },
10029 // A defaulted `proc` parameter — the other modelled slot that
10030 // holds a value, decided by the parent kind rather than by a
10031 // command name. `{x y}` is data the interpreter assigns, never
10032 // a script it runs, and it billed a fourth `{}` before #1318.
10033 // The three that remain are the parameter list, the parameter
10034 // spec and the body: those two list delimiters group a
10035 // declaration rather than quote a value, the line #1354 drew
10036 // for `Arguments`.
10037 BracedWordValueCase {
10038 source: "proc p {a {b {x y}}} { puts $a }\n",
10039 counts: [2, 4, 7, 7],
10040 operands: &["p", "a", "b", "x", "y", "puts", "$a"],
10041 },
10042 // A literal nested in a script, the direction that must keep
10043 // working: the `proc` body is a block (`{}` twice with the
10044 // parameter list) and the list it appends is not.
10045 BracedWordValueCase {
10046 source: "proc p {} { lappend l {a b} }\n",
10047 counts: [2, 3, 5, 5],
10048 operands: &["p", "lappend", "l", "a", "b"],
10049 },
10050 // A computed command name resolves to nothing, so the value
10051 // default decides. `$c` is the reference's own operand.
10052 BracedWordValueCase {
10053 source: "set c puts\n$c {a b}\n",
10054 counts: [1, 1, 5, 5],
10055 operands: &["c", "puts", "$c", "a", "b"],
10056 },
10057 // #1382's headline, and the pair that names the defect: `after`
10058 // is a script-taking command, but only from its *second*
10059 // argument — the first is a millisecond count. Both spellings
10060 // of that count are one operand and no operator; before #1382
10061 // the braced one billed a `{}` for a block the line does not
10062 // contain, reading N1 2 against the bare form's 1. The `{}`
10063 // both rows do report is the `{puts hi}` script, which is real.
10064 BracedWordValueCase {
10065 source: "after {100} {puts hi}\n",
10066 counts: [1, 1, 4, 4],
10067 operands: &["after", "100", "puts", "hi"],
10068 },
10069 BracedWordValueCase {
10070 source: "after 100 {puts hi}\n",
10071 counts: [1, 1, 4, 4],
10072 operands: &["after", "100", "puts", "hi"],
10073 },
10074 // The same pair for `time script ?count?`, whose value slot is
10075 // the *last* rather than the first — so a rule that hard-coded
10076 // "argument 0 is the value" passes the `after` pair and fails
10077 // this one. The script argument keeps its `{}` in both rows,
10078 // which is what makes the operator column a 1 and not a 0.
10079 //
10080 // The *bare* row is the half that catches it, which is not the
10081 // half it looks like. Measured by giving `time` `after`'s slot
10082 // rule: `time {puts hi} 3` reads n1 0 / N1 0, its one block
10083 // demoted to a value — while the braced row below is unmoved
10084 // at [1, 1, 4, 4], because `{puts hi}` loses the `{}` that
10085 // `{3}` gains and the operand texts are the same either way.
10086 // Do not trim the bare spelling as redundant with the braced
10087 // one; here it is the discriminating row of the two.
10088 BracedWordValueCase {
10089 source: "time {puts hi} {3}\n",
10090 counts: [1, 1, 4, 4],
10091 operands: &["time", "puts", "hi", "3"],
10092 },
10093 BracedWordValueCase {
10094 source: "time {puts hi} 3\n",
10095 counts: [1, 1, 4, 4],
10096 operands: &["time", "puts", "hi", "3"],
10097 },
10098 // A braced `proc` *name*, the one value slot the grammar names
10099 // as a field rather than by position. `{my proc}` is an
10100 // identifier containing a space, not a body, and billed a
10101 // fourth `{}` before #1382. The three operators left are the
10102 // `proc` keyword and the two braces of the parameter list and
10103 // the body — and note the interior words survive, `proc` being
10104 // an operand here and an operator one column over.
10105 BracedWordValueCase {
10106 source: "proc {my proc} {} {}\n",
10107 counts: [2, 3, 3, 3],
10108 operands: &["my", "proc", "{}"],
10109 },
10110 // The two `namespace` subcommands that take values throughout,
10111 // both of them Tcl 8.6 standard-library spellings. Their braced
10112 // argument is a pattern list and a dictionary; neither is
10113 // evaluated, and each billed a `{}` before #1382 — n1 2 / N1 2
10114 // against the 1 / 1 here. The `namespace` keyword is the
10115 // operator that remains, which is what keeps these two rows
10116 // from reading `effort` 0.0.
10117 BracedWordValueCase {
10118 source: "namespace export {a b}\n",
10119 counts: [1, 1, 3, 3],
10120 operands: &["export", "a", "b"],
10121 },
10122 BracedWordValueCase {
10123 source: "namespace ensemble create -map {a b}\n",
10124 counts: [1, 1, 5, 5],
10125 operands: &["ensemble", "create", "-map", "a", "b"],
10126 },
10127 // A `trap` clause written outside a `try`, which is how both
10128 // grammars reach the generic-command path for it. This is the
10129 // `ScriptSlots::LastOf` row — the pattern and the variable list
10130 // are values and the handler is the script — and the only one
10131 // whose slot is counted from the *end* of the list, so a rule
10132 // that read `Only(2)` from the front would need the arity to
10133 // pass it. Before #1382 all three braces billed: N1 3.
10134 BracedWordValueCase {
10135 source: "trap {POSIX} {code msg} {puts hi}\n",
10136 counts: [1, 1, 6, 6],
10137 operands: &["trap", "POSIX", "code", "msg", "puts", "hi"],
10138 },
10139 ];
10140
10141 /// Regression for #1318. `braced_word` carries both a block and a
10142 /// plain literal, and the grammars spell the two identically, so
10143 /// #1314's kind-scoped guard reached only the value slots they
10144 /// special-case: `set x {a b}` reported no operator while
10145 /// `lappend x {a b}` reported a `{}` for a block the line does not
10146 /// contain. The role is recognised out-of-band, by the enclosing
10147 /// command's leading word (grammar-dispatch §9).
10148 ///
10149 /// The Tcl-only rows are the constructs this grammar models with
10150 /// no node of its own (#467, #1264) — `switch` and `for` — plus the
10151 /// braced `switch` *pattern* that must not be mistaken for an arm
10152 /// body, in both the one-arm-per-line and the one-line layouts.
10153 /// Their iRules counterparts are dedicated nodes and are covered by
10154 /// the sibling test.
10155 #[cfg(feature = "tcl")]
10156 #[test]
10157 fn tcl_braced_word_role_follows_the_enclosing_command_1318() {
10158 check_braced_word_value_cases::<TclParser>(&BRACED_WORD_VALUE_CASES, "foo.tcl");
10159 let tcl_only: [BracedWordValueCase; 8] = [
10160 // `switch` arm bodies. The grammar flattens `pat body pat
10161 // body` into a `command` named after the first pattern, so
10162 // the bodies read as arguments of a command called `a` —
10163 // the shape the value default would call a literal. The
10164 // arm list's own `switch` name is what rescues them, and
10165 // `puts` occurring in both bodies is what separates n2
10166 // from N2.
10167 BracedWordValueCase {
10168 source: "switch $v { a { puts A } b { puts B } }\n",
10169 counts: [1, 3, 7, 8],
10170 operands: &["switch", "$v", "a", "b", "puts", "A", "B"],
10171 },
10172 // …and the limit of that rescue. A braced *pattern* is the
10173 // arm command's `name`, not an argument, so it is a
10174 // literal — `switch -regexp $v { {^a.*b$} {…} }` is
10175 // idiomatic and its regex must not be wrapped in a block.
10176 // Rescuing the whole arm command would read N1 4 here.
10177 // The leading `-exact` / `--` options are `simple_word`
10178 // operands of the *`switch`* command and shift nothing:
10179 // the arm list is found as the sole `braced_word` argument,
10180 // not by index (grammar-dispatch §9).
10181 BracedWordValueCase {
10182 source: "switch -exact -- $v {\n {a b} { puts X }\n default { puts Y }\n}\n",
10183 counts: [1, 3, 10, 11],
10184 operands: &[
10185 "switch", "-exact", "--", "$v", "a", "b", "puts", "X", "default", "Y",
10186 ],
10187 },
10188 // The same arms on one line. Only the *first* pattern is
10189 // the arm command's name; `{^b}` is its third argument, and
10190 // the name test alone rescued it as a body — N1 4 here,
10191 // against N1 3 for the identical arms written one per line.
10192 // The arm list is `pattern body pattern body …` by Tcl's
10193 // own definition, so the arguments alternate and the
10194 // even-indexed ones are the bodies.
10195 BracedWordValueCase {
10196 source: "switch -regexp $v { {^a} { puts A } {^b} { puts B } }\n",
10197 counts: [1, 3, 8, 9],
10198 operands: &["switch", "-regexp", "$v", "^a", "puts", "A", "^b", "B"],
10199 },
10200 // The words that can interpose between a pattern and its
10201 // body take one slot each and leave the parity intact: a
10202 // `-` fall-through body and a `default` pattern are both
10203 // `simple_word`s. The two braced bodies are at indexes 2
10204 // and 4 of the run and both keep their `{}`.
10205 BracedWordValueCase {
10206 source: "switch $v { a - b { puts AB } default { puts D } }\n",
10207 counts: [1, 3, 9, 10],
10208 operands: &["switch", "$v", "a", "-", "b", "puts", "AB", "default", "D"],
10209 },
10210 // `for` has four braced arguments and all four are
10211 // evaluated, so all four keep their braces. `i` appears as
10212 // the `set` target and again inside `incr`, and `$i` in
10213 // the condition and the body, which is what separates
10214 // n2 = 8 from N2 = 10. `<` is an *operand* here rather
10215 // than a comparison operator: with no `for` rule there is
10216 // no `expr` slot, so the condition parses as a command and
10217 // its comparison never surfaces as a token — the same
10218 // grammar limitation #1264 recorded for ABC.
10219 BracedWordValueCase {
10220 source: "for {set i 0} {$i < 3} {incr i} { puts $i }\n",
10221 counts: [2, 5, 8, 10],
10222 operands: &["for", "i", "0", "$i", "<", "3", "incr", "puts"],
10223 },
10224 // #1382's guard rows: an arm *pattern* spelled like a
10225 // command whose value slots the rule now withdraws. An arm
10226 // list parses as commands, so the pattern becomes a real
10227 // command name and the arm *body* lands in one of that
10228 // command's value slots — where only `Getter::is_switch_arm`
10229 // keeps it a block. Both rows are `switch` bodies that must
10230 // keep their `{}`, and both were measured with the guard
10231 // short-circuited to `false`.
10232 //
10233 // This one is the positional half: `after`'s slot 0 is the
10234 // millisecond count, so without the guard `{ puts B }` reads
10235 // as that value and the row drops to N1 1.
10236 BracedWordValueCase {
10237 source: "switch $v { after { puts B } }\n",
10238 counts: [1, 2, 5, 5],
10239 operands: &["switch", "$v", "after", "puts", "B"],
10240 },
10241 // …and this is the field-keyed half. `proc` needs three
10242 // words, so the arm patterns must be written one per line
10243 // for the grammar to build a `procedure` at all — on one
10244 // line it recovers into an `ERROR` and no rule fires. Here
10245 // it swallows two arms, taking `{ puts A }` as its `name`
10246 // field and `{ puts B }` as its body; without the guard the
10247 // name's brace goes and the row reads N1 5. The `namespace`
10248 // arm is the third spelling of the same hazard and is not
10249 // discriminating: its subcommand slot holds the braced body
10250 // itself rather than a `simple_word`, so no layout resolves
10251 // and the construct-wide script answer stands either way.
10252 // `proc` and `namespace` are operators here and `after` an
10253 // operand, which is the three patterns' whole difference.
10254 BracedWordValueCase {
10255 source: "switch $v {\n proc { puts A }\n after { puts B }\n namespace { puts C }\n}\n",
10256 counts: [3, 6, 7, 9],
10257 operands: &["switch", "$v", "after", "puts", "A", "B", "C"],
10258 },
10259 // The arity guard on `ScriptSlots::LastOf`, Tcl-only
10260 // because the iRules grammar models `trap_handler` and so
10261 // never reaches the generic-command path (#1382 review).
10262 // Tcl models neither `on` nor `trap`, so the whole tail
10263 // after the `try` body parses as *one* `trap` command with
10264 // five arguments — `{p} {v} {puts b} finally {puts c}`.
10265 // Reading "the script is the last argument" off that names
10266 // the `finally` body and calls `{puts b}` a value, which
10267 // dropped the `{}` of a block the line does contain and
10268 // read N1 3. All five braces are blocks here, which is the
10269 // answer a list the signature cannot explain has to get.
10270 // The `trap` row in the shared table is the same clause at
10271 // its documented arity, where two of the three braces
10272 // really are values — so a guard stuck on "always script"
10273 // fails there and one stuck on "always last" fails here.
10274 BracedWordValueCase {
10275 source: "try {puts a} trap {p} {v} {puts b} finally {puts c}\n",
10276 counts: [2, 6, 8, 10],
10277 operands: &["a", "b", "c", "finally", "p", "puts", "trap", "v"],
10278 },
10279 ];
10280 check_braced_word_value_cases::<TclParser>(&tcl_only, "foo.tcl");
10281 }
10282
10283 /// The iRules twin of the test above. The shared table runs
10284 /// unchanged — the two getters are deliberate clones and #1318
10285 /// names both — and the dialect rows cover the handler bodies,
10286 /// which have no Tcl spelling.
10287 #[cfg(feature = "irules")]
10288 #[test]
10289 fn irules_braced_word_role_follows_the_enclosing_command_1318() {
10290 check_braced_word_value_cases::<IrulesParser>(&BRACED_WORD_VALUE_CASES, "foo.irule");
10291 let irules_only: [BracedWordValueCase; 4] = [
10292 // A `when` handler body is a modelled slot, so the literal
10293 // inside it is classified independently of the block that
10294 // holds it.
10295 BracedWordValueCase {
10296 source: "when HTTP_REQUEST {\n lappend b {x y}\n}\n",
10297 counts: [2, 2, 5, 5],
10298 operands: &["HTTP_REQUEST", "lappend", "b", "x", "y"],
10299 },
10300 // This grammar models `switch` with `switch_arm` children,
10301 // so the arm body fills a modelled slot and never reaches
10302 // the command-name list the Tcl rows need.
10303 BracedWordValueCase {
10304 source: "when HTTP_REQUEST {\n switch $v { a { pool p1 } }\n}\n",
10305 counts: [3, 5, 5, 5],
10306 operands: &["HTTP_REQUEST", "$v", "a", "pool", "p1"],
10307 },
10308 // `on` and `trap` are why the command list carries two
10309 // words no *Tcl* command spells. The grammar emits
10310 // `on_handler` / `trap_handler` only under `try` — pinned
10311 // by `irules_try_handler_kinds_appear_only_under_try` — so
10312 // a statement-level handler is a generic command, and
10313 // without the list entry its body would lose the `{}` that
10314 // the second occurrence here counts.
10315 BracedWordValueCase {
10316 source: "when RULE_INIT { set x 1 }\non error { log local0. oops }\n",
10317 counts: [3, 4, 8, 8],
10318 operands: &[
10319 "RULE_INIT",
10320 "x",
10321 "1",
10322 "on",
10323 "error",
10324 "log",
10325 "local0.",
10326 "oops",
10327 ],
10328 },
10329 // #1382 inside a modelled handler slot: the `when` body is
10330 // a block and keeps its `{}`, and the `after` delay nested
10331 // in it is a value and does not. Before #1382 this read
10332 // N1 4. The shared table's `after` pair already runs for
10333 // this dialect; what this row adds is that the rule reaches
10334 // a command nested inside a handler, where the `{}` billed
10335 // for the handler could otherwise mask a withdrawal.
10336 BracedWordValueCase {
10337 source: "when HTTP_REQUEST {\n after {100} {puts hi}\n}\n",
10338 counts: [2, 3, 5, 5],
10339 operands: &["HTTP_REQUEST", "after", "100", "puts", "hi"],
10340 },
10341 ];
10342 check_braced_word_value_cases::<IrulesParser>(&irules_only, "foo.irule");
10343 }
10344
10345 #[cfg(feature = "php")]
10346 #[test]
10347 fn php_operators_and_operands() {
10348 check_metrics::<PhpParser>(
10349 "<?php
10350 function avg(int $a, int $b, int $c): int {
10351 return ($a + $b + $c) / 3;
10352 }",
10353 "foo.php",
10354 |metric| {
10355 // After #695 only the opening delimiters count: `()` and
10356 // `{}` fold to one operator each per balanced pair, so the
10357 // former `)`/`}` closers no longer inflate n1/N1 (was
10358 // 11 unique / 15 total).
10359 //
10360 // Operands after #1293, tallied by `get_id` (source bytes):
10361 // `avg` × 1, `int` × 4 (the `primitive_type` wrapper at
10362 // all four type positions — its `int` keyword child is
10363 // suppressed under it), `$a` / `$b` / `$c` × 2 each,
10364 // `3` × 1 ⇒ n2 = 6, N2 = 12. Between #1259 and #1293 the
10365 // keyword leaf doubled the type count ⇒ 6 / 16; before
10366 // #1259 each `$v` also contributed its sigil-less `name`
10367 // leaf ⇒ 9 / 22.
10368 assert_eq!(metric.halstead.unique_operators(), 9);
10369 assert_eq!(metric.halstead.total_operators(), 12);
10370 assert_eq!(metric.halstead.unique_operands(), 6);
10371 assert_eq!(metric.halstead.total_operands(), 12);
10372 insta::assert_json_snapshot!(metric.halstead);
10373 },
10374 );
10375 }
10376
10377 #[cfg(feature = "php")]
10378 #[test]
10379 fn php_simple_function() {
10380 check_metrics::<PhpParser>(
10381 "<?php
10382 function inc(int $x): int { return $x + 1; }",
10383 "foo.php",
10384 |metric| {
10385 // After #695 only opening delimiters count: the `)`/`}`
10386 // closers no longer add operators (was 9 unique / 9 total).
10387 //
10388 // Operands after #1293: `inc` × 1, `int` × 2 (the
10389 // `primitive_type` wrapper at both type positions, its
10390 // `int` keyword child suppressed under it), `$x` × 2,
10391 // `1` × 1 ⇒ n2 = 4, N2 = 6. Between #1259 and #1293 the
10392 // keyword leaf doubled the type count ⇒ 4 / 8; before
10393 // #1259 `$x` also contributed its `x` leaf twice ⇒ 5 / 10.
10394 assert_eq!(metric.halstead.unique_operators(), 7);
10395 assert_eq!(metric.halstead.total_operators(), 7);
10396 assert_eq!(metric.halstead.unique_operands(), 4);
10397 assert_eq!(metric.halstead.total_operands(), 6);
10398 insta::assert_json_snapshot!(metric.halstead);
10399 },
10400 );
10401 }
10402
10403 #[cfg(feature = "php")]
10404 #[test]
10405 fn php_variable_reference_counts_once() {
10406 // Regression: issue #1259. `$x` parses as a `variable_name`
10407 // wrapping a `name` leaf, and both kinds were in the operand
10408 // arm — so every variable reference contributed twice to N2 and
10409 // planted a sigil-less twin (`x` beside `$x`) in the n2
10410 // vocabulary. Since `$var` is the most common token class in
10411 // PHP, that roughly doubled N2 for real files.
10412 //
10413 // Source: the issue's reproducer plus a re-reference of `$a` and
10414 // `$b`, so N2 exceeds n2 and the assertions can tell "counted
10415 // once per occurrence" from "deduplicated into the vocabulary".
10416 // <?php $a = null; $b = true; $c = NULL; $a = $b;
10417 //
10418 // Operands by text key: `$a` × 2, `$b` × 2, `$c`, `null`, `true`,
10419 // `NULL` ⇒ n2 = 6, N2 = 8. Before the fix the `a` / `b` / `c`
10420 // leaves added 3 unique and 5 occurrences ⇒ 9 / 13.
10421 check_metrics::<PhpParser>(
10422 "<?php\n$a = null;\n$b = true;\n$c = NULL;\n$a = $b;\n",
10423 "foo.php",
10424 |metric| {
10425 assert_eq!(metric.halstead.unique_operands(), 6);
10426 assert_eq!(metric.halstead.total_operands(), 8);
10427 },
10428 );
10429 }
10430
10431 #[cfg(feature = "php")]
10432 #[test]
10433 fn php_dynamic_variable_name_counts_once_at_any_depth() {
10434 // Regression: issue #1259. Variable-variable syntax nests the
10435 // wrappers, so the double count compounds: `$$a` is a
10436 // `dynamic_variable_name` → `variable_name` → `name` chain that
10437 // scored 3 for one reference, and `$$$b` scored 4. Only the
10438 // outermost wrapper may count.
10439 //
10440 // Source: <?php $$a = 1; $$$b = 2; ${$c} = 3; $$a = 4;
10441 // The trailing re-assignment repeats `$$a` so N2 exceeds n2 and
10442 // the assertions can tell "counted once per occurrence" from
10443 // "deduplicated into the vocabulary".
10444 //
10445 // Operands: `$$a` × 2, `$$$b`, `${$c}`, `1`, `2`, `3`, `4`
10446 // ⇒ n2 = 7, N2 = 8. Before the fix: 14 / 17 (measured), each
10447 // target contributing its whole nesting chain — `$$a` → `$a` →
10448 // `a` is 3 (twice over), `$$$b` → `$$b` → `$b` → `b` is 4, and
10449 // `${$c}` → `$c` → `c` is 3, plus the four integers.
10450 check_metrics::<PhpParser>(
10451 "<?php $$a = 1; $$$b = 2; ${$c} = 3; $$a = 4;",
10452 "foo.php",
10453 |metric| {
10454 assert_eq!(metric.halstead.unique_operands(), 7);
10455 assert_eq!(metric.halstead.total_operands(), 8);
10456 },
10457 );
10458 }
10459
10460 #[cfg(feature = "php")]
10461 #[test]
10462 fn php_dynamic_variable_name_guard_is_parent_scoped() {
10463 // Companion to the two tests above (#1259): the guards fire on
10464 // the *parent* kind, never on the kind alone, so the two
10465 // positions where a nested node is a reference in its own right
10466 // keep counting.
10467 //
10468 // Source: <?php $y = "brace ${z} end"; $s = ${$a . 'b'};
10469 //
10470 // `"${z}"` is a `dynamic_variable_name` whose `name` child is
10471 // suppressed (the wrapper `${z}` carries the reference), while
10472 // `${$a . 'b'}` reaches its `$a` through a `binary_expression`,
10473 // so that `variable_name`'s parent is not a
10474 // `dynamic_variable_name` and it counts normally.
10475 //
10476 // Operands: `$y`, `${z}`, `$s`, `${$a . 'b'}`, `$a`, `'b'` — one
10477 // each ⇒ n2 = 6, N2 = 6. A guard written as a blanket kind
10478 // exclusion instead of a parent check would drop `$a` ⇒ 5 / 5.
10479 check_metrics::<PhpParser>(
10480 "<?php $y = \"brace ${z} end\"; $s = ${$a . 'b'};",
10481 "foo.php",
10482 |metric| {
10483 assert_eq!(metric.halstead.unique_operands(), 6);
10484 assert_eq!(metric.halstead.total_operands(), 6);
10485 },
10486 );
10487 }
10488
10489 #[cfg(feature = "php")]
10490 #[test]
10491 fn php_type_wrappers_count_the_type_once() {
10492 // Regression: issue #1293. A parameter type nests wrapper nodes
10493 // whose text spans the node below them — `primitive_type` around
10494 // the `int` keyword token, `named_type` around a `name`,
10495 // `optional_type` around either — and every level was in the
10496 // operand arm, so `int` scored 2 and `?int` scored 3.
10497 //
10498 // Source: the issue's first reproducer.
10499 // <?php
10500 // function f(int $a, bool $b, float $c, string $d, array $e,
10501 // Foo $g): ?int { return 0; }
10502 //
10503 // Operands by text key: `f`, the five `primitive_type` parameter
10504 // types, `Foo` (the `name` under its `named_type`), `$a`..`$g`,
10505 // the return `int`, and `0`. `int` occurs twice (parameter and
10506 // return) ⇒ n2 = 14, N2 = 15. Before the fix: 15 / 23 — the
10507 // extra vocabulary entry being `?int`, which the `?` operator
10508 // already accounts for.
10509 check_metrics::<PhpParser>(
10510 "<?php\nfunction f(int $a, bool $b, float $c, string $d, \
10511 array $e, Foo $g): ?int { return 0; }\n",
10512 "foo.php",
10513 |metric| {
10514 assert_eq!(metric.halstead.unique_operands(), 14);
10515 assert_eq!(metric.halstead.total_operands(), 15);
10516 },
10517 );
10518 }
10519
10520 #[cfg(feature = "php")]
10521 #[test]
10522 fn php_qualified_name_counts_its_components_once() {
10523 // Regression: issue #1293. `Foo\Bar\Baz` parses as
10524 // `qualified_name` → `namespace_name` → `name` × N, and all
10525 // three kinds were operands, so one three-part path scored 5 and
10526 // planted `Foo`, `Foo\Bar` and `Foo\Bar\Baz` in the vocabulary.
10527 // The components carry the operand and `\` stays an operator,
10528 // matching how PHP's own `::` and `->` already read here.
10529 //
10530 // Source: the issue's second reproducer.
10531 // <?php
10532 // namespace App\Sub;
10533 // use Foo\Bar\Baz;
10534 // $o = new \Vendor\Pkg\Thing();
10535 //
10536 // Operands: `App`, `Sub`, `Foo`, `Bar`, `Baz`, `$o`, `Vendor`,
10537 // `Pkg`, `Thing` — one each ⇒ n2 = 9, N2 = 9. Before the fix:
10538 // 14 / 14.
10539 check_metrics::<PhpParser>(
10540 "<?php\nnamespace App\\Sub;\nuse Foo\\Bar\\Baz;\n\
10541 $o = new \\Vendor\\Pkg\\Thing();\n",
10542 "foo.php",
10543 |metric| {
10544 assert_eq!(metric.halstead.unique_operands(), 9);
10545 assert_eq!(metric.halstead.total_operands(), 9);
10546 },
10547 );
10548 }
10549
10550 #[cfg(feature = "php")]
10551 #[test]
10552 fn php_nested_type_wrappers_count_once_at_any_depth() {
10553 // Companion to the two tests above (#1293): the type and
10554 // qualified-name wrappers compose, so a single annotation can
10555 // stack five levels — `?A\B` is `optional_type` → `named_type` →
10556 // `qualified_name` → `namespace_name` → `name`, which scored 6
10557 // operands for two identifiers. `union_type` and
10558 // `intersection_type` stack the same way over their members;
10559 // their `|` and `&` are already operators.
10560 //
10561 // Source:
10562 // <?php function k(?A\B $p, int|string $q, C&D $r): ?A\B
10563 // { return 0; }
10564 // The return type repeats `?A\B` so N2 exceeds n2 and the
10565 // assertions can tell "counted once per occurrence" from
10566 // "deduplicated into the vocabulary".
10567 //
10568 // Operands: `k`, `A` × 2, `B` × 2, `$p`, `int`, `string`, `$q`,
10569 // `C`, `D`, `$r`, `0` ⇒ n2 = 11, N2 = 13.
10570 check_metrics::<PhpParser>(
10571 "<?php function k(?A\\B $p, int|string $q, C&D $r): ?A\\B { return 0; }",
10572 "foo.php",
10573 |metric| {
10574 assert_eq!(metric.halstead.unique_operands(), 11);
10575 assert_eq!(metric.halstead.total_operands(), 13);
10576 },
10577 );
10578 }
10579
10580 #[cfg(feature = "php")]
10581 #[test]
10582 fn php_childless_primitive_types_still_count() {
10583 // Guards the direction of the #1293 fix for `primitive_type`,
10584 // where — unlike the qualified-name wrappers — the *wrapper*
10585 // carries the operand and the keyword leaf is suppressed. The
10586 // grammar emits no token node under `primitive_type` for
10587 // `callable`, `iterable`, `mixed`, `void`, `false` or `true`
10588 // (verified with `bca dump`), so the other direction would score
10589 // those six types zero — grammar-dispatch §6.
10590 //
10591 // Source:
10592 // <?php function q(callable $a, iterable $b, mixed $c,
10593 // false $d, true $e): void { }
10594 //
10595 // Operands: `q`, `callable`, `iterable`, `mixed`, `false`,
10596 // `true`, `void`, `$a`..`$e` ⇒ n2 = 12, N2 = 12. Dropping
10597 // `PrimitiveType` from the operand arm instead of gating its
10598 // leaf gives 6 / 6.
10599 check_metrics::<PhpParser>(
10600 "<?php function q(callable $a, iterable $b, mixed $c, \
10601 false $d, true $e): void { }",
10602 "foo.php",
10603 |metric| {
10604 assert_eq!(metric.halstead.unique_operands(), 12);
10605 assert_eq!(metric.halstead.total_operands(), 12);
10606 },
10607 );
10608 }
10609
10610 #[cfg(feature = "php")]
10611 #[test]
10612 fn php_primitive_type_keyword_guard_is_parent_scoped() {
10613 // Companion to the test above (#1293): the keyword suppression
10614 // fires on the *parent* kind, never on the kind alone.
10615 // `array` is also the head token of an `array(…)` literal, where
10616 // it is the construct's only operand and must keep counting; a
10617 // `(int)` / `(string)` cast is a childless `cast_type` that
10618 // never reaches the guard at all.
10619 //
10620 // Source: <?php $x = array(1, 2); $y = (int) $x; $z = (string) $x;
10621 //
10622 // Operands: `$x` × 3, `array`, `1`, `2`, `$y`, `int`, `$z`,
10623 // `string` ⇒ n2 = 8, N2 = 10. A blanket kind exclusion instead
10624 // of a parent check would drop the `array` head ⇒ 7 / 9.
10625 check_metrics::<PhpParser>(
10626 "<?php $x = array(1, 2); $y = (int) $x; $z = (string) $x;",
10627 "foo.php",
10628 |metric| {
10629 assert_eq!(metric.halstead.unique_operands(), 8);
10630 assert_eq!(metric.halstead.total_operands(), 10);
10631 },
10632 );
10633 }
10634
10635 #[cfg(feature = "php")]
10636 #[test]
10637 fn php_encapsed_string_interpolation_no_double_count() {
10638 // Regression: issue #184. A PHP `"Hello $name!"` used to be
10639 // classified as a Halstead operand (the wrapping
10640 // `encapsed_string`) AND have its inner `variable_name`
10641 // (`$name`) plus the inner `name` token classified as
10642 // operands too. With the fix, the wrapping literal drops to
10643 // `Unknown` when it carries any `$var` / `${name}` / `{$expr}`
10644 // child, so `$name` is counted exactly once at each text
10645 // occurrence.
10646 //
10647 // Source:
10648 // <?php $name = "world"; echo "Hello $name!";
10649 //
10650 // Inert operand: `"world"` (no interpolation, still operand).
10651 // Operands by text key (`get_id` keys by source bytes):
10652 // `$name` × 2 (assignment LHS and `$name` inside the
10653 // interpolated string), `"world"` × 1.
10654 // u_operands = 2, N2 = 3.
10655 // Without the #184 fix the wrapping `"Hello $name!"` would also
10656 // count → 3 / 4. This test additionally pinned the *inner* `name`
10657 // leaf of each `variable_name` (a further 2 occurrences, 1 unique
10658 // ⇒ the historical 3 / 5) until #1259 recognised that as the same
10659 // double count one level down.
10660 check_metrics::<PhpParser>(
10661 "<?php $name = \"world\"; echo \"Hello $name!\";",
10662 "foo.php",
10663 |metric| {
10664 assert_eq!(metric.halstead.unique_operands(), 2);
10665 assert_eq!(metric.halstead.total_operands(), 3);
10666 },
10667 );
10668 }
10669
10670 #[cfg(feature = "php")]
10671 #[test]
10672 fn php_encapsed_string_no_interpolation_still_operand() {
10673 // The fix for #184 only drops `EncapsedString`/`Heredoc` from
10674 // the operand arm when interpolation is present. An inert
10675 // double-quoted string must still count as exactly one
10676 // operand, identical to the single-quoted equivalent.
10677 //
10678 // Source: `<?php echo "Hello world!";`
10679 // Operands: `"Hello world!"` × 1 → u_operands = 1, N2 = 1.
10680 check_metrics::<PhpParser>("<?php echo \"Hello world!\";", "foo.php", |metric| {
10681 assert_eq!(metric.halstead.unique_operands(), 1);
10682 assert_eq!(metric.halstead.total_operands(), 1);
10683 });
10684 }
10685
10686 #[cfg(feature = "php")]
10687 #[test]
10688 fn php_heredoc_interpolation_no_double_count() {
10689 // Regression: issue #184. A PHP heredoc whose body
10690 // interpolates `$name` previously counted both the wrapping
10691 // `heredoc` node and the inner `$name` as operands; the fix
10692 // drops the wrapper when its `heredoc_body` carries any
10693 // interpolation child.
10694 //
10695 // Source:
10696 // <?php $name = "x"; echo <<<EOT
10697 // hi $name
10698 // EOT;
10699 //
10700 // Operands by text key: `$name` × 2, `"x"` × 1 (inert encapsed
10701 // string, still an operand). With the fix u_operands = 2,
10702 // N2 = 3. Without it the wrapping heredoc text would add one
10703 // more unique operand. The sigil-less `name` leaf inside each
10704 // `variable_name` was counted too until #1259.
10705 check_metrics::<PhpParser>(
10706 "<?php $name = \"x\"; echo <<<EOT\nhi $name\nEOT;\n",
10707 "foo.php",
10708 |metric| {
10709 assert_eq!(metric.halstead.unique_operands(), 2);
10710 assert_eq!(metric.halstead.total_operands(), 3);
10711 },
10712 );
10713 }
10714
10715 #[cfg(feature = "php")]
10716 #[test]
10717 fn php_nowdoc_unaffected() {
10718 // `Nowdoc` (single-quoted heredoc) never interpolates and is
10719 // never matched by `php_string_has_interpolation`. It must
10720 // continue counting as exactly one operand regardless of the
10721 // text inside, mirroring single-quoted `String`.
10722 //
10723 // Source:
10724 // <?php echo <<<'EOT'
10725 // plain $name not interpolated
10726 // EOT;
10727 //
10728 // Operands: the nowdoc literal × 1 → u_operands = 1, N2 = 1.
10729 check_metrics::<PhpParser>(
10730 "<?php echo <<<'EOT'\nplain $name not interpolated\nEOT;\n",
10731 "foo.php",
10732 |metric| {
10733 assert_eq!(metric.halstead.unique_operands(), 1);
10734 assert_eq!(metric.halstead.total_operands(), 1);
10735 },
10736 );
10737 }
10738
10739 #[cfg(feature = "php")]
10740 #[test]
10741 fn php_encapsed_string_bare_member_access_no_double_count() {
10742 // Regression: issue #184 follow-up. The PHP grammar allows
10743 // bare `$obj->prop` interpolation inside `"…"` without
10744 // surrounding `{ … }`; tree-sitter-php emits this as a
10745 // direct `member_access_expression` child of
10746 // `encapsed_string` (kind_id 329 in the current grammar).
10747 // The wrapper must drop to `Unknown` for that form too —
10748 // otherwise the inner `$obj` and `prop` `name` tokens are
10749 // walked as operands while the wrapper also counts,
10750 // double-counting `N2`.
10751 //
10752 // Source:
10753 // <?php $obj = new stdClass; $obj->prop = "x"; echo "Hi $obj->prop!";
10754 //
10755 // Operands tallied by `get_id` (keyed on source bytes):
10756 // `$obj` × 3 (LHS assignment, member-access target,
10757 // inside the interpolated string)
10758 // `prop` (name) × 2 (member-access RHS twice — a bare `name`
10759 // outside any `variable_name`, so #1259's
10760 // guard leaves it an operand)
10761 // `stdClass` × 1
10762 // `"x"` × 1
10763 // ⇒ u_operands = 4, N2 = 7.
10764 // With the bug the wrapping `"Hi $obj->prop!"` text adds one
10765 // more unique operand and one more occurrence ⇒ 5 / 8.
10766 check_metrics::<PhpParser>(
10767 "<?php $obj = new stdClass; $obj->prop = \"x\"; echo \"Hi $obj->prop!\";",
10768 "foo.php",
10769 |metric| {
10770 assert_eq!(metric.halstead.unique_operands(), 4);
10771 assert_eq!(metric.halstead.total_operands(), 7);
10772 },
10773 );
10774 }
10775
10776 #[cfg(feature = "php")]
10777 #[test]
10778 fn php_encapsed_string_bare_subscript_no_double_count() {
10779 // Regression: issue #184 follow-up. Bare `$arr[0]` inside
10780 // `"…"` produces a `subscript_expression` child of
10781 // `encapsed_string` (kind_id 351). The wrapper must drop to
10782 // `Unknown` for that form.
10783 //
10784 // Source:
10785 // <?php $arr = [1]; echo "Hi $arr[0]!";
10786 //
10787 // Operands tallied by `get_id`:
10788 // `$arr` × 2, `1` × 1, `0` × 1.
10789 // ⇒ u_operands = 3, N2 = 4.
10790 // With the bug the wrapping `"Hi $arr[0]!"` text adds 1 / 1.
10791 // The inner `arr` leaf of each `variable_name` added a further
10792 // 1 / 2 until #1259.
10793 check_metrics::<PhpParser>(
10794 "<?php $arr = [1]; echo \"Hi $arr[0]!\";",
10795 "foo.php",
10796 |metric| {
10797 assert_eq!(metric.halstead.unique_operands(), 3);
10798 assert_eq!(metric.halstead.total_operands(), 4);
10799 },
10800 );
10801 }
10802
10803 #[cfg(feature = "php")]
10804 #[test]
10805 fn php_shell_command_expression_inert_is_operand() {
10806 // Regression: issue #288. Backtick command literals (PHP's
10807 // `shell_command_expression`) were filtered as strings by
10808 // `Checker::is_string` and `Alterator::alterate`, but never
10809 // classified as Halstead operands — so they contributed
10810 // nothing to N2 / eta2. An inert backtick literal must now
10811 // count as exactly one operand, matching `EncapsedString`
10812 // and `Heredoc`.
10813 //
10814 // Source: `<?php $out = ` + backtick `ls` + backtick + `;`
10815 // Operands tallied by `get_id`:
10816 // `$out` × 1, backtick literal × 1.
10817 // ⇒ u_operands = 2, N2 = 2.
10818 // Before the fix the backtick literal vanished from the count
10819 // ⇒ u_operands = 1, N2 = 1. (The inner `out` leaf of the
10820 // `variable_name` added another 1 / 1 until #1259.)
10821 check_metrics::<PhpParser>("<?php $out = `ls`;", "foo.php", |metric| {
10822 assert_eq!(metric.halstead.unique_operands(), 2);
10823 assert_eq!(metric.halstead.total_operands(), 2);
10824 });
10825 }
10826
10827 #[cfg(feature = "php")]
10828 #[test]
10829 fn php_shell_command_expression_interpolation_no_double_count() {
10830 // Regression: issue #288. PHP backtick literals DO support
10831 // `$var` interpolation (see tree-sitter-php node-types.json:
10832 // `shell_command_expression` children include `variable_name`,
10833 // `dynamic_variable_name`, `member_access_expression`,
10834 // `subscript_expression`). With the fix the wrapper drops to
10835 // `Unknown` when it carries any interpolation child, exactly
10836 // as `EncapsedString` does.
10837 //
10838 // Source: `<?php $dir = "/tmp"; $out = ` + backtick `ls $dir` +
10839 // backtick + `;`
10840 //
10841 // Operands tallied by `get_id`:
10842 // `$dir` × 2 (assignment LHS, inside backticks),
10843 // `$out` × 1, `"/tmp"` × 1.
10844 // ⇒ u_operands = 3, N2 = 4.
10845 // Without the interpolation guard the wrapping backtick literal
10846 // would also count ⇒ u_operands = 4, N2 = 5. The sigil-less
10847 // `dir` / `out` leaves added a further 2 / 3 until #1259.
10848 check_metrics::<PhpParser>(
10849 "<?php $dir = \"/tmp\"; $out = `ls $dir`;",
10850 "foo.php",
10851 |metric| {
10852 assert_eq!(metric.halstead.unique_operands(), 3);
10853 assert_eq!(metric.halstead.total_operands(), 4);
10854 },
10855 );
10856 }
10857
10858 #[cfg(feature = "php")]
10859 #[test]
10860 fn php_interpolation_opener_is_not_an_operator() {
10861 // Regression: issue #1314. `Php::LBRACE` is *both* the
10862 // compound-statement brace and the complex-interpolation
10863 // opener, so `"dq {$y} end"` reported a `{}` operator — and
10864 // reported it against the same vocabulary entry a real block
10865 // uses, which no other language does.
10866 //
10867 // expected: operators `=` × 2, `;` × 2 → n1 = 2, N1 = 4.
10868 // Operands `$s`, `$t`, `$y` × 2 → n2 = 3, N2 = 4. Before the
10869 // guard the two openers added `{}` → n1 = 3, N1 = 6.
10870 check_metrics::<PhpParser>(
10871 "<?php\n$s = \"dq {$y} end\";\n$t = \"dq {$y} end\";\n",
10872 "foo.php",
10873 |metric| {
10874 assert_eq!(metric.halstead.unique_operators(), 2);
10875 assert_eq!(metric.halstead.total_operators(), 4);
10876 assert_eq!(metric.halstead.unique_operands(), 3);
10877 assert_eq!(metric.halstead.total_operands(), 4);
10878 },
10879 );
10880 }
10881
10882 #[cfg(feature = "php")]
10883 #[test]
10884 fn php_interpolation_opener_guard_covers_every_wrapper() {
10885 // The opener is a direct child of four distinct parents, and
10886 // each is an independent leg of the guard (grammar-dispatch
10887 // section 11) — a fixture covering only `encapsed_string`
10888 // leaves the other three dead.
10889 //
10890 // One row per parent, so a failure names the leg that broke.
10891 // The heredoc's brace hangs off `heredoc_body` rather than
10892 // `heredoc`; the backtick form is `shell_command_expression`;
10893 // and the bare `${$y}` variable-variable is a
10894 // `dynamic_variable_name`, the one position that is not inside
10895 // a string at all.
10896 //
10897 // expected per row: operators `=` × 2, `;` × 2 → n1 = 2,
10898 // N1 = 4; three distinct operands with one repeated → n2 = 3,
10899 // N2 = 4.
10900 for (label, source) in [
10901 (
10902 "encapsed_string",
10903 "<?php\n$s = \"dq {$y} end\";\n$t = \"dq {$y} end\";\n",
10904 ),
10905 (
10906 "heredoc_body",
10907 "<?php\n$h = <<<EOT\na {$y} b\nEOT;\n$i = <<<EOT\na {$y} b\nEOT;\n",
10908 ),
10909 (
10910 "shell_command_expression",
10911 "<?php\n$b = `ls {$y}`;\n$c = `ls {$y}`;\n",
10912 ),
10913 ("dynamic_variable_name", "<?php\n$q = ${$y};\n$r = ${$y};\n"),
10914 ] {
10915 assert_halstead_counts::<PhpParser>(source, "foo.php", [2, 4, 3, 4], label);
10916 }
10917 }
10918
10919 #[cfg(feature = "php")]
10920 #[test]
10921 fn php_every_interpolation_spelling_scores_alike() {
10922 // The policy stated as a test (#1314). PHP writes one
10923 // interpolation three ways; the choice is spelling, so all
10924 // three must score identically. Before the guard the two
10925 // braced forms reported a `{}` the bare `$y` form did not.
10926 //
10927 // `"${y}"` is deprecated as of PHP 8.2 and removed in 9.0, but
10928 // the pinned grammar still parses it and it is still in the
10929 // wild, so it stays a row here.
10930 //
10931 // The fixture deliberately omits a `$y = …` declaration: with
10932 // one, the bare and `{$y}` forms key their operand as `$y` and
10933 // collapse into the declaration's entry while `${y}` keys as
10934 // `${y}` and does not, so n2 would differ for a reason that has
10935 // nothing to do with this guard.
10936 //
10937 // expected per spelling: operators `=` × 2, `;` × 2 → n1 = 2,
10938 // N1 = 4; operands `$s`, `$t`, the interpolated reference × 2
10939 // → n2 = 3, N2 = 4.
10940 for literal in ["\"a $y b\"", "\"a {$y} b\"", "\"a ${y} b\""] {
10941 assert_halstead_counts::<PhpParser>(
10942 &format!("<?php\n$s = {literal};\n$t = {literal};\n"),
10943 "foo.php",
10944 [2, 4, 3, 4],
10945 &format!("interpolation {literal}"),
10946 );
10947 }
10948 }
10949
10950 #[cfg(feature = "php")]
10951 #[test]
10952 fn php_compound_statement_brace_still_counts() {
10953 // Control for #1314: the guard is scoped to the four
10954 // interpolating wrappers, so a real block keeps its `{}`. A
10955 // guard widened to every `LBRACE` would take `{}` out of the
10956 // operator set entirely and fail here.
10957 //
10958 // expected: operators `function`, `()`, `{}` × 2, `if`,
10959 // `return`, `;` → n1 = 6, N1 = 8. Operands `f`, `1`, `2` →
10960 // n2 = N2 = 3.
10961 check_metrics::<PhpParser>(
10962 "<?php\nfunction f() { if (1) { return 2; } }\n",
10963 "foo.php",
10964 |metric| {
10965 assert_eq!(metric.halstead.unique_operators(), 6);
10966 assert_eq!(metric.halstead.total_operators(), 8);
10967 assert_eq!(metric.halstead.unique_operands(), 3);
10968 assert_eq!(metric.halstead.total_operands(), 3);
10969 },
10970 );
10971 }
10972
10973 #[cfg(feature = "php")]
10974 #[test]
10975 fn php_interpolation_guard_is_parent_scoped_not_ancestor_scoped() {
10976 // The input that separates the parent-scoped guard from the
10977 // ancestor-scanning mutant — the mutant #1256's post-mortem
10978 // says survives every ordinary fixture. PHP is one of only two
10979 // languages in #1314 where such an input exists: a closure
10980 // inside a complex interpolation puts a *compound-statement*
10981 // brace under an `encapsed_string` ancestor while its parent is
10982 // the `compound_statement`. An ancestor scan swallows it.
10983 //
10984 // expected: operators `=`, `;` × 2, `->`, `()` × 2, `function`,
10985 // `{}`, `return` → n1 = 7, N1 = 9. Operands `$s`, `$o`, `m`,
10986 // `1` → n2 = N2 = 4. Under the ancestor-scoped mutant the
10987 // closure's brace vanishes: n1 = 6, N1 = 8.
10988 check_metrics::<PhpParser>(
10989 "<?php\n$s = \"{$o->m(function() { return 1; })}\";\n",
10990 "foo.php",
10991 |metric| {
10992 assert_eq!(metric.halstead.unique_operators(), 7);
10993 assert_eq!(metric.halstead.total_operators(), 9);
10994 assert_eq!(metric.halstead.unique_operands(), 4);
10995 assert_eq!(metric.halstead.total_operands(), 4);
10996 },
10997 );
10998 }
10999
11000 #[cfg(feature = "php")]
11001 #[test]
11002 fn php_promoted_asymmetric_visibility_scores_like_a_property() {
11003 // tree-sitter-php 0.24.2 parsed `private(set)` on a promoted
11004 // parameter as ERROR and billed `private` and `set` as operands.
11005 // 0.25.1 parses it as the `visibility_modifier` a property
11006 // declaration already gets, so it now scores the same way.
11007 //
11008 // expected: operators `class`, `{}` × 2, `public` × 2, `function`,
11009 // `()` × 2 (parameters, `(set)`), `private` → n1 = 6, N1 = 9.
11010 // Operands `A`, `__construct`, `int`, `$x` → n2 = N2 = 4.
11011 check_metrics::<PhpParser>(
11012 "<?php\nclass A {\n public function __construct(public private(set) int $x) {}\n}\n",
11013 "foo.php",
11014 |metric| {
11015 assert_eq!(metric.halstead.unique_operators(), 6);
11016 assert_eq!(metric.halstead.total_operators(), 9);
11017 assert_eq!(metric.halstead.unique_operands(), 4);
11018 assert_eq!(metric.halstead.total_operands(), 4);
11019 },
11020 );
11021 }
11022
11023 #[cfg(feature = "elixir")]
11024 #[test]
11025 fn elixir_operators_and_operands() {
11026 // Exercises every Halstead family classified in Elixir's
11027 // `get_op_type`: control-flow keywords (`do`, `end`, `fn`),
11028 // structural punctuation — only the *opening* delimiters `(`,
11029 // `[` count after #695 (the `)`/`]` closers were dropped), plus
11030 // `,`, `.`, `@`,
11031 // arithmetic (`+`, `-`, `*`, `/`), comparison (`==`, `>`),
11032 // logical (`&&`, `||`, `and`, `or`, `!`), pipe (`|>`), capture
11033 // (`&`), assignment/match (`=`), and the stab arrow (`->`).
11034 // The body mixes identifiers, integers, atoms, and a string.
11035 check_metrics::<ElixirParser>(
11036 "defmodule Foo do\n @doc \"add\"\n def calc(a, b) do\n result = a + b * 2\n flag = result > 0 && a == b\n out = if flag, do: result, else: -result\n [out, a, b]\n end\nend\n",
11037 "foo.ex",
11038 |metric| {
11039 // Positive headline assertions on integer counts. After
11040 // #695 only opening delimiters count: the `)`/`]` closers
11041 // no longer add operators (was 15 unique / 23 total).
11042 assert_eq!(metric.halstead.unique_operators(), 13);
11043 assert_eq!(metric.halstead.total_operators(), 21);
11044 assert_eq!(metric.halstead.unique_operands(), 16);
11045 assert_eq!(metric.halstead.total_operands(), 27);
11046 insta::assert_json_snapshot!(
11047 metric.halstead,
11048 @r#"
11049 {
11050 "unique_operators": 13,
11051 "total_operators": 21,
11052 "unique_operands": 16,
11053 "total_operands": 27,
11054 "length": 48,
11055 "estimated_program_length": 112.10571633583419,
11056 "purity_ratio": 2.3355357569965456,
11057 "vocabulary": 29,
11058 "volume": 233.18308776612344,
11059 "difficulty": 10.96875,
11060 "level": 0.09116809116809117,
11061 "effort": 2557.7269939346666,
11062 "time": 142.09594410748147,
11063 "bugs": 0.062342115670886794
11064 }
11065 "#
11066 );
11067 },
11068 );
11069 }
11070
11071 #[cfg(feature = "ruby")]
11072 #[test]
11073 fn ruby_operators_and_operands() {
11074 // A small Ruby method exercising operators (def/if/end keyword
11075 // tokens, `+`, `==`, `<=`, structural punctuation) and operands
11076 // (`n`, `1`, `factorial`). Anchors the unique/total counts on
11077 // both sides and snapshots the full Halstead derivation.
11078 //
11079 // Lesson 4 invariants: u_operators / u_operands here equal the
11080 // dedupe lengths the `--ops` accessor would emit on the same
11081 // source. Any future grammar bump that adds an aliased kind_id
11082 // to either side will trip this without snapshot drift.
11083 check_metrics::<RubyParser>(
11084 "def factorial(n)\n return 1 if n <= 1\n n * factorial(n - 1)\nend\n",
11085 "foo.rb",
11086 |metric| {
11087 // After #695 only the `(` opener counts (folded `()`); the
11088 // `)` closer — which appeared twice across the two calls —
11089 // no longer adds an operator (was 9 unique / 11 total).
11090 assert_eq!(metric.halstead.unique_operators(), 8);
11091 assert_eq!(metric.halstead.total_operators(), 9);
11092 assert_eq!(metric.halstead.unique_operands(), 3);
11093 assert_eq!(metric.halstead.total_operands(), 9);
11094 insta::assert_json_snapshot!(metric.halstead);
11095 },
11096 );
11097 }
11098
11099 #[cfg(feature = "ruby")]
11100 #[test]
11101 fn ruby_halstead_plain_string_operand() {
11102 // A bare string literal contributes exactly one operand. The
11103 // counterpart to `ruby_halstead_interpolated_string_no_double_count`
11104 // — verifies the "no interpolation" branch of the same arm
11105 // (see `get_op_type`'s `R::String | …` case in
11106 // `big-code-analysis-ast/src/getter.rs`).
11107 // expected: operators = {def, end} = 2; operands = {f, "hello"} = 2.
11108 check_metrics::<RubyParser>("def f\n \"hello\"\nend\n", "foo.rb", |metric| {
11109 assert_eq!(metric.halstead.unique_operators(), 2);
11110 assert_eq!(metric.halstead.total_operators(), 2);
11111 assert_eq!(metric.halstead.unique_operands(), 2);
11112 assert_eq!(metric.halstead.total_operands(), 2);
11113 });
11114 }
11115
11116 #[cfg(feature = "ruby")]
11117 #[test]
11118 fn ruby_halstead_interpolated_string_no_double_count() {
11119 // Regression mirror for #180 (Bash) / #183 (C#): when a Ruby
11120 // string literal carries an `Interpolation` child, the
11121 // wrapping `String` node is intentionally classified as
11122 // `Unknown` so the inner expression's identifiers are not
11123 // double-counted as operands.
11124 //
11125 // expected: for `def f(name)\n "Hi #{name}"\nend\n` —
11126 // operators: def, (, ), #{, }, end → u_operators = 6.
11127 // operands: f, name (param), name (inside `#{name}`). The
11128 // wrapping `"…#{name}"` literal is skipped by the
11129 // `is_child(R::Interpolation)` guard; the operand store
11130 // keys by token text so the two `name` occurrences dedupe
11131 // into one distinct entry → u_operands = 2, operands = 3
11132 // (`f` once, `name` twice).
11133 // Without the guard, the wrapping literal would also count,
11134 // inflating u_operands to 3 and operands to 4.
11135 check_metrics::<RubyParser>("def f(name)\n \"Hi #{name}\"\nend\n", "foo.rb", |metric| {
11136 assert_eq!(metric.halstead.unique_operands(), 2);
11137 assert_eq!(metric.halstead.total_operands(), 3);
11138 });
11139 }
11140
11141 #[cfg(feature = "ruby")]
11142 #[test]
11143 fn ruby_halstead_symbol_literal_operand() {
11144 // `:foo` is a `SimpleSymbol` leaf — counts as a single
11145 // operand, no interpolation guard needed (only
11146 // `DelimitedSymbol` (`:"…#{x}…"`) can interpolate).
11147 // expected: operators = {def, end} = 2; operands = {f, :ok} = 2.
11148 check_metrics::<RubyParser>("def f\n :ok\nend\n", "foo.rb", |metric| {
11149 assert_eq!(metric.halstead.unique_operators(), 2);
11150 assert_eq!(metric.halstead.unique_operands(), 2);
11151 });
11152 }
11153
11154 #[cfg(feature = "ruby")]
11155 #[test]
11156 fn ruby_halstead_regex_operand() {
11157 // `/foo/` parses as a `Regex` node — one operand. Its two
11158 // `SLASH` delimiters used to fall through to the shared
11159 // arithmetic arm and add a `/` operator that is nowhere in the
11160 // source; #1312 parent-guards them to `Unknown`.
11161 // expected: u_operators = {def, (, =~, end} = 4, N1 = 4 (only
11162 // the `(` opener counts after #695 — the `)` closer was
11163 // dropped; was 5 with the fabricated `/`); u_operands =
11164 // {f, s, /foo/} = 3, N2 = 4 (`s` twice: parameter and use).
11165 check_metrics::<RubyParser>("def f(s)\n s =~ /foo/\nend\n", "foo.rb", |metric| {
11166 assert_eq!(metric.halstead.unique_operators(), 4);
11167 assert_eq!(metric.halstead.total_operators(), 4);
11168 assert_eq!(metric.halstead.unique_operands(), 3);
11169 assert_eq!(metric.halstead.total_operands(), 4);
11170 });
11171 }
11172
11173 #[cfg(feature = "ruby")]
11174 #[test]
11175 fn ruby_regex_delimiters_are_not_operators() {
11176 // Regression: issue #1312, the Ruby sibling of Elixir #1256.
11177 // Both of a `Regex` literal's delimiter tokens are `SLASH` —
11178 // the same kind id real division uses — so `x = /abc/`
11179 // reported a `/` operator with no division in the source.
11180 //
11181 // expected: operators `=` → n1 = N1 = 1. Operands `x` and the
11182 // `/abc/` literal → n2 = N2 = 2. Before the guard the two
11183 // delimiters added `/` → n1 = 2, N1 = 3.
11184 check_metrics::<RubyParser>("x = /abc/\n", "foo.rb", |metric| {
11185 assert_eq!(metric.halstead.unique_operators(), 1);
11186 assert_eq!(metric.halstead.total_operators(), 1);
11187 assert_eq!(metric.halstead.unique_operands(), 2);
11188 assert_eq!(metric.halstead.total_operands(), 2);
11189 });
11190 }
11191
11192 #[cfg(feature = "ruby")]
11193 #[test]
11194 fn ruby_regex_delimiter_choice_is_invariant() {
11195 // Companion to the test above (#1312): `%r`-form regexes are
11196 // the same literal spelled differently, so every delimiter
11197 // choice must produce identical counts. tree-sitter-ruby
11198 // aliases all of them to `SLASH` — verified with `bca dump`,
11199 // which shows `%r{`/`}`, `%r(`/`)`, `%r[`/`]`, `%r<`/`>`,
11200 // `%r|`/`|` and `%r!`/`!` every one emitting kind `SLASH` —
11201 // so each row here genuinely exercises the guard rather than
11202 // reaching a different, already-clean path.
11203 //
11204 // expected per variant: operator `=` → n1 = N1 = 1; operands
11205 // `x` and the literal → n2 = N2 = 2.
11206 for literal in [
11207 "/abc/", "%r{abc}", "%r(abc)", "%r[abc]", "%r<abc>", "%r|abc|", "%r!abc!",
11208 ] {
11209 assert_halstead_counts::<RubyParser>(
11210 &format!("x = {literal}\n"),
11211 "foo.rb",
11212 [1, 1, 2, 2],
11213 &format!("regex literal {literal}"),
11214 );
11215 }
11216 }
11217
11218 #[cfg(feature = "ruby")]
11219 #[test]
11220 fn ruby_division_survives_the_regex_guard() {
11221 // Control for #1312: the guard is scoped to a `Regex` parent,
11222 // so real division still counts. Two divisions, so a mutant
11223 // that collapsed repeated hits would move `N1` even though
11224 // `n1` held (the #1294 count-only-anchor lesson).
11225 //
11226 // expected: operators `=` and `/` × 2 → n1 = 2, N1 = 3.
11227 // Operands `z`, `a`, `b`, `c` → n2 = N2 = 4.
11228 check_metrics::<RubyParser>("z = a / b / c\n", "foo.rb", |metric| {
11229 assert_eq!(metric.halstead.unique_operators(), 2);
11230 assert_eq!(metric.halstead.total_operators(), 3);
11231 assert_eq!(metric.halstead.unique_operands(), 4);
11232 assert_eq!(metric.halstead.total_operands(), 4);
11233 });
11234 }
11235
11236 #[cfg(feature = "ruby")]
11237 #[test]
11238 fn ruby_regex_guard_is_parent_scoped_not_ancestor_scoped() {
11239 // The one input that separates the correct parent-scoped guard
11240 // from the ancestor-scoped mutant of it (#1312, mirroring
11241 // #1256's Elixir case): a division *inside* a regex's `#{…}`
11242 // interpolation. Its `/` has `Binary` as its parent but the
11243 // `Regex` as a further ancestor, so an ancestor scan would
11244 // swallow it. Every other fixture in this file passes under
11245 // both spellings. Two interpolations, so the mutant moves both
11246 // n1 (2 → 1) and N1 (3 → 1).
11247 //
11248 // expected: operators `=`, `/` × 2 → n1 = 2, N1 = 3. Operands
11249 // `w`, `p`, `q`, `r`, `t` → n2 = N2 = 5; the wrapping `Regex`
11250 // is skipped because it carries an `Interpolation` child (the
11251 // #180 double-count guard).
11252 //
11253 // Was n1 = 3, N1 = 5 until #1314 dropped `#{` from the operator
11254 // arm. The mutant still moves both axes, so this fixture is as
11255 // discriminating as it was — it just no longer counts the two
11256 // interpolation openers alongside the two divisions.
11257 check_metrics::<RubyParser>("w = /a#{p / q}c#{r / t}b/\n", "foo.rb", |metric| {
11258 assert_eq!(metric.halstead.unique_operators(), 2);
11259 assert_eq!(metric.halstead.total_operators(), 3);
11260 assert_eq!(metric.halstead.unique_operands(), 5);
11261 assert_eq!(metric.halstead.total_operands(), 5);
11262 });
11263 }
11264
11265 #[cfg(feature = "ruby")]
11266 #[test]
11267 fn ruby_regex_start_alias_never_reaches_kind_id() {
11268 // Drift marker for the `R::SLASH2` half of #1312's guard.
11269 // `SLASH2` is the aliased regex-start token: it sits in the
11270 // enum beside the other literal-start aliases (`DQUOTE`,
11271 // `COLONDQUOTE`, `BQUOTE2`, `PERCENTwLPAREN`) and the runtime
11272 // `public_symbol_map` collapses it to `SLASH` before
11273 // `kind_id()`, exactly like `LPAREN2` in #768. It is listed in
11274 // the guard rather than the arithmetic arm because a regex
11275 // delimiter is the only thing it could ever be; this pins that
11276 // it is currently unreachable, so a grammar bump that starts
11277 // emitting it fails here instead of silently changing a metric.
11278 let path = PathBuf::from("foo.rb");
11279 for source in ["x = /abc/\n", "x = %r{abc}\n"] {
11280 let parser = RubyParser::new(source.as_bytes().to_vec(), &path, None);
11281 assert!(
11282 !ast_has_kind_id(&parser, Ruby::SLASH2 as u16),
11283 "Ruby::SLASH2 must stay collapsed to Ruby::SLASH for `{source}`"
11284 );
11285 // Positive control: the id the guard actually fires on is
11286 // present, so the assertion above cannot pass merely
11287 // because no delimiter was parsed at all.
11288 assert!(
11289 ast_has_kind_id(&parser, Ruby::SLASH as u16),
11290 "Ruby::SLASH must be the delimiter kind for `{source}`"
11291 );
11292 }
11293 }
11294
11295 #[cfg(feature = "ruby")]
11296 #[test]
11297 fn ruby_subshell_delimiters_are_not_operators() {
11298 // Regression: issue #1360, the second delimiter family of the
11299 // fabrication #1312 removed for regexes. tree-sitter-ruby
11300 // aliases both ends of a subshell literal to `BQUOTE` — the
11301 // same kind id the `` def ` `` method-name marker uses — so
11302 // `` x = `echo hi` `` reported a `` ` `` operator with no
11303 // backtick method call in the source.
11304 //
11305 // expected: operator `=` → n1 = N1 = 1. Operands `x` and the
11306 // `` `echo hi` `` literal → n2 = N2 = 2. Before the guard the
11307 // two delimiters added `` ` `` → n1 = 2, N1 = 3.
11308 assert_halstead_counts::<RubyParser>(
11309 "x = `echo hi`\n",
11310 "foo.rb",
11311 [1, 1, 2, 2],
11312 "backtick subshell",
11313 );
11314 }
11315
11316 #[cfg(feature = "ruby")]
11317 #[test]
11318 fn ruby_subshell_delimiter_choice_is_invariant() {
11319 // Companion to the test above (#1360): `%x`-form subshells are
11320 // the same literal spelled differently, so every delimiter
11321 // choice must produce identical counts. tree-sitter-ruby
11322 // aliases all of them to `BQUOTE` — verified with `bca dump`,
11323 // which shows `` ` ``/`` ` ``, `%x{`/`}`, `%x(`/`)`, `%x[`/`]`,
11324 // `%x<`/`>`, `%x|`/`|` and `%x!`/`!` every one emitting kind
11325 // `BQUOTE` — so each row here genuinely exercises the guard
11326 // rather than reaching a different, already-clean path. Note
11327 // the `%x!…!` and `%x|…|` closers in particular: they are
11328 // `BQUOTE`, not the `BANG` / `PIPE` operators they look like.
11329 //
11330 // The empty row records that `` `` `` parses to a `Subshell`
11331 // holding nothing but its two delimiters, and still bills that
11332 // wrapper as one operand once both are suppressed. It is
11333 // documentation, not a discriminator: the guard deletes nothing
11334 // from the operand arm, so grammar-dispatch §6's
11335 // childless-variant-regresses-to-zero hazard cannot arise here
11336 // and this row moves with the first row under every mutant.
11337 //
11338 // expected per variant: operator `=` → n1 = N1 = 1; operands
11339 // `x` and the literal → n2 = N2 = 2.
11340 for literal in [
11341 "`echo`", "%x{echo}", "%x(echo)", "%x[echo]", "%x<echo>", "%x|echo|", "%x!echo!", "``",
11342 ] {
11343 assert_halstead_counts::<RubyParser>(
11344 &format!("x = {literal}\n"),
11345 "foo.rb",
11346 [1, 1, 2, 2],
11347 &format!("subshell literal {literal}"),
11348 );
11349 }
11350 }
11351
11352 #[cfg(feature = "ruby")]
11353 #[test]
11354 fn ruby_backtick_method_name_survives_the_subshell_guard() {
11355 // Control for #1360, and the reason the kind is gated rather
11356 // than dropped from the operator arm (grammar-dispatch §6):
11357 // `` ` `` is a legal Ruby method name. `bca dump` at the pinned
11358 // grammar shows `` def `(cmd) `` emitting a `BQUOTE` wrapped in
11359 // a named `operator` node, so its parent is `Operator` and the
11360 // parent-scoped guard leaves it alone. Deleting `BQUOTE` from
11361 // the operator arm would have scored this construct zero.
11362 //
11363 // expected: operators `def`, `` ` ``, `(` and `end` →
11364 // n1 = N1 = 4 (only the `(` opener counts after #695).
11365 // Operand `cmd` twice, parameter and use → n2 = 1, N2 = 2.
11366 assert_halstead_counts::<RubyParser>(
11367 "def `(cmd)\n cmd\nend\n",
11368 "foo.rb",
11369 [4, 4, 1, 2],
11370 "backtick method name",
11371 );
11372 }
11373
11374 #[cfg(feature = "ruby")]
11375 #[test]
11376 fn ruby_subshell_guard_is_parent_scoped_not_ancestor_scoped() {
11377 // The one input that separates the correct parent-scoped guard
11378 // from the ancestor-scoped mutant of it (#1360, mirroring
11379 // #1312's regex case): backtick *methods* used inside a
11380 // subshell's `#{…}` interpolation. Their `BQUOTE` has the named
11381 // `operator` node as its parent but the `Subshell` as a further
11382 // ancestor, so an ancestor scan would swallow both. Every other
11383 // fixture in this file passes under either spelling. Two
11384 // markers — a definition and a call — so the mutant moves both
11385 // n1 (7 → 6) and N1 (10 → 8), per the #1294 count-only-anchor
11386 // lesson.
11387 //
11388 // expected: operators `=` (1), `def` (1), `` ` `` (2), `(` (2),
11389 // `;` (2), `end` (1), `.` (1) → n1 = 7, N1 = 10. Operands `n`,
11390 // `c` × 2, `s`, `"x"` → n2 = 4, N2 = 5; the wrapping `Subshell`
11391 // is skipped because it carries `Interpolation` children (the
11392 // #180 double-count guard), and its `a` / `b` string content is
11393 // classified by no arm.
11394 check_metrics::<RubyParser>(
11395 "n = `a#{def `(c); c; end}b#{s.`(\"x\")}`\n",
11396 "foo.rb",
11397 |metric| {
11398 assert_eq!(metric.halstead.unique_operators(), 7);
11399 assert_eq!(metric.halstead.total_operators(), 10);
11400 assert_eq!(metric.halstead.unique_operands(), 4);
11401 assert_eq!(metric.halstead.total_operands(), 5);
11402 },
11403 );
11404 }
11405
11406 #[cfg(feature = "ruby")]
11407 #[test]
11408 fn ruby_subshell_start_alias_never_reaches_kind_id() {
11409 // Drift marker for #1360's guard, the `BQUOTE2` sibling of
11410 // `ruby_regex_start_alias_never_reaches_kind_id`. `BQUOTE2`
11411 // sits in the enum's literal-start alias block beside `DQUOTE`,
11412 // `COLONDQUOTE` and `SLASH2`, and the runtime
11413 // `public_symbol_map` collapses it to `BQUOTE` before
11414 // `kind_id()`, exactly like `LPAREN2` in #768.
11415 //
11416 // `BQUOTE2` is therefore in no dispatch arm at all — not the
11417 // guard and not the operator arm — which is the asymmetry with
11418 // `SLASH2`, a kind #1312 had to *move* out of the arithmetic
11419 // arm. Adding it to the guard would change nothing, since the
11420 // wildcard already answers `Unknown` for it. What that leaves
11421 // unguarded is the reverse direction: a bump that started
11422 // emitting `BQUOTE2` would drop a subshell opener silently
11423 // (already the wanted answer) but would also stop counting a
11424 // `` def ` `` marker spelled that way. This pins the
11425 // reachability so such a bump fails here rather than moving a
11426 // metric unobserved.
11427 let path = PathBuf::from("foo.rb");
11428 for source in ["x = `echo`\n", "x = %x{echo}\n"] {
11429 let parser = RubyParser::new(source.as_bytes().to_vec(), &path, None);
11430 assert!(
11431 !ast_has_kind_id(&parser, Ruby::BQUOTE2 as u16),
11432 "Ruby::BQUOTE2 must stay collapsed to Ruby::BQUOTE for `{source}`"
11433 );
11434 // Positive control: the id the guard actually fires on is
11435 // present, so the assertion above cannot pass merely
11436 // because no delimiter was parsed at all.
11437 assert!(
11438 ast_has_kind_id(&parser, Ruby::BQUOTE as u16),
11439 "Ruby::BQUOTE must be the delimiter kind for `{source}`"
11440 );
11441 }
11442 }
11443
11444 #[cfg(feature = "ruby")]
11445 #[test]
11446 fn ruby_interpolation_opener_is_not_an_operator() {
11447 // Behaviour change, not a fabrication fix: #1314 drops
11448 // `HASHLBRACE` from Ruby's operator arm. `#{` is a token of its
11449 // own here — unlike PHP's `{`, which aliases the
11450 // compound-statement brace — so nothing was being miscounted;
11451 // the question was whether an interpolation opener is an
11452 // operation at all, and across the five interpolating languages
11453 // three already said no. Ruby Halstead operator counts drop for
11454 // interpolated literals as a result.
11455 //
11456 // Asserted as an invariance: the interpolated and plain
11457 // spellings of one string must now score identically, which is
11458 // the policy rather than a magic number. Before the change the
11459 // interpolated row was n1 = 2, N1 = 3.
11460 //
11461 // expected per row: operator `=` × 2 → n1 = 1, N1 = 2; operands
11462 // `s`, `t`, and the literal's content contribution × 2 →
11463 // n2 = 3, N2 = 4.
11464 for literal in ["\"a #{y} b\"", "\"a b\""] {
11465 assert_halstead_counts::<RubyParser>(
11466 &format!("s = {literal}\nt = {literal}\n"),
11467 "foo.rb",
11468 [1, 2, 3, 4],
11469 &format!("literal {literal}"),
11470 );
11471 }
11472 }
11473
11474 #[cfg(feature = "ruby")]
11475 #[test]
11476 fn ruby_interpolation_opener_drop_covers_every_literal() {
11477 // `HASHLBRACE` is one arm, but it fires under every Ruby
11478 // literal that interpolates, so the drop is not specific to
11479 // double-quoted strings. A symbol and a regex — two literals
11480 // whose `#{…}` reaches the same token — must contribute no
11481 // operator for the opener either.
11482 //
11483 // expected: operator `=` × 3 → n1 = 1, N1 = 3. Operands `y`,
11484 // `1`, `a`, `b`, plus the two interpolated `y` references →
11485 // n2 = 4, N2 = 6. Before the change each `#{` added one →
11486 // n1 = 2, N1 = 5.
11487 check_metrics::<RubyParser>("y = 1\na = :\"s#{y}\"\nb = /r#{y}/\n", "foo.rb", |metric| {
11488 assert_eq!(metric.halstead.unique_operators(), 1);
11489 assert_eq!(metric.halstead.total_operators(), 3);
11490 assert_eq!(metric.halstead.unique_operands(), 4);
11491 assert_eq!(metric.halstead.total_operands(), 6);
11492 });
11493 }
11494
11495 #[cfg(feature = "ruby")]
11496 #[test]
11497 fn ruby_element_containers_count_elements_not_the_composite_1353() {
11498 // #1353. `chained_string`, `string_array` and `symbol_array`
11499 // hold *classified operand* children instead of the raw
11500 // `string_content` every other string-like literal wraps, so
11501 // the arm's shared `Interpolation` guard never fired and the
11502 // wrapper was billed alongside each element. Every row below
11503 // scored n2 4 / N2 4 for three operands, the extra entry being
11504 // the wrapper's whole-span text — which made the vocabulary
11505 // depend on how the author grouped the literals.
11506 //
11507 // `assert_ops_operands` pins the operand *text*, not just the
11508 // count: a wrapper coming back would show up as `"one" "two"` /
11509 // `%w[x y]` / `%i[p q]` rather than as an off-by-one number.
11510 //
11511 // The kind assertions are the grammar-dispatch §1 / §2 drift
11512 // marker — a bump that renumbers a wrapper or an element would
11513 // otherwise leave these counts passing while measuring a
11514 // construct the arm no longer names.
11515 //
11516 // Two of the three rows also pin the arm's *membership*:
11517 // dropping `StringArray` or `SymbolArray` from it fails
11518 // `ruby_empty_word_and_symbol_arrays_still_bill_one_operand_1353`.
11519 // `ChainedString`'s membership is unobservable and no test can
11520 // pin it — `repeat1($.string)` guarantees the guard always
11521 // fires, so `Unknown` and "not in the arm at all" are the same
11522 // answer for every input. It is listed for symmetry with its
11523 // siblings, and correct-by-construction is the only defence
11524 // available there.
11525 for (source, wrapper, element, operands) in [
11526 (
11527 "a = \"one\" \"two\"\n",
11528 Ruby::ChainedString,
11529 Ruby::String,
11530 vec!["a", "\"one\"", "\"two\""],
11531 ),
11532 (
11533 "b = %w[x y]\n",
11534 Ruby::StringArray,
11535 Ruby::BareString,
11536 vec!["b", "x", "y"],
11537 ),
11538 (
11539 "c = %i[p q]\n",
11540 Ruby::SymbolArray,
11541 Ruby::BareSymbol,
11542 vec!["c", "p", "q"],
11543 ),
11544 ] {
11545 let parser =
11546 RubyParser::new(source.as_bytes().to_vec(), &PathBuf::from("foo.rb"), None);
11547 assert!(
11548 ast_has_kind_id(&parser, wrapper as u16),
11549 "the container kind this arm gates on is unreachable for `{source}`"
11550 );
11551 assert!(
11552 ast_has_kind_id(&parser, element as u16),
11553 "the element kind this arm gates on is unreachable for `{source}`"
11554 );
11555
11556 // expected: operator `=` → n1 = N1 = 1; operands are the
11557 // assignment target and the two elements → n2 = N2 = 3.
11558 assert_halstead_counts::<RubyParser>(source, "foo.rb", [1, 1, 3, 3], source);
11559 assert_ops_operands::<RubyParser>(source, "foo.rb", 3, operands);
11560 }
11561 }
11562
11563 #[cfg(feature = "ruby")]
11564 #[test]
11565 fn ruby_empty_word_and_symbol_arrays_still_bill_one_operand_1353() {
11566 // The childless spelling, and the whole reason #1353 gates the
11567 // three container kinds instead of dropping them from the arm
11568 // (grammar-dispatch §6). `%w[]` / `%i[]` parse to a wrapper
11569 // holding nothing but its two delimiter tokens, so the tempting
11570 // "just delete the wrapper" fix — the one #1351 was right to
11571 // take for Bash's `command_name` — would score an empty literal
11572 // zero operands where the source plainly has a literal. This is
11573 // the assertion to watch fail against that alternative.
11574 //
11575 // expected: operator `=` → n1 = N1 = 1; operands the assignment
11576 // target and the empty literal itself → n2 = N2 = 2.
11577 for (source, operands) in [
11578 ("d = %w[]\n", vec!["d", "%w[]"]),
11579 ("e = %i[]\n", vec!["e", "%i[]"]),
11580 ] {
11581 assert_halstead_counts::<RubyParser>(source, "foo.rb", [1, 1, 2, 2], source);
11582 assert_ops_operands::<RubyParser>(source, "foo.rb", 2, operands);
11583 }
11584 }
11585
11586 #[cfg(feature = "ruby")]
11587 #[test]
11588 fn ruby_interpolated_array_elements_are_not_double_counted_1353() {
11589 // `bare_string` and `bare_symbol` are two aliases of a single
11590 // grammar production (`_literal_contents`), one per array form,
11591 // so `%W[…]` and `%I[…]` must score identically. Until #1353
11592 // only `bare_string` carried the interpolation guard and
11593 // `bare_symbol` sat in the plain operand arm, so `%I[a#{n}b c]`
11594 // billed the element `a#{n}b` *and* the `n` inside it (n2 4)
11595 // where `%W[a#{n}b c]` billed 3.
11596 //
11597 // Asserted as an invariance over the two spellings, the way
11598 // `ruby_interpolation_opener_is_not_an_operator` is — the
11599 // policy is the claim, not the magic number. The third row is
11600 // the composed case: a `chained_string` whose own guard defers
11601 // to an element that is itself interpolated.
11602 //
11603 // expected per row: operator `=` → n1 = N1 = 1; operands the
11604 // assignment target, the interpolated expression `n`, and the
11605 // one inert element → n2 = N2 = 3. The operand *text* is pinned
11606 // too, because 3 is also the count an implementation that
11607 // suppressed the inert element instead of the wrapper would
11608 // report.
11609 for (source, operands) in [
11610 ("w = %W[a#{n}b c]\n", vec!["w", "n", "c"]),
11611 ("w = %I[a#{n}b c]\n", vec!["w", "n", "c"]),
11612 ("a = \"x#{n}\" \"y\"\n", vec!["a", "n", "\"y\""]),
11613 ] {
11614 assert_halstead_counts::<RubyParser>(source, "foo.rb", [1, 1, 3, 3], source);
11615 assert_ops_operands::<RubyParser>(source, "foo.rb", 3, operands);
11616 }
11617 }
11618
11619 #[cfg(feature = "ruby")]
11620 #[test]
11621 fn ruby_suffixed_numeric_literals_bill_one_operand_1359() {
11622 // #1359, the wrapper/leaf double count one arm below #1353's.
11623 // `rational` and `complex` wrap the numeral they suffix, and
11624 // `get_op_type` billed every level: `a = 1r` scored n2 3 / N2 3
11625 // for two operands, and `c = 3ri` — a `complex` over a
11626 // `rational` over an `integer` — scored n2 3 / N2 4 for the
11627 // same two.
11628 //
11629 // The rows are ordered by nesting depth on purpose, because the
11630 // half-fix is a plausible one to write. Guard the two leaf
11631 // kinds and not the intermediate `rational` — `R::Integer |
11632 // R::Float` where the arm says `R::Integer | R::Float |
11633 // R::Rational` — and the `1r` and `2i` rows still pass; the
11634 // `ri` rows, whose middle `rational` must be suppressed as well
11635 // as the leaf beneath it, are the two that fail (measured:
11636 // `c = 3ri` reports n2 3 / N2 3).
11637 //
11638 // The trailing rows are the ones that must *not* move. `g = 4`
11639 // has no wrapper at all and pins that the guard is scoped to
11640 // the numerals inside one (grammar-dispatch §6 — nothing
11641 // regresses to zero). `"#{4r}"` and `-3r` put the wrapper
11642 // somewhere other than an assignment right-hand side, under a
11643 // node that guards its own children (`Interpolation`, whose
11644 // enclosing `String` is suppressed by the arm above) and under
11645 // one that does not (`unary`); both must still bill the
11646 // wrapper once. They do *not* pin parent-versus-ancestor
11647 // scoping and nothing can: `complex` and `rational` admit
11648 // numeral children only, so an ancestor-scoped guard would
11649 // classify every Ruby input identically. And `1r + 1r` is the
11650 // row where N2 and n2 come apart, so the `N2` column is
11651 // load-bearing on its own rather than tracking `n2`.
11652 //
11653 // The kind assertions are the grammar-dispatch §1 / §2 drift
11654 // marker: a bump that renumbered `complex` or `rational`, or
11655 // stopped nesting them, would leave the counts passing while
11656 // measuring a construct the guard no longer names. `IntOrFloat`
11657 // is the §2 half — the hidden `_int_or_float` the two wrappers
11658 // are written over, which a bump could promote into the
11659 // numeral's parent and silently stop the guard firing. Not
11660 // every kind a row mentions is one the guard names
11661 // (`Interpolation` is context), and `unary` and `binary` are
11662 // deliberately absent: they carry five and three aliased
11663 // variants here, so naming one would pin the alias rather than
11664 // the shape.
11665 for (source, kinds, counts, operands) in [
11666 (
11667 "a = 1r\n",
11668 &[Ruby::Rational as u16, Ruby::Integer as u16][..],
11669 [1, 1, 2, 2],
11670 vec!["a", "1r"],
11671 ),
11672 (
11673 "b = 2i\n",
11674 &[Ruby::Complex as u16, Ruby::Integer as u16][..],
11675 [1, 1, 2, 2],
11676 vec!["b", "2i"],
11677 ),
11678 (
11679 "c = 3ri\n",
11680 &[
11681 Ruby::Complex as u16,
11682 Ruby::Rational as u16,
11683 Ruby::Integer as u16,
11684 ][..],
11685 [1, 1, 2, 2],
11686 vec!["c", "3ri"],
11687 ),
11688 (
11689 "d = 1.5r\n",
11690 &[Ruby::Rational as u16, Ruby::Float as u16][..],
11691 [1, 1, 2, 2],
11692 vec!["d", "1.5r"],
11693 ),
11694 (
11695 "e = 2.5i\n",
11696 &[Ruby::Complex as u16, Ruby::Float as u16][..],
11697 [1, 1, 2, 2],
11698 vec!["e", "2.5i"],
11699 ),
11700 (
11701 "f = 1.5ri\n",
11702 &[
11703 Ruby::Complex as u16,
11704 Ruby::Rational as u16,
11705 Ruby::Float as u16,
11706 ][..],
11707 [1, 1, 2, 2],
11708 vec!["f", "1.5ri"],
11709 ),
11710 (
11711 "g = 4\n",
11712 &[Ruby::Integer as u16][..],
11713 [1, 1, 2, 2],
11714 vec!["g", "4"],
11715 ),
11716 (
11717 "h = \"#{4r}\"\n",
11718 &[Ruby::Interpolation as u16, Ruby::Rational as u16][..],
11719 [1, 1, 2, 2],
11720 vec!["h", "4r"],
11721 ),
11722 (
11723 "i = -3r\n",
11724 &[Ruby::Rational as u16, Ruby::Integer as u16][..],
11725 [2, 2, 2, 2],
11726 vec!["i", "3r"],
11727 ),
11728 (
11729 "j = 1r + 1r\n",
11730 &[Ruby::Rational as u16, Ruby::Integer as u16][..],
11731 [2, 2, 2, 3],
11732 vec!["j", "1r"],
11733 ),
11734 ] {
11735 let parser =
11736 RubyParser::new(source.as_bytes().to_vec(), &PathBuf::from("foo.rb"), None);
11737 for kind in kinds {
11738 assert!(
11739 ast_has_kind_id(&parser, *kind),
11740 "the kind this row measures is unreachable for `{source}`"
11741 );
11742 }
11743 assert!(
11744 !ast_has_kind_id(&parser, Ruby::IntOrFloat as u16),
11745 "`_int_or_float` is hidden; a grammar that emits it would \
11746 sit between the wrapper and the numeral for `{source}`"
11747 );
11748
11749 assert_halstead_counts::<RubyParser>(source, "foo.rb", counts, source);
11750 assert_ops_operands::<RubyParser>(source, "foo.rb", operands.len(), operands);
11751 }
11752 }
11753
11754 #[cfg(feature = "ruby")]
11755 #[test]
11756 fn ruby_numeric_suffixes_stay_distinct_operands_1359() {
11757 // The reason #1359 keeps the *wrapper* and gates the leaf
11758 // rather than dropping the two wrapper kinds from the arm, as
11759 // #1351 and #1358 did for their Bash wrappers. `1`, `1r`, `1i`
11760 // and `1ri` are four distinct Ruby constants — Integer,
11761 // Rational, Complex and Complex-of-Rational — and only the
11762 // wrapper's span carries the suffix that says which.
11763 //
11764 // This is the assertion to watch fail against the leaf-keeping
11765 // alternative: billing the `integer` would file all four under
11766 // the operand text `1`, reporting n2 5 / N2 8 where the source
11767 // plainly has eight distinct operands. The count alone does not
11768 // say that, so the vocabulary is pinned by text.
11769 //
11770 // expected: operator `=` → n1 1, N1 4; four targets and four
11771 // constants, none repeated → n2 = N2 = 8.
11772 let source = "w = 1\nx = 1r\ny = 1i\nz = 1ri\n";
11773 assert_halstead_counts::<RubyParser>(source, "foo.rb", [1, 4, 8, 8], source);
11774 assert_ops_operands::<RubyParser>(
11775 source,
11776 "foo.rb",
11777 8,
11778 vec!["w", "x", "y", "z", "1", "1r", "1i", "1ri"],
11779 );
11780 }
11781
11782 /// Comprehensive iRules Halstead test exercising every operator family
11783 /// classified in `get_op_type`: declaration/control keywords (`proc`,
11784 /// `set`, `if`, `return`), structural punctuation (`{}` `[]` `()`),
11785 /// arithmetic (`+`), comparison (`>`), the word-form string comparator
11786 /// (`eq`), and short-circuit logical (`&&`). Anchored on the integer
11787 /// `n1`/`N1`/`n2`/`N2` headline values; the float fields are derived and
11788 /// bit-brittle, so they are not pinned.
11789 ///
11790 /// The second half pins the lesson-4 invariant: the independent
11791 /// text-keyed `operands_and_operators` store must dedupe to the same
11792 /// `n1`/`n2`. A classification change that moved one store without the
11793 /// other (e.g. a kind landing in both the operator and operand arms)
11794 /// would break this even though the snapshot stayed green.
11795 #[cfg(feature = "irules")]
11796 #[test]
11797 fn irules_operators_and_operands() {
11798 let source = "proc f { a b } {
11799 set x [expr { $a + $b }]
11800 if { $x > 0 && $a eq \"go\" } {
11801 return $x
11802 }
11803 return 0
11804}
11805";
11806 check_metrics::<IrulesParser>(source, "foo.irule", |metric| {
11807 // After #695 only opening delimiters count: the `}`/`]`
11808 // closers no longer add operators (was 12 unique / 20 total).
11809 assert_eq!(metric.halstead.unique_operators(), 10);
11810 assert_eq!(metric.halstead.total_operators(), 14);
11811 // Operands fell 12 / 16 → 10 / 14 with #1354: the proc body
11812 // and the `if` body are `BracedWord` script kinds and are no
11813 // longer operands beside the commands they contain.
11814 assert_eq!(metric.halstead.unique_operands(), 10);
11815 assert_eq!(metric.halstead.total_operands(), 14);
11816 });
11817
11818 let path = PathBuf::from("foo.irule");
11819 let parser = IrulesParser::new(source.as_bytes().to_vec(), &path, None);
11820 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
11821 let unique_operators: HashSet<&str> = ops.operators.iter().map(String::as_str).collect();
11822 let unique_operands: HashSet<&str> = ops.operands.iter().map(String::as_str).collect();
11823 assert_eq!(
11824 unique_operators.len(),
11825 10,
11826 "dedupe(ops.operators) must equal n1; operators were {:?}",
11827 ops.operators
11828 );
11829 assert_eq!(
11830 unique_operands.len(),
11831 10,
11832 "dedupe(ops.operands) must equal n2; operands were {:?}",
11833 ops.operands
11834 );
11835 }
11836
11837 /// An inert `"hello world"` double-quoted string (no `$var` / `[cmd]`
11838 /// interpolation child) contributes exactly **one** operand — the
11839 /// wrapping `QuotedWord`. Operands are `f`, `s` and `"hello world"` —
11840 /// n2 = 3, the same as Tcl since #1294 restored its `set` target to
11841 /// the operand count and #1354 dropped the proc-body `braced_word`
11842 /// from both. Mirrors `tcl_inert_quoted_word_counts_as_operand`
11843 /// (#277).
11844 #[cfg(feature = "irules")]
11845 #[test]
11846 fn irules_inert_quoted_word_counts_as_operand() {
11847 let source = "proc f {} {\n set s \"hello world\"\n}\n";
11848 check_metrics::<IrulesParser>(source, "foo.irule", |metric| {
11849 // After #695 only the `{` opener counts; the `}` closer no
11850 // longer adds an operator (was 4 unique / 6 total).
11851 assert_eq!(metric.halstead.unique_operators(), 3);
11852 assert_eq!(metric.halstead.total_operators(), 4);
11853 assert_eq!(metric.halstead.unique_operands(), 3);
11854 assert_eq!(metric.halstead.total_operands(), 3);
11855 });
11856
11857 let path = PathBuf::from("foo.irule");
11858 let parser = IrulesParser::new(source.as_bytes().to_vec(), &path, None);
11859 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
11860 // The inert quoted word is present as exactly one operand (not
11861 // dropped, not split): dropping it would mean the inert branch was
11862 // over-guarded.
11863 let quoted = ops
11864 .operands
11865 .iter()
11866 .filter(|o| o.as_str() == "\"hello world\"")
11867 .count();
11868 assert_eq!(quoted, 1, "inert quoted word must be one operand");
11869 let unique_operands: HashSet<&str> = ops.operands.iter().map(String::as_str).collect();
11870 assert_eq!(unique_operands.len(), 3, "operands were {:?}", ops.operands);
11871 }
11872
11873 /// Regression for the `QuotedWord` interpolation guard (the #277 /
11874 /// Bash-#180 / C#-#183 / PHP-#184 pattern). An interpolated
11875 /// `"$x is $y"` must contribute **zero** operands for the wrapping
11876 /// `QuotedWord`; the inner `$x` / `$y` `variable_substitution` nodes are
11877 /// walked separately and count on their own. Operands are `f`, `x`, `y`,
11878 /// `s`, `$x`, `$y` = 6 (7 before #1354 dropped the proc-body
11879 /// `braced_word`). If the guard regressed (wrapper classified
11880 /// `Operand`), the wrapper string would add a 7th operand. This is the
11881 /// branch that had no test before.
11882 #[cfg(feature = "irules")]
11883 #[test]
11884 fn irules_interpolated_quoted_word_no_double_count() {
11885 let source = "proc f {x y} {\n set s \"$x is $y\"\n}\n";
11886 check_metrics::<IrulesParser>(source, "foo.irule", |metric| {
11887 // After #695 only the `{` opener counts; the `}` closer no
11888 // longer adds an operator (was 4 unique / 6 total).
11889 assert_eq!(metric.halstead.unique_operators(), 3);
11890 assert_eq!(metric.halstead.total_operators(), 4);
11891 assert_eq!(metric.halstead.unique_operands(), 6);
11892 assert_eq!(metric.halstead.total_operands(), 6);
11893 });
11894
11895 let path = PathBuf::from("foo.irule");
11896 let parser = IrulesParser::new(source.as_bytes().to_vec(), &path, None);
11897 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
11898 // The wrapping interpolated string must NOT appear as an operand;
11899 // its inner substitutions must. The wrapper, if wrongly counted,
11900 // would surface as the quoted literal `"$x is $y"` (with quotes,
11901 // like the inert `"hello world"` operand). Match that exact token —
11902 // a substring check would false-match the proc-body `braced_word`
11903 // operand, which legitimately contains the source text.
11904 assert!(
11905 !ops.operands.iter().any(|o| o.as_str() == "\"$x is $y\""),
11906 "interpolated wrapper must not be an operand; operands were {:?}",
11907 ops.operands
11908 );
11909 assert!(
11910 ops.operands.iter().any(|o| o.as_str() == "$x")
11911 && ops.operands.iter().any(|o| o.as_str() == "$y"),
11912 "inner $x / $y substitutions must each be operands; operands were {:?}",
11913 ops.operands
11914 );
11915 let unique_operands: HashSet<&str> = ops.operands.iter().map(String::as_str).collect();
11916 assert_eq!(unique_operands.len(), 6, "operands were {:?}", ops.operands);
11917 }
11918
11919 /// Exercises the operator families not covered by
11920 /// `irules_operators_and_operands`: bitwise (`& | ^ ~ << >>`), ternary
11921 /// (`? :`), the keyword string comparators (`starts_with`, `ends_with`,
11922 /// `contains`, `matches`, `eq`, `ne`), and the keyword logical operator
11923 /// (`and`). Pins every operator-family arm in `get_op_type` plus the
11924 /// lesson-4 dedupe invariant.
11925 #[cfg(feature = "irules")]
11926 #[test]
11927 fn irules_bitwise_ternary_string_ops() {
11928 let source = "proc f { a b } {
11929 set bits [expr { $a & $b | $a ^ ~$b }]
11930 set sh [expr { $a << 2 | $b >> 1 }]
11931 set t [expr { $a > 0 ? $a : $b }]
11932 if { $a starts_with \"x\" && $b ends_with \"y\" } { return 1 }
11933 if { $a contains \"z\" || $b matches \"q\" } { return 2 }
11934 if { $a eq \"m\" and $b ne \"n\" } { return 3 }
11935 return $b
11936}
11937";
11938 check_metrics::<IrulesParser>(source, "foo.irule", |metric| {
11939 // After #695 only opening delimiters count: the `}`/`]`
11940 // closers no longer add operators (was 26 unique / 57 total).
11941 assert_eq!(metric.halstead.unique_operators(), 24);
11942 assert_eq!(metric.halstead.total_operators(), 43);
11943 // Operands fell 23 / 42 → 19 / 38 with #1354: the proc body
11944 // and the three single-statement `if` bodies are
11945 // `BracedWord` script kinds and no longer count as operands.
11946 assert_eq!(metric.halstead.unique_operands(), 19);
11947 assert_eq!(metric.halstead.total_operands(), 38);
11948 });
11949
11950 let path = PathBuf::from("foo.irule");
11951 let parser = IrulesParser::new(source.as_bytes().to_vec(), &path, None);
11952 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
11953 let unique_operators: HashSet<&str> = ops.operators.iter().map(String::as_str).collect();
11954 let unique_operands: HashSet<&str> = ops.operands.iter().map(String::as_str).collect();
11955 assert_eq!(
11956 unique_operators.len(),
11957 24,
11958 "dedupe(ops.operators) must equal n1; operators were {:?}",
11959 ops.operators
11960 );
11961 assert_eq!(
11962 unique_operands.len(),
11963 19,
11964 "dedupe(ops.operands) must equal n2; operands were {:?}",
11965 ops.operands
11966 );
11967 }
11968
11969 /// A bare `$x` produces one `variable_substitution` operand. Its inner
11970 /// `id` leaf (the *named* `Id` node — not the anonymous `Id2` token Tcl
11971 /// has there) must NOT be counted separately, or every variable
11972 /// reference double-counts. `get_op_type` excludes `Id` whose parent is
11973 /// a `VariableSubstitution`. Operands: `f`, the proc arg `x`, `return`
11974 /// and `$x` — four, with no duplicate (`total_operands()` == 4; the
11975 /// proc-body `braced_word` was a fifth before #1354). If the guard
11976 /// regressed, the inner `id` "x" would add a fifth operand occurrence
11977 /// (it text-collides with the proc arg `x`, so `u_operands` would stay
11978 /// 4 but `total_operands()` would rise to 5 — hence the total, not just
11979 /// the unique count, is asserted).
11980 #[cfg(feature = "irules")]
11981 #[test]
11982 fn irules_array_reference_bills_the_reference_and_the_index() {
11983 // The iRules twin of
11984 // `tcl_array_reference_bills_the_reference_and_the_index`. Until
11985 // `ArrayIndex` left the operand arm this fixture billed `(k)` and
11986 // `($i)` beside the six operands below — the grammar-dispatch §5
11987 // wrapper-plus-leaf count, and a divergence from Tcl.
11988 assert_ops_operands::<IrulesParser>(
11989 "set arr(k) 1\nset z \"$arr($i)\"\n",
11990 "foo.irule",
11991 6,
11992 vec!["arr", "k", "1", "z", "$arr($i)", "$i"],
11993 );
11994 }
11995
11996 #[cfg(feature = "irules")]
11997 #[test]
11998 fn irules_bare_variable_operand() {
11999 let source = "proc f {x} {\n return $x\n}\n";
12000 check_metrics::<IrulesParser>(source, "foo.irule", |metric| {
12001 // After #695 only the `{` opener counts (folded `{}`); the
12002 // `}` closer no longer adds an operator (was 3 unique / 5 total).
12003 assert_eq!(metric.halstead.unique_operators(), 2);
12004 assert_eq!(metric.halstead.total_operators(), 3);
12005 assert_eq!(metric.halstead.unique_operands(), 4);
12006 assert_eq!(metric.halstead.total_operands(), 4);
12007 });
12008
12009 let path = PathBuf::from("foo.irule");
12010 let parser = IrulesParser::new(source.as_bytes().to_vec(), &path, None);
12011 let ops = crate::ops::ops_inner(&parser, None).expect("ops walk succeeds");
12012 let bare_var = ops.operands.iter().filter(|o| o.as_str() == "$x").count();
12013 assert_eq!(
12014 bare_var, 1,
12015 "bare $x must be exactly one operand (inner id leaf not double-counted); operands were {:?}",
12016 ops.operands
12017 );
12018 }
12019
12020 #[cfg(feature = "irules")]
12021 #[test]
12022 fn irules_braced_word_delimiter_is_not_an_operator() {
12023 // Regression: issue #1314. The iRules twin of
12024 // `tcl_braced_word_delimiter_is_not_an_operator` — the two
12025 // getters are deliberate clones, so the guard lands in both and
12026 // is asserted in both.
12027 //
12028 // One fixture covers the guard and its control: `{braced word}`
12029 // and `{y}` are values whose openers must not count, while the
12030 // handler body, the `if` condition and the `if` body are
12031 // `BracedWord` / `Expr` and must keep theirs. A guard keyed on
12032 // the brace alone would take `{}` out of the operator set
12033 // entirely and fail here.
12034 //
12035 // expected: operators `when`, `set` × 2, `if`, `contains`,
12036 // `[]`, `{}` × 3 → n1 = 6, N1 = 9. Every operand is distinct →
12037 // n2 = N2 = 7: `HTTP_REQUEST`, `a`, `b`, `HTTP::uri`, `"x"` and
12038 // the two braced words `{braced word}` and `{y}`. Before the
12039 // guard the two value openers added two more `{}` occurrences →
12040 // N1 = 11; before #1354 the handler and `if` bodies and the
12041 // inner `braced` / `word` / `y` were operands too → n2 = 12.
12042 check_metrics::<IrulesParser>(
12043 "when HTTP_REQUEST {\n set a {braced word}\n if {[HTTP::uri] contains \"x\"} { set b {y} }\n}\n",
12044 "foo.irule",
12045 |metric| {
12046 assert_eq!(metric.halstead.unique_operators(), 6);
12047 assert_eq!(metric.halstead.total_operators(), 9);
12048 assert_eq!(metric.halstead.unique_operands(), 7);
12049 assert_eq!(metric.halstead.total_operands(), 7);
12050 },
12051 );
12052 }
12053
12054 /// Regression for #563: the two Halstead `Display` labels must use the
12055 /// underscore key that matches the JSON/CSV field name, so a user can grep
12056 /// the same token across `Display` and JSON. The space-separated forms
12057 /// (`estimated program length` / `purity ratio`) were the only outliers,
12058 /// mirroring the `dump` fix in #562.
12059 #[cfg(feature = "cpp")]
12060 #[test]
12061 fn display_halstead_labels_use_underscore_keys() {
12062 check_metrics::<CppParser>("int a = 42;", "foo.cpp", |metric| {
12063 let out = metric.halstead.to_string();
12064 assert!(
12065 out.contains("estimated_program_length: "),
12066 "Display must use the underscore key `estimated_program_length`:\n{out}"
12067 );
12068 assert!(
12069 out.contains("purity_ratio: "),
12070 "Display must use the underscore key `purity_ratio`:\n{out}"
12071 );
12072 assert!(
12073 !out.contains("estimated program length"),
12074 "Display must not emit the space-separated `estimated program length`:\n{out}"
12075 );
12076 assert!(
12077 !out.contains("purity ratio"),
12078 "Display must not emit the space-separated `purity ratio`:\n{out}"
12079 );
12080 });
12081 }
12082
12083 /// `@"…"` is one `string_literal` holding its `@` as a child, so the
12084 /// literal is the operand, keyed by its whole text, and the marker is
12085 /// not an operator on top — it was, which planted a phantom `@` in
12086 /// n1 for a file whose only `@` was in NSString literals and billed
12087 /// the same byte in both streams. Boxing (`@42`) keeps its `@`: there
12088 /// the token is a child of the `at_expression`, not of the literal.
12089 #[cfg(feature = "objc")]
12090 #[test]
12091 fn objc_nsstring_literal_is_one_operand() {
12092 // expected: [n1, N1, n2, N2], each operator count including the
12093 // wrapper's `@implementation` (#1552). Before the guard the first
12094 // two rows read [9, 9, 5, 5] and [9, 10, 6, 6] — one `@` operator
12095 // per literal; the boxing control is unchanged.
12096 let cases = [
12097 ("NSString *s = @\"str\";", [8, 8, 5, 5]),
12098 ("NSString *t = @\"x\" @\"y\";", [8, 8, 6, 6]),
12099 ("NSNumber *n = @42;", [9, 9, 5, 5]),
12100 ];
12101 for (body, counts) in cases {
12102 let source = format!("@implementation Foo\n- (void)m {{\n {body}\n}}\n@end\n");
12103 assert_halstead_counts::<ObjcParser>(&source, "foo.m", counts, body);
12104 }
12105 assert_ops_operands::<ObjcParser>(
12106 "@implementation Foo\n- (void)m {\n NSString *s = @\"str\";\n}\n@end\n",
12107 "foo.m",
12108 5,
12109 vec!["Foo", "m", "NSString", "s", "@\"str\""],
12110 );
12111 }
12112
12113 /// Comprehensive Objective-C Halstead fixture exercising a message
12114 /// send (`[self log:@"hi"]`), an ObjC string literal (`@"hi"`), an
12115 /// `if`, a short-circuit `&&`, arithmetic (`+`), comparisons, and
12116 /// assignment. Pins every field and enforces the lesson-4 invariants
12117 /// `unique_operators == n1` / `unique_operands == n2` via the
12118 /// independent `--ops` store.
12119 #[cfg(feature = "objc")]
12120 #[test]
12121 fn objc_operators_and_operands() {
12122 let source = "@implementation Foo
12123- (int)bar:(int)x {
12124 int y = x + 1;
12125 if (x > 0 && y < 10) {
12126 [self log:@\"hi\"];
12127 }
12128 return y;
12129}
12130@end
12131";
12132 check_metrics::<ObjcParser>(source, "foo.m", |metric| {
12133 // n1 = 15 unique operators:
12134 // `&&`, `()`, `+`, `-`, `:`, `;`, `<`, `=`, `>`,
12135 // `[]` (message send), `if`, `int`, `return`, `{}`,
12136 // `@implementation` (#1552).
12137 // The `@` of `@"hi"` is part of the literal's operand
12138 // key, not an operator (see `objc_nsstring_literal_is_one_operand`).
12139 // n2 = 10 unique operands:
12140 // `Foo`, `bar`, `log`, `self`, `x`, `y`, `0`, `1`, `10`,
12141 // `@"hi"` (the ObjC string literal).
12142 assert_eq!(metric.halstead.unique_operators(), 15);
12143 assert_eq!(metric.halstead.unique_operands(), 10);
12144 insta::assert_json_snapshot!(metric.halstead, @r#"
12145 {
12146 "unique_operators": 15,
12147 "total_operators": 23,
12148 "unique_operands": 10,
12149 "total_operands": 14,
12150 "length": 37,
12151 "estimated_program_length": 91.82263988300141,
12152 "purity_ratio": 2.481692969810849,
12153 "vocabulary": 25,
12154 "volume": 171.8226790216648,
12155 "difficulty": 10.5,
12156 "level": 0.09523809523809523,
12157 "effort": 1804.1381297274804,
12158 "time": 100.22989609597113,
12159 "bugs": 0.049399808887691035
12160 }
12161 "#);
12162 });
12163 // Lesson-4 invariant: dedupe(ops.operands) == n2 (10), via the
12164 // independent text-keyed `--ops` store.
12165 assert_ops_operands::<ObjcParser>(
12166 source,
12167 "foo.m",
12168 10,
12169 vec![
12170 "Foo", "bar", "log", "self", "x", "y", "0", "1", "10", "@\"hi\"",
12171 ],
12172 );
12173 }
12174
12175 /// #1316 fixture separating the vocabulary and occurrence axes
12176 /// (#1294): `'x'` appears twice, so a correct fix adds four `n2`
12177 /// entries and five `N2` hits. The `'ab'` multi-character constant
12178 /// is also the grammar-dispatch section 5 pin — that literal carries
12179 /// *two* `character` children and must still bill one operand.
12180 ///
12181 /// Both #1316 fixtures are plain C, which every C-family grammar
12182 /// parses to the same shape, so one source proves the same thing
12183 /// about each of the four clones.
12184 #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12185 const C_FAMILY_CHAR_REPEATS: &str =
12186 "char a = 'x';\nchar b = 'x';\nchar c = 'y';\nchar d = '\\n';\nint e = 'ab';\n";
12187
12188 /// #1316 fixture holding all five spellings the grammars admit. Each
12189 /// opens on a distinct delimiter kind (`'`, `L'`, `u'`, `U'`, `u8'`),
12190 /// and operands key on source text, so the five are five vocabulary
12191 /// entries rather than one.
12192 #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12193 const C_FAMILY_CHAR_PREFIXES: &str =
12194 "char a = 'x';\nchar b = L'x';\nchar c = u'x';\nchar d = U'x';\nchar e = u8'x';\n";
12195
12196 /// Asserts both #1316 fixtures for one C-family language, through
12197 /// the metrics store *and* the text-keyed `--ops` store (the
12198 /// lesson-4 invariant `n2 == len(dedupe(ops.operands))`).
12199 ///
12200 /// The `--ops` half pins *which text* each literal is billed under,
12201 /// which the counts cannot see: billing a literal's `character`
12202 /// payload instead of the whole literal keeps `n2` at 9 while the
12203 /// vocabulary silently becomes `x` rather than `'x'`. (Billing
12204 /// *both* is caught earlier, by the counts.) It is a second *walk*
12205 /// rather than a second classification — `ops_inner` reads the keys
12206 /// of the same `HalsteadMaps` — which is worth knowing before
12207 /// reading it as independent corroboration of the count.
12208 #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12209 #[track_caller]
12210 fn assert_char_literal_operands<T: crate::MetricSuite>(file: &str, label: &str) {
12211 // `char` x4 and `int` are text-keyed primitive operators, so
12212 // n1 = 4 (`;`, `=`, `char`, `int`) and N1 = 5 + 5 + 4 + 1.
12213 // Operands: `a`..`e`, plus `'x'` (twice), `'y'`, `'\n'`, `'ab'`.
12214 let repeats = format!("{label}: repeated / escaped / multi-char literals");
12215 assert_halstead_counts::<T>(C_FAMILY_CHAR_REPEATS, file, [4, 15, 9, 10], &repeats);
12216 assert_ops_operands::<T>(
12217 C_FAMILY_CHAR_REPEATS,
12218 file,
12219 9,
12220 vec!["a", "b", "c", "d", "e", "'x'", "'y'", "'\\n'", "'ab'"],
12221 );
12222
12223 // Every declaration is `char` here, so the `int` primitive
12224 // operator of the other fixture is gone and n1 drops to 3. Ten
12225 // distinct operands, each seen once.
12226 let prefixes = format!("{label}: L / u / U / u8 prefixed literals");
12227 assert_halstead_counts::<T>(C_FAMILY_CHAR_PREFIXES, file, [3, 15, 10, 10], &prefixes);
12228 assert_ops_operands::<T>(
12229 C_FAMILY_CHAR_PREFIXES,
12230 file,
12231 10,
12232 vec![
12233 "a", "b", "c", "d", "e", "'x'", "L'x'", "u'x'", "U'x'", "u8'x'",
12234 ],
12235 );
12236 }
12237
12238 /// Regression for #1316: a C-family character literal is a Halstead
12239 /// operand.
12240 ///
12241 /// `char_literal` was in no arm of `CCode` / `CppCode` /
12242 /// `MozcppCode` / `ObjcCode`'s `get_op_type`, so a character literal
12243 /// contributed *nothing* — not an operator (correct) and not an
12244 /// operand (wrong) — while Rust, Java, Kotlin, C#, Go and Elixir all
12245 /// counted theirs. Both fixtures measured `n2` 5, `N2` 5 before the
12246 /// fix: the five declared identifiers and not one literal.
12247 ///
12248 /// Each language asserts separately over the same source rather than
12249 /// sharing one call, so reverting one clone's arm fails that row
12250 /// alone (grammar-dispatch section 11) — verified by perturbing each
12251 /// of the four arms in turn. A single shared assertion would be
12252 /// satisfied by whichever clone still had the arm.
12253 ///
12254 /// Mozcpp owns no file extension, so no integration snapshot ever
12255 /// reaches its clone; its row is the whole coverage that arm has.
12256 #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12257 #[test]
12258 fn c_family_char_literals_are_operands() {
12259 assert_char_literal_operands::<CParser>("chars.c", "c");
12260 assert_char_literal_operands::<CppParser>("chars.cpp", "cpp");
12261 assert_char_literal_operands::<MozcppParser>("chars.cpp", "mozcpp");
12262 assert_char_literal_operands::<ObjcParser>("chars.m", "objc");
12263 }
12264
12265 /// ObjC boxes a character literal as `@'y'` — an `at_expression`
12266 /// wrapping the same `char_literal`, with the `@` counted as its own
12267 /// operator. The wrapper is in no operand arm, so the boxed form
12268 /// bills exactly the literal it wraps and stays distinct from a bare
12269 /// one (#1316).
12270 #[cfg(feature = "objc")]
12271 #[test]
12272 fn objc_boxed_char_literal_is_one_operand() {
12273 let source = "char a = 'x';\nid b = @'y';\n";
12274 // n1: `char`, `=`, `;`, `@`. N1: 1 + 2 + 2 + 1.
12275 // n2 / N2: `a`, `b`, `'x'`, `'y'` — `id` is a `typedefed_specifier`
12276 // and is classified by neither arm. Before #1316 this was n2 2,
12277 // N2 2.
12278 assert_halstead_counts::<ObjcParser>(source, "boxed.m", [4, 6, 4, 4], "objc @'y'");
12279 assert_ops_operands::<ObjcParser>(source, "boxed.m", 4, vec!["a", "b", "'x'", "'y'"]);
12280 }
12281
12282 /// Walks both #1316 fixtures under all four C-family `Getter`s and
12283 /// pins the two grammar facts the new operand arm rests on.
12284 ///
12285 /// * **Grammar-dispatch section 1.** In the positions these
12286 /// fixtures exercise, every node the grammar spells `char_literal`
12287 /// carries the one `kind_id` the arm lists. That is the weaker
12288 /// half of the alias evidence — an alias arises in a *different*
12289 /// syntactic position, which no fixture can enumerate. The strong
12290 /// half is that these generated enums do carry numeric-suffix
12291 /// aliases in quantity (`language_c.rs` alone has ninety) and none
12292 /// of the four spells a `CharLiteral2`, so its absence is a
12293 /// measurement rather than a silence. This loop is what notices if
12294 /// a grammar bump changes that under an existing fixture.
12295 /// * **Grammar-dispatch section 5.** No child of a `char_literal` is
12296 /// classified. That is what makes listing the wrapper safe rather
12297 /// than a wrapper/leaf double count: the opening delimiter, the
12298 /// closing `'`, and the `character` / `escape_sequence` payload
12299 /// must all stay `Unknown`, or every literal would bill two
12300 /// operands and a prefixed one three.
12301 ///
12302 /// Both loops are non-vacuous by assertion, since a fixture that
12303 /// stopped containing a character literal would otherwise make this
12304 /// test pass having checked nothing.
12305 #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12306 #[test]
12307 fn c_family_char_literal_internals_stay_unclassified() {
12308 #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12309 fn check<L: LanguageInfo + Getter>(char_literal: u16, label: &str) {
12310 let mut literals = 0_usize;
12311 let mut children = 0_usize;
12312 for source in [C_FAMILY_CHAR_REPEATS, C_FAMILY_CHAR_PREFIXES] {
12313 for_each_node_with_chain::<L>(source.as_bytes(), |node, chain| {
12314 if node.kind() == "char_literal" {
12315 assert_eq!(
12316 node.kind_id(),
12317 char_literal,
12318 "{label}: a `char_literal` carries kind_id {} rather than the \
12319 {char_literal} the operand arm lists — an alias the arm cannot see",
12320 node.kind_id()
12321 );
12322 literals += 1;
12323 }
12324 if chain
12325 .last()
12326 .is_none_or(|parent| parent.kind_id() != char_literal)
12327 {
12328 return;
12329 }
12330 children += 1;
12331 // `_with_code` is the spelling `compute_halstead`
12332 // calls. The default forwards to the byte-less form,
12333 // so today the two agree for every C-family
12334 // language — which is exactly why asking the wrong
12335 // one would read as correct right up until one of
12336 // these four grew an override (grammar-dispatch
12337 // section 7).
12338 assert!(
12339 matches!(
12340 L::get_op_type_with_code(
12341 node,
12342 source.as_bytes(),
12343 Ancestors::known(chain)
12344 ),
12345 TokenRole::Unknown
12346 ),
12347 "{label}: `{}` inside a character literal is classified, so the \
12348 literal now double-counts against its wrapper",
12349 node.kind()
12350 );
12351 });
12352 }
12353 // Ten literals across the two fixtures; each holds two
12354 // delimiters plus at least one payload leaf, and `'ab'` two.
12355 assert_eq!(literals, 10, "{label}: fixtures lost a character literal");
12356 assert_eq!(children, 31, "{label}: fixtures lost a literal's internals");
12357 }
12358
12359 check::<CCode>(C::CharLiteral as u16, "c");
12360 check::<CppCode>(Cpp::CharLiteral as u16, "cpp");
12361 check::<MozcppCode>(Mozcpp::CharLiteral as u16, "mozcpp");
12362 check::<ObjcCode>(Objc::CharLiteral as u16, "objc");
12363 }
12364
12365 /// #1361 fixture pairing the two spellings of a member access whose
12366 /// receiver is the enclosing object: `this->x` and `p->x`.
12367 ///
12368 /// The pairing is the point, not decoration. `field_expression` is
12369 /// `<receiver> -> <field>`, so the two lines are the same AST shape
12370 /// and must bill the same *kinds* of thing — a receiver operand, the
12371 /// `->` operator, a field operand. Before the fix `this` was in
12372 /// neither arm, so the two shapes scored differently and `m1` billed
12373 /// a `->` with only one operand around it.
12374 ///
12375 /// Plain C++ that the mozcpp fork parses identically, so one source
12376 /// proves the same thing about both clones.
12377 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
12378 const CPP_THIS_RECEIVER_PARITY: &str = "struct S {
12379 int x;
12380 int m1() { return this->x; }
12381 int m2(S* p) { return p->x; }
12382 };";
12383
12384 /// #1361 fixture holding one representative position per kind of
12385 /// container the pinned `tree-sitter-cpp` can put a `this` in: the
12386 /// `->` receiver, a dereference inside a parenthesised expression, a
12387 /// bare return value, both lambda-capture spellings (`[this]` and
12388 /// `[=, this]`), a `decltype` trailing return type, a constructor
12389 /// body following a member-initialiser list, and a call argument.
12390 ///
12391 /// Representative, not exhaustive — `delete this;`, `throw this;`,
12392 /// `[&, this]`, a default member initialiser and a `this->template
12393 /// f<int>()` all parse cleanly too. Every one of them is the same
12394 /// childless `{this:215}` leaf under an unclassified wrapper, which
12395 /// is precisely why the arm needs no guard and why enumerating them
12396 /// would add rows without adding a distinct path (grammar-dispatch
12397 /// section 11).
12398 ///
12399 /// C++23's explicit object parameter (`int g(this S&& self)`) is
12400 /// deliberately absent: the pinned grammar cannot parse it and emits
12401 /// `type_identifier "this"` plus an `ERROR`, which
12402 /// `for_each_node_with_chain` rejects outright. That spelling is
12403 /// already an operand through `TypeIdentifier`, so the construct is
12404 /// unaffected by this arm either way.
12405 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
12406 const CPP_THIS_POSITIONS: &str = "struct S {
12407 int x;
12408 void g(S*);
12409 int arrow() { return this->x; }
12410 int deref() { return (*this).x; }
12411 S* ret() { return this; }
12412 void cap() { auto l = [this]() { return x; }; }
12413 void cap2() { auto l = [=, this]() { return x; }; }
12414 auto trail() -> decltype(this->x) { return x; }
12415 S(int v) : x(v) { this->x = v; }
12416 void call() { g(this); }
12417 };";
12418
12419 /// Asserts `CPP_THIS_RECEIVER_PARITY` for one C++-family language,
12420 /// through the metrics store *and* the text-keyed `--ops` store
12421 /// (the lesson-4 invariant `n2 == len(dedupe(ops.operands))`).
12422 ///
12423 /// The `--ops` half pins *which text* the new operand is billed
12424 /// under, which the counts alone cannot see: billing the enclosing
12425 /// `field_expression` instead of the `this` leaf would hold `n2` at
12426 /// 6 while the vocabulary silently became `this->x`.
12427 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
12428 #[track_caller]
12429 fn assert_this_receiver_parity<T: crate::MetricSuite>(file: &str, label: &str) {
12430 // Operators, keyed by kind_id except the text-keyed primitives:
12431 // `{` x3 (class body, `m1`, `m2`), `int` x3, `;` x4 (the field,
12432 // the two returns, the struct terminator), `(` x2, `return` x2,
12433 // `->` x2, the one `*` of `S* p`, and `struct` (#1552). #695
12434 // dropped every closing delimiter. n1 = 8, N1 = 18.
12435 //
12436 // Operands, keyed by source text: `S` x2 (the name and the
12437 // parameter type), `x` x3, `m1`, `m2`, `p` x2, and — the fix —
12438 // `this` x1. n2 = 6, N2 = 10. Before #1361: n2 5, N2 9, with
12439 // every operator count identical, which is the whole claim.
12440 let counts = format!("{label}: this->x and p->x are the same shape");
12441 assert_halstead_counts::<T>(CPP_THIS_RECEIVER_PARITY, file, [8, 18, 6, 10], &counts);
12442 assert_ops_operands::<T>(
12443 CPP_THIS_RECEIVER_PARITY,
12444 file,
12445 6,
12446 vec!["S", "x", "m1", "m2", "p", "this"],
12447 );
12448 }
12449
12450 /// Every C++ overloaded-operator name the grammar spells as a token
12451 /// of its own, plus the `co_await` expression and the conversion
12452 /// operator's `operator` keyword (#1296).
12453 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
12454 const CPP_OPERATOR_NAMES: &str =
12455 "struct S { int operator[](int); void operator()(); bool operator co_await(); };
12456long operator\"\"_x(unsigned long long);
12457S::operator bool() const;
12458S s; int a = s[1]; auto t = co_await s;
12459";
12460
12461 /// Asserts `CPP_OPERATOR_NAMES` bills each operator name, keyword and
12462 /// `co_await` as an operator under parser `T`.
12463 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
12464 #[track_caller]
12465 fn assert_cpp_operator_names<T: crate::MetricSuite>(label: &str) {
12466 // Operators (n1 = 18, N1 = 41): `operator` 5 (four
12467 // `operator_name`s and the `operator_cast`); the names `[]` 1,
12468 // `()` 1, `""` 1; `co_await` 2 (the name and the expression);
12469 // the `(` of a parameter list 5; `;` 9; `int` 3; `long` 3;
12470 // `bool` 2; `=` 2; `{` 1; `void` 1; `unsigned` 1; `::` 1; the
12471 // subscript `[` 1; `struct` 1 (#1552); the trailing `const` 1
12472 // (#1578). `auto` is in no arm.
12473 //
12474 // Operands (n2 = 6, N2 = 9): `s` 3, `S` 2 (the qualifier in
12475 // `S::operator bool` is a namespace identifier, which #1096
12476 // leaves unbilled), `_x`, `a`, `1`, `t`.
12477 //
12478 // Before #1296: (12, 30, 6, 9) with `struct`, the five operator
12479 // kinds billed nowhere.
12480 assert_halstead_counts::<T>(CPP_OPERATOR_NAMES, "ops.cpp", [18, 41, 6, 9], label);
12481
12482 // `operator[]` / `operator()` are kinds of their own, so they sit
12483 // beside the subscript `[` and the parameter-list `(` as two more
12484 // operators, each rendered as the operator it names so that every
12485 // listed entry is distinct.
12486 let ops = ops_of::<T>(CPP_OPERATOR_NAMES, "ops.cpp");
12487 for (name, entries) in [
12488 ("operator", 1),
12489 ("co_await", 1),
12490 ("\"\"", 1),
12491 ("operator[]", 1),
12492 ("operator()", 1),
12493 ("[]", 1),
12494 ("()", 1),
12495 ] {
12496 assert_eq!(
12497 ops.operators.iter().filter(|o| *o == name).count(),
12498 entries,
12499 "{label}: `{name}` must be {entries} operator entry; operators were {:?}",
12500 ops.operators
12501 );
12502 assert!(
12503 !ops.operands.iter().any(|o| o == name),
12504 "{label}: `{name}` must not be an operand; operands were {:?}",
12505 ops.operands
12506 );
12507 }
12508 }
12509
12510 /// Regression for #1296: a C++ operator name is a Halstead operator,
12511 /// as `operator +`'s `+` already was. One row per clone, so reverting
12512 /// either arm fails its own row (grammar-dispatch section 11); Mozcpp
12513 /// owns no file extension, so this row is its only coverage.
12514 #[cfg(all(feature = "cpp", feature = "mozcpp"))]
12515 #[test]
12516 fn cpp_operator_names_are_operators() {
12517 assert_cpp_operator_names::<CppParser>("cpp");
12518 assert_cpp_operator_names::<MozcppParser>("mozcpp");
12519 }
12520
12521 /// Asserts a coroutine's `co_yield` and `co_return` keywords bill as
12522 /// operators under parser `T`, as `return` does (#1547).
12523 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
12524 #[track_caller]
12525 fn assert_cpp_coroutine_keywords<T: crate::MetricSuite>(label: &str) {
12526 let source = "T gen() { co_yield 1; co_yield 2; co_return 3; }";
12527 // Operators (n1 = 5, N1 = 8): `()` 1, `{}` 1, `co_yield` 2,
12528 // `co_return` 1, `;` 3. Operands (n2 = 5, N2 = 5): `T`, `gen`,
12529 // `1`, `2`, `3`. Before #1547: (3, 5, 5, 5), both keywords
12530 // billed nowhere.
12531 assert_halstead_counts::<T>(source, "foo.cpp", [5, 8, 5, 5], label);
12532 let ops = ops_of::<T>(source, "foo.cpp");
12533 for keyword in ["co_yield", "co_return"] {
12534 assert!(
12535 ops.operators.iter().any(|o| o == keyword),
12536 "{label}: `{keyword}` must be an operator; operators were {:?}",
12537 ops.operators
12538 );
12539 }
12540 }
12541
12542 /// Regression for #1546: a zero-width node is billed in neither
12543 /// Halstead half. Error recovery inserts one for a token the source
12544 /// lacks, usually flagged MISSING, and every getter that classifies
12545 /// the kind billed it — an identifier as the empty-string operand.
12546 /// One fixture per language, each a parse on which recovery inserts
12547 /// an operand-classified zero-width node; before the fix every row
12548 /// billed `""`. C#'s and Elixir's are not flagged MISSING, which is
12549 /// why the guard keys on width: an `is_missing()` guard leaves those
12550 /// two rows billing `""`. Each row names its zero-width operand's
12551 /// kind and flag, so the two unflagged rows keep discriminating. The
12552 /// rows pin no counts, since the recovery trees are the grammars' to
12553 /// change (grammar-dispatch §6); the operator half, an inserted `;` or
12554 /// `)`, is anchored by `cpp_missing_tokens_in_valid_code_are_not_billed`.
12555 #[cfg(any(
12556 feature = "bash",
12557 feature = "c",
12558 feature = "cpp",
12559 feature = "csharp",
12560 feature = "elixir",
12561 feature = "go",
12562 feature = "groovy",
12563 feature = "irules",
12564 feature = "java",
12565 feature = "javascript",
12566 feature = "kotlin",
12567 feature = "lua",
12568 feature = "mozcpp",
12569 feature = "mozjs",
12570 feature = "objc",
12571 feature = "perl",
12572 feature = "php",
12573 feature = "python",
12574 feature = "ruby",
12575 feature = "rust",
12576 feature = "tcl",
12577 feature = "typescript",
12578 ))]
12579 #[test]
12580 fn zero_width_nodes_bill_neither_halstead_half() {
12581 // `(language, file, source, kind, is_missing)`: the last two name
12582 // the zero-width operand each fixture's recovery inserts, so a
12583 // grammar bump that recovers some other way fails the row rather
12584 // than passing it on an unrelated zero-width node.
12585 let cases: &[(LANG, &str, &str, &str, bool)] = &[
12586 #[cfg(feature = "bash")]
12587 (LANG::Bash, "foo.sh", "echo a $( )\n", "word", true),
12588 #[cfg(feature = "c")]
12589 (
12590 LANG::C,
12591 "foo.c",
12592 "int x; void () { x = 1; }\n",
12593 "identifier",
12594 true,
12595 ),
12596 #[cfg(feature = "cpp")]
12597 (
12598 LANG::Cpp,
12599 "foo.cpp",
12600 "void f(S *s) {\n s->~D<F>();\n}\n",
12601 "identifier",
12602 true,
12603 ),
12604 #[cfg(feature = "csharp")]
12605 (
12606 LANG::Csharp,
12607 "foo.cs",
12608 "class A { int = 1; }\n",
12609 "identifier",
12610 false,
12611 ),
12612 #[cfg(feature = "elixir")]
12613 (LANG::Elixir, "foo.ex", "x = a.\n", "identifier", false),
12614 #[cfg(feature = "go")]
12615 (
12616 LANG::Go,
12617 "foo.go",
12618 "package m\nfunc f() { a.() }\n",
12619 "type_identifier",
12620 true,
12621 ),
12622 #[cfg(feature = "groovy")]
12623 (
12624 LANG::Groovy,
12625 "foo.groovy",
12626 "def x = a.\n",
12627 "identifier",
12628 true,
12629 ),
12630 #[cfg(feature = "irules")]
12631 (
12632 LANG::Irules,
12633 "foo.irule",
12634 "when HTTP_REQUEST { set x [expr {1 + }] }\n",
12635 "number",
12636 true,
12637 ),
12638 #[cfg(feature = "java")]
12639 (
12640 LANG::Java,
12641 "Foo.java",
12642 "class A { int = 1; }\n",
12643 "identifier",
12644 true,
12645 ),
12646 #[cfg(feature = "javascript")]
12647 (
12648 LANG::Javascript,
12649 "foo.js",
12650 "let {a: } = b;\n",
12651 "identifier",
12652 true,
12653 ),
12654 #[cfg(feature = "kotlin")]
12655 (
12656 LANG::Kotlin,
12657 "foo.kt",
12658 "fun f(: Int) {}\n",
12659 "identifier",
12660 true,
12661 ),
12662 #[cfg(feature = "lua")]
12663 (LANG::Lua, "foo.lua", "x = a.\n", "identifier", true),
12664 #[cfg(feature = "mozcpp")]
12665 (
12666 LANG::Mozcpp,
12667 "foo.cpp",
12668 "void f(S *s) {\n s->~D<F>();\n}\n",
12669 "identifier",
12670 true,
12671 ),
12672 #[cfg(feature = "mozjs")]
12673 (
12674 LANG::Mozjs,
12675 "foo.jsm",
12676 "let {a: } = b;\n",
12677 "identifier",
12678 true,
12679 ),
12680 #[cfg(feature = "objc")]
12681 (
12682 LANG::Objc,
12683 "foo.m",
12684 "@interface A\n- (void)f:;\n@end\n",
12685 "identifier",
12686 true,
12687 ),
12688 #[cfg(feature = "perl")]
12689 (LANG::Perl, "foo.pl", "sub { $x = ; }\n", "identifier", true),
12690 #[cfg(feature = "php")]
12691 (LANG::Php, "foo.php", "<?php $x = $a->;\n", "name", true),
12692 #[cfg(feature = "python")]
12693 (
12694 LANG::Python,
12695 "foo.py",
12696 "for in x:\n pass\n",
12697 "identifier",
12698 true,
12699 ),
12700 #[cfg(feature = "ruby")]
12701 (LANG::Ruby, "foo.rb", "x = a.\n", "identifier", true),
12702 #[cfg(feature = "rust")]
12703 (
12704 LANG::Rust,
12705 "foo.rs",
12706 "fn f() { let x = a.; }\n",
12707 "field_identifier",
12708 true,
12709 ),
12710 #[cfg(feature = "tcl")]
12711 (LANG::Tcl, "foo.tcl", "puts [expr {1 + }]\n", "number", true),
12712 #[cfg(feature = "typescript")]
12713 (
12714 LANG::Typescript,
12715 "foo.ts",
12716 "let {a: } = b;\n",
12717 "identifier",
12718 true,
12719 ),
12720 #[cfg(feature = "typescript")]
12721 (LANG::Tsx, "foo.tsx", "let {a: } = b;\n", "identifier", true),
12722 ];
12723 crate::test_support::assert_fixtures_present(cases);
12724
12725 let mut billed = Vec::new();
12726 for (lang, file, source, kind, missing) in cases {
12727 let ast = crate::test_support::parse_named(*lang, file, source);
12728 assert!(
12729 ast.root_node().preorder().any(|node| {
12730 node.start_byte() == node.end_byte()
12731 && node.kind() == *kind
12732 && node.is_missing() == *missing
12733 }),
12734 "{lang:?}: `{source}` no longer recovers with a zero-width `{kind}` \
12735 (is_missing: {missing}); find another fixture"
12736 );
12737 let ops = ast.ops().expect("ops walk must yield a top-level Ops");
12738 assert!(
12739 !ops.operands.is_empty(),
12740 "{lang:?}: `{source}` billed no operand at all"
12741 );
12742 billed.extend(ops.operands.iter().any(String::is_empty).then_some(*lang));
12743 }
12744 assert!(
12745 billed.is_empty(),
12746 "a zero-width node was billed as the empty operand in {billed:?}"
12747 );
12748 }
12749
12750 /// Asserts two valid C++ programs that tree-sitter-cpp 0.23.4 can only
12751 /// parse by inserting MISSING tokens bill only the tokens written
12752 /// (#1546), under parser `T`. Both come from the DeepSpeech corpus.
12753 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
12754 #[track_caller]
12755 fn assert_cpp_missing_tokens_unbilled<T: crate::MetricSuite>(label: &str) {
12756 // `openfst/dfs-visit.h:75`: an explicit destructor call through a
12757 // template name. Recovery reads `s->~D<F` as comparisons and
12758 // inserts a MISSING identifier inside the trailing `()`.
12759 // Operators (n1 = 9, N1 = 10): `void`, `()` 2, `*`, `{}`, `->`,
12760 // `~`, `<`, `>`, `;`. Operands (n2 = 5, N2 = 6): `f`, `S`, `s` 2,
12761 // `D`, `F`. Before #1546: (9, 10, 6, 7), the extra being `""`.
12762 let destructor = "void f(S *s) {\n s->~D<F>();\n}\n";
12763 assert_halstead_counts::<T>(destructor, "foo.cpp", [9, 10, 5, 6], label);
12764 let ops = ops_of::<T>(destructor, "foo.cpp");
12765 assert_eq!(ops.operands, ["D", "F", "S", "f", "s"], "{label}");
12766
12767 // `kenlm/lm/search_trie.cc:286`: a `typename` functional cast
12768 // used as a statement. Recovery inserts a MISSING `)` after
12769 // `quant_` and a MISSING `;` after `order`. Operators (n1 = 9,
12770 // N1 = 11): `void`, `()` 3, `{}`, `::`, `,`, `-`, `.`, the one
12771 // written `;`, and `typename` (#1557). Operands (n2 = 7, N2 = 7):
12772 // `f`, `MiddlePointer`, `quant_`, `order`, `2`, `Write`, `w`.
12773 // Before #1546 the MISSING `;` was a second `;`.
12774 let cast = "void f() {\n typename Quant::MiddlePointer(quant_, order - 2).Write(w);\n}\n";
12775 assert_halstead_counts::<T>(cast, "foo.cpp", [9, 11, 7, 7], label);
12776 }
12777
12778 /// Regression for #1546 on the two C++ clones. Mozcpp owns no file
12779 /// extension, so this row is its only coverage.
12780 #[cfg(all(feature = "cpp", feature = "mozcpp"))]
12781 #[test]
12782 fn cpp_missing_tokens_in_valid_code_are_not_billed() {
12783 assert_cpp_missing_tokens_unbilled::<CppParser>("cpp");
12784 assert_cpp_missing_tokens_unbilled::<MozcppParser>("mozcpp");
12785 }
12786
12787 /// Regression for #1547: `co_return` and `co_yield` are Halstead
12788 /// operators. One row per clone, so reverting either arm fails its
12789 /// own row; Mozcpp owns no file extension, so this row is its only
12790 /// coverage.
12791 #[cfg(all(feature = "cpp", feature = "mozcpp"))]
12792 #[test]
12793 fn cpp_coroutine_keywords_are_operators() {
12794 assert_cpp_coroutine_keywords::<CppParser>("cpp");
12795 assert_cpp_coroutine_keywords::<MozcppParser>("mozcpp");
12796 }
12797
12798 /// Regression for #1361: a C++ `this` is a Halstead operand.
12799 ///
12800 /// `Cpp::This` / `Mozcpp::This` were in neither arm of
12801 /// `CppCode::get_op_type` / `MozcppCode::get_op_type`, so a `this`
12802 /// contributed *nothing* — not an operator and not an operand —
12803 /// while eleven of the fourteen languages here bill their
12804 /// self-reference as an operand. Same shape as #1316's character
12805 /// literals, and the inverse of the #1351-#1355 over-counts.
12806 ///
12807 /// Each language asserts separately over the same source rather than
12808 /// sharing one call, so reverting one clone's arm fails that row
12809 /// alone (grammar-dispatch section 11). Mozcpp owns no file
12810 /// extension, so no integration snapshot reaches its clone; this row
12811 /// and `cpp_and_mozcpp_agree_on_this` in `tests/parity/` are the
12812 /// whole coverage that arm has.
12813 #[cfg(all(feature = "cpp", feature = "mozcpp"))]
12814 #[test]
12815 fn cpp_this_is_an_operand() {
12816 assert_this_receiver_parity::<CppParser>("this.cpp", "cpp");
12817 assert_this_receiver_parity::<MozcppParser>("this.cpp", "mozcpp");
12818 }
12819
12820 /// The `This` arm is unconditional: every `this` in
12821 /// `CPP_THIS_POSITIONS` classifies as an operand whatever encloses
12822 /// it (#1361).
12823 ///
12824 /// The counts in `cpp_this_is_an_operand` are measured on one
12825 /// position only. An arm reached through a parent-scoped guard —
12826 /// the shape the neighbouring `RawStringLiteral` `LPAREN` arm uses,
12827 /// and the shape a reviewer might reasonably add here — would pass
12828 /// that test and still drop `this` in a lambda capture or a
12829 /// `decltype`. This walk is what makes the arm's unconditional
12830 /// reach a measurement rather than an assumption.
12831 ///
12832 /// Named for the *property* rather than for the fixture: the fixture
12833 /// samples one position per container kind and is deliberately not
12834 /// exhaustive (see `CPP_THIS_POSITIONS`), so a name promising "every
12835 /// position" would claim more than it checks.
12836 #[cfg(all(feature = "cpp", feature = "mozcpp"))]
12837 #[test]
12838 fn cpp_this_is_an_operand_regardless_of_position() {
12839 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
12840 fn check<L: LanguageInfo + Getter>(label: &str) {
12841 let mut seen = 0_usize;
12842 for_each_node_with_chain::<L>(CPP_THIS_POSITIONS.as_bytes(), |node, chain| {
12843 if node.kind() != "this" {
12844 return;
12845 }
12846 seen += 1;
12847 // `_with_code` is the spelling `compute_halstead` calls.
12848 // The default forwards to the byte-less form, so the two
12849 // agree for both languages today — which is exactly why
12850 // asking the wrong one would read as correct right up
12851 // until one of them grew an override
12852 // (grammar-dispatch section 7).
12853 assert!(
12854 matches!(
12855 L::get_op_type_with_code(
12856 node,
12857 CPP_THIS_POSITIONS.as_bytes(),
12858 Ancestors::known(chain)
12859 ),
12860 TokenRole::Operand
12861 ),
12862 "{label}: a `this` under `{}` is not an operand",
12863 chain.last().map_or("<root>", Node::kind)
12864 );
12865 });
12866 // One per position: `->` receiver, `(*this)`, bare return,
12867 // `[this]`, `[=, this]`, `decltype(this->x)`, the
12868 // constructor body, and the call argument.
12869 assert_eq!(seen, 8, "{label}: fixture lost a `this` position");
12870 }
12871
12872 check::<CppCode>("cpp");
12873 check::<MozcppCode>("mozcpp");
12874 }
12875
12876 /// Pins the two grammar facts the #1361 operand arm rests on, across
12877 /// both fixtures and both languages.
12878 ///
12879 /// * **Grammar-dispatch section 1.** Every node the grammar spells
12880 /// `this` carries the one `kind_id` the arm lists. That is the
12881 /// weaker half of the alias evidence — an alias arises in a
12882 /// *different* syntactic position, which no fixture can enumerate.
12883 /// The strong half is that these generated enums carry
12884 /// numeric-suffix aliases in quantity (`FunctionDefinition2..4`,
12885 /// `QualifiedIdentifier2..4`, `LPAREN2`, `Try2`, `GT2` in this one
12886 /// file) and neither spells a `This2` — `This = 215` is the sole
12887 /// variant rendering `"this"` in `language_cpp.rs` and
12888 /// `language_mozcpp.rs`. This loop is what notices if a grammar
12889 /// bump changes that under an existing fixture.
12890 /// * **Grammar-dispatch section 5.** A `this` is a childless leaf and
12891 /// no node containing it is classified. That is both halves of the
12892 /// double-count question: nothing below it can be billed a second
12893 /// time, and no `field_expression` / `pointer_expression` /
12894 /// `lambda_capture_specifier` / `argument_list` wrapper bills the
12895 /// same source text from above.
12896 #[cfg(all(feature = "cpp", feature = "mozcpp"))]
12897 #[test]
12898 fn cpp_this_is_a_childless_unaliased_leaf() {
12899 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
12900 fn check<L: LanguageInfo + Getter>(this: u16, label: &str) {
12901 let mut seen = 0_usize;
12902 for source in [CPP_THIS_RECEIVER_PARITY, CPP_THIS_POSITIONS] {
12903 for_each_node_with_chain::<L>(source.as_bytes(), |node, chain| {
12904 if node.kind() != "this" {
12905 return;
12906 }
12907 seen += 1;
12908 assert_eq!(
12909 node.kind_id(),
12910 this,
12911 "{label}: a `this` carries kind_id {} rather than the {this} the \
12912 operand arm lists — an alias the arm cannot see",
12913 node.kind_id()
12914 );
12915 assert_eq!(
12916 node.child_count(),
12917 0,
12918 "{label}: `this` grew children, which the operand arm would now \
12919 double-count against"
12920 );
12921 assert!(!chain.is_empty(), "{label}: a `this` cannot be the root");
12922 // Every ancestor, not just the parent. The claim is
12923 // that *no node containing* a `this` is classified,
12924 // and `(*this)` alone puts two wrappers between the
12925 // leaf and the nearest classified node — checking
12926 // only `chain.last()` would assert something
12927 // narrower than the comment on the arm promises.
12928 // `chain` is the *node's* ancestry, so the ancestor
12929 // at `i` has `chain[..i]` for its own.
12930 for (i, ancestor) in chain.iter().enumerate() {
12931 assert!(
12932 matches!(
12933 L::get_op_type_with_code(
12934 ancestor,
12935 source.as_bytes(),
12936 Ancestors::known(&chain[..i])
12937 ),
12938 TokenRole::Unknown
12939 ),
12940 "{label}: `{}` contains a `this` and is itself classified, so the \
12941 reference now counts twice",
12942 ancestor.kind()
12943 );
12944 }
12945 });
12946 }
12947 // One `this` in the parity fixture, eight in the positions
12948 // fixture. Asserted so a fixture that stopped containing one
12949 // cannot make this test pass having checked nothing.
12950 assert_eq!(seen, 9, "{label}: fixtures lost a `this`");
12951 }
12952
12953 check::<CppCode>(Cpp::This as u16, "cpp");
12954 check::<MozcppCode>(Mozcpp::This as u16, "mozcpp");
12955 }
12956
12957 /// Builds a `HalsteadMaps` from explicit occurrence counts.
12958 ///
12959 /// The per-language tests above reach these maps only through a
12960 /// parse, which cannot produce a *chosen* overlap between a child
12961 /// and its parent — the cases `merge` exists to get right.
12962 fn halstead_maps_of<'a>(
12963 operators: &[(u16, u64)],
12964 primitive_operators: &[(&'a [u8], u64)],
12965 operands: &[(&'a [u8], u64)],
12966 ) -> HalsteadMaps<'a> {
12967 HalsteadMaps {
12968 operators: operators.iter().copied().collect(),
12969 primitive_operators: primitive_operators.iter().copied().collect(),
12970 operands: operands.iter().copied().collect(),
12971 }
12972 }
12973
12974 /// `HalsteadMaps::operators` must stay on the crate's integer hasher.
12975 ///
12976 /// Swapping a hasher moves no metric value, so every other test in
12977 /// this file passes just as well with #1108 reverted. Both halves
12978 /// here are needed: the typed binding stops compiling if the field
12979 /// goes back to a default-hasher `HashMap`, and the `type_name`
12980 /// comparison still fails at runtime if `IntKeyHashMap` itself is
12981 /// ever redefined to wrap `RandomState`.
12982 ///
12983 /// The two text-keyed maps are pinned to SipHash in the same test,
12984 /// because moving *them* would be a regression rather than an
12985 /// optimisation. `crate::int_hash`'s module doc is the single place
12986 /// that argues why analysed source text does not qualify.
12987 #[test]
12988 fn halstead_operator_map_uses_the_int_key_hasher() {
12989 use std::any::{type_name, type_name_of_val};
12990 use std::hash::BuildHasherDefault;
12991
12992 use crate::int_hash::IntKeyHasher;
12993
12994 let maps = HalsteadMaps::new();
12995
12996 let operators: &IntKeyHashMap<u16, u64> = &maps.operators;
12997 assert_eq!(
12998 type_name_of_val(operators.hasher()),
12999 type_name::<BuildHasherDefault<IntKeyHasher>>(),
13000 "the kind_id-keyed operator map must use the int_hash hasher"
13001 );
13002
13003 let siphash = type_name::<std::collections::hash_map::RandomState>();
13004 assert_eq!(
13005 type_name_of_val(maps.operands.hasher()),
13006 siphash,
13007 "operand keys come from the analysed source, so the keyed hash \
13008 is what stops a crafted file from flooding this map"
13009 );
13010 assert_eq!(
13011 type_name_of_val(maps.primitive_operators.hasher()),
13012 siphash,
13013 "primitive-operator keys come from the analysed source, so the \
13014 keyed hash is what stops a crafted file from flooding this map"
13015 );
13016 }
13017
13018 /// `merge` sums overlapping keys and adopts disjoint ones, in all
13019 /// three maps, and `finalize` reads the union back as n1/N1/n2/N2.
13020 ///
13021 /// Every count differs from every other and none is zero, so a
13022 /// dropped key, an overwrite where an addition belongs, or a map
13023 /// crossed with its neighbour all change the totals.
13024 #[test]
13025 fn halstead_maps_merge_sums_overlaps_and_adopts_disjoint_keys() {
13026 let mut parent = halstead_maps_of(
13027 &[(1, 2), (2, 3)],
13028 &[(b"int", 1)],
13029 &[(b"alpha", 4), (b"beta", 7)],
13030 );
13031 let child = halstead_maps_of(
13032 &[(2, 5), (7, 11)],
13033 &[(b"double", 13)],
13034 &[(b"alpha", 17), (b"gamma", 19)],
13035 );
13036
13037 parent.merge(&child);
13038
13039 // expected: operators {1: 2, 2: 3+5, 7: 11}; primitives
13040 // {int: 1, double: 13}; operands {alpha: 4+17, beta: 7,
13041 // gamma: 19}.
13042 assert_eq!(
13043 parent,
13044 halstead_maps_of(
13045 &[(1, 2), (2, 8), (7, 11)],
13046 &[(b"int", 1), (b"double", 13)],
13047 &[(b"alpha", 21), (b"beta", 7), (b"gamma", 19)],
13048 )
13049 );
13050
13051 let mut stats = Stats::default();
13052 parent.finalize(&mut stats);
13053 // expected: n1 = 3 kind ids + 2 primitives; N1 = (2+8+11) +
13054 // (1+13); n2 = 3 texts; N2 = 21+7+19.
13055 assert_eq!(stats.unique_operators(), 5);
13056 assert_eq!(stats.total_operators(), 35);
13057 assert_eq!(stats.unique_operands(), 3);
13058 assert_eq!(stats.total_operands(), 47);
13059 }
13060
13061 /// Merging an empty child leaves the parent untouched.
13062 ///
13063 /// A space with no operators or operands is the common case for a
13064 /// leaf getter or an empty function body, and `finalize` runs on
13065 /// the parent afterwards either way.
13066 #[test]
13067 fn halstead_maps_merge_of_empty_child_is_a_no_op() {
13068 let mut parent = halstead_maps_of(&[(3, 5)], &[(b"char", 2)], &[(b"delta", 9)]);
13069 let before = parent.clone();
13070
13071 parent.merge(&HalsteadMaps::new());
13072
13073 assert_eq!(parent, before);
13074
13075 let mut stats = Stats::default();
13076 parent.finalize(&mut stats);
13077 // expected: n1 = 1 kind id + 1 primitive; N1 = 5 + 2; n2 = 1;
13078 // N2 = 9.
13079 assert_eq!(stats.unique_operators(), 2);
13080 assert_eq!(stats.total_operators(), 7);
13081 assert_eq!(stats.unique_operands(), 1);
13082 assert_eq!(stats.total_operands(), 9);
13083 }
13084
13085 /// Folding a chain of nested spaces bottom-up must reach the union
13086 /// of every level, re-merging already-merged maps on the way up.
13087 ///
13088 /// This is what `spaces.rs` and `ops.rs` actually do: each space is
13089 /// merged into its parent as the walk pops it, so by the time the
13090 /// root sees a grandchild's counts they have already passed through
13091 /// one `merge`. The literal expectation below is what discriminates
13092 /// — the `nested == flat` cross-check on its own does not, because
13093 /// any entry-wise fold over the same levels agrees with itself
13094 /// however it is associated, including a broken one.
13095 #[test]
13096 fn halstead_maps_merge_folds_a_nested_chain() {
13097 let levels = [
13098 halstead_maps_of(&[(1, 1)], &[(b"int", 1)], &[(b"a", 1)]),
13099 halstead_maps_of(&[(1, 2), (2, 3)], &[], &[(b"a", 2), (b"b", 4)]),
13100 halstead_maps_of(&[(2, 5)], &[(b"long", 6)], &[(b"b", 7)]),
13101 halstead_maps_of(&[(3, 8)], &[(b"int", 9)], &[(b"c", 10)]),
13102 ];
13103
13104 // Bottom-up: the deepest level folds into its parent, that
13105 // result into *its* parent, and so on up to the root.
13106 let mut nested = levels[levels.len() - 1].clone();
13107 for level in levels.iter().rev().skip(1) {
13108 let mut outer = level.clone();
13109 outer.merge(&nested);
13110 nested = outer;
13111 }
13112
13113 // Flat: every level merged directly into the root.
13114 let mut flat = levels[0].clone();
13115 for level in &levels[1..] {
13116 flat.merge(level);
13117 }
13118
13119 // expected: every key summed across the four levels — operators
13120 // {1: 1+2, 2: 3+5, 3: 8}, primitives {int: 1+9, long: 6},
13121 // operands {a: 1+2, b: 4+7, c: 10}.
13122 assert_eq!(
13123 nested,
13124 halstead_maps_of(
13125 &[(1, 3), (2, 8), (3, 8)],
13126 &[(b"int", 10), (b"long", 6)],
13127 &[(b"a", 3), (b"b", 11), (b"c", 10)],
13128 )
13129 );
13130 assert_eq!(nested, flat);
13131
13132 let mut stats = Stats::default();
13133 nested.finalize(&mut stats);
13134 // expected: n1 = 3 kind ids + 2 primitives; N1 = (3+8+8) +
13135 // (10+6); n2 = 3 texts; N2 = 3+11+10.
13136 assert_eq!(stats.unique_operators(), 5);
13137 assert_eq!(stats.total_operators(), 35);
13138 assert_eq!(stats.unique_operands(), 3);
13139 assert_eq!(stats.total_operands(), 24);
13140 }
13141
13142 /// A `kind_id` at the top of the `u16` range must behave like any
13143 /// other key.
13144 ///
13145 /// The largest grammar in the workspace (`mozcpp`) tops out around
13146 /// 640 symbols, so nothing near `u16::MAX` occurs today — but the
13147 /// map is keyed by the raw id, and a dense-array representation
13148 /// (the shape #1108 considered and rejected) is exactly what such a
13149 /// key would break. Pinning it keeps that trade-off honest if the
13150 /// representation is ever revisited.
13151 #[test]
13152 fn halstead_maps_handle_the_full_kind_id_range() {
13153 let mut parent = halstead_maps_of(&[(0, 3), (u16::MAX, 5)], &[], &[]);
13154 parent.merge(&halstead_maps_of(&[(u16::MAX, 7)], &[], &[]));
13155
13156 let mut stats = Stats::default();
13157 parent.finalize(&mut stats);
13158 // expected: two distinct kind ids, occurrences 3 and 5+7.
13159 assert_eq!(stats.unique_operators(), 2);
13160 assert_eq!(stats.total_operators(), 15);
13161 }
13162}