Skip to main content

big_code_analysis/
ops.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(clippy::wildcard_imports, clippy::enum_glob_use)]
8
9use std::borrow::Cow;
10
11use crate::checker::Checker;
12use crate::error::MetricsError;
13use crate::getter::Getter;
14use crate::node::{Ancestors, FoldCache, Node, SiblingCache};
15use crate::spaces::{SpaceKind, line_span, push_children};
16
17use crate::halstead::{Halstead, HalsteadMaps};
18
19use crate::MetricSuite;
20
21/// All operands and operators of a space.
22#[derive(Debug, Clone)]
23pub struct Ops {
24    /// The name of a function space.
25    ///
26    /// For the top-level (file-level) `Ops` the value is whatever
27    /// `Source::name` the caller supplied to the [`crate::Ast::ops`]
28    /// seam — `Some` or `None`.
29    ///
30    /// For nested spaces, `None` means an error occurred in parsing the
31    /// name of the function space from the AST.
32    pub name: Option<String>,
33    /// `true` when [`Ops::name`] was produced by lossy conversion (the
34    /// original path contained non-UTF-8 bytes and was rendered using
35    /// U+FFFD replacement characters). The explicit-name
36    /// [`crate::Ast::ops`] seam never sets it, since a caller-supplied
37    /// `String` name is UTF-8 by construction, so it is always `false`
38    /// in current code paths. Retained as a wire field for forward
39    /// compatibility; skipped from JSON output when `false` so existing
40    /// schemas keep their shape.
41    pub name_was_lossy: bool,
42    /// The first line of a function space.
43    pub start_line: usize,
44    /// The last line of a function space.
45    pub end_line: usize,
46    /// The space kind.
47    pub kind: SpaceKind,
48    /// All subspaces contained in a function space.
49    pub spaces: Vec<Ops>,
50    /// The **distinct** operands of a space — the deduplicated Halstead
51    /// operand vocabulary (`n2`), one entry per unique operand, not every
52    /// occurrence. Sorted in byte-lexicographic order, so the same input
53    /// always yields the same sequence (#1091).
54    pub operands: Vec<String>,
55    /// The **distinct** operators of a space — the deduplicated Halstead
56    /// operator vocabulary (`n1`), one entry per unique operator, not
57    /// every occurrence. Sorted in byte-lexicographic order, so the same
58    /// input always yields the same sequence (#1091).
59    pub operators: Vec<String>,
60}
61
62// Space nesting is caller-controlled, so the compiler-generated `Drop`
63// glue would recurse once per level and abort the process on a deep tree
64// (#1056). See [`crate::recursion`].
65crate::recursion::impl_iterative_drop!(Ops, spaces);
66
67impl Ops {
68    /// Project this tree into its [`crate::wire::Ops`] form — the
69    /// plain, `Deserialize`-capable record that defines the serialized
70    /// shape.
71    #[must_use]
72    pub fn to_wire(&self) -> crate::wire::Ops {
73        crate::wire::Ops::from(self)
74    }
75
76    fn new<'a, T: Getter>(
77        node: &Node<'a>,
78        code: &[u8],
79        ancestors: Ancestors<'a, '_>,
80        kind: SpaceKind,
81    ) -> Self {
82        let (start_position, end_position) = line_span(node, kind);
83        // The top-level Unit's name is overwritten by `ops_inner` with the
84        // caller-supplied name before returning, so computing it here is
85        // wasted work. Non-top-level Unit spaces have no resolvable name, so
86        // leaving `None` matches the documented "could not be resolved"
87        // semantics rather than inventing the `<anonymous>` placeholder the
88        // default getter returns. Other kinds keep the AST-derived name.
89        // Mirrors the `SpaceKind::Unit` handling in `FuncSpace::new`.
90        let name = (kind != SpaceKind::Unit)
91            .then(|| T::get_func_space_name(node, code, ancestors).map(str::to_owned))
92            .flatten();
93        Self {
94            name,
95            name_was_lossy: false,
96            spaces: Vec::new(),
97            kind,
98            start_line: start_position,
99            end_line: end_position,
100            operators: Vec::new(),
101            operands: Vec::new(),
102        }
103    }
104}
105
106#[derive(Debug, Clone)]
107struct State<'a> {
108    ops: Ops,
109    halstead_maps: HalsteadMaps<'a>,
110}
111
112/// Pushes a synthetic `Unit` root onto the state stack when the grammar
113/// hands us a non-`Unit` root.
114///
115/// Mirrors [`crate::spaces::push_synthetic_unit_root`] on the metrics
116/// seam: some grammars (e.g. tree-sitter-lua / tree-sitter-mozcpp on
117/// unparseable input) return an `ERROR` root that is not classified as a
118/// function space, so without this push the walk would never open a
119/// frame and `ops_inner` would return [`MetricsError::EmptyRoot`] for an
120/// input where `metrics()` succeeds (issue #789). A `Unit` root needs no
121/// wrapper, so nothing is pushed in that case.
122fn push_synthetic_unit_root<T: MetricSuite>(
123    state_stack: &mut Vec<State>,
124    node: &Node,
125    code: &[u8],
126) {
127    // `Ancestors::unknown()`: `node` is the tree root here, so it has
128    // no ancestors to hand over either way.
129    if T::Getter::get_space_kind_with_code(node, code, Ancestors::unknown()) != SpaceKind::Unit {
130        state_stack.push(State {
131            ops: Ops::new::<T::Getter>(node, code, Ancestors::unknown(), SpaceKind::Unit),
132            halstead_maps: HalsteadMaps::new(),
133        });
134    }
135}
136
137// Space-kind classifications `ops_inner` has performed on this thread.
138//
139// The classification only ever reaches `Ops::new`, so running it on a
140// node that opens no space is work thrown away — and nothing about the
141// walk's *output* can tell the two apart. The counter is the
142// observable: it reads one per space after the lookup was moved inside
143// the `func_space` branch, and one per *node* before, which is what
144// makes hoisting it back out a test failure rather than a silent
145// regression (#1110).
146crate::observation::counter!(space_kind_lookups);
147
148/// Classifies a node that is about to open a function space.
149///
150/// Classification happens after the decision that a space opens, the
151/// same way [`crate::spaces::compute`]'s `open_func_space` does it
152/// (#522), and through the same source-aware classifier, so a space
153/// both seams open carries the same [`SpaceKind`] in either walk.
154/// The `_with_code` variant is what lets Elixir's macro-shaped
155/// `defmodule` / `def` declarations — plain `Call` nodes distinguished
156/// only by their target identifier text — come back as `Class` /
157/// `Function` rather than `Unknown` (#275, #1130).
158///
159/// The lookup is a per-language `match` on the node's kind for most
160/// grammars, but C#'s reaches a child scan for a bodied indexer or
161/// property and Elixir's reads the `Call` target text and scans the
162/// ancestor chain for an enclosing `quote` block, so it is not free on
163/// every node either.
164fn classify_space_kind<'a, T: MetricSuite>(
165    node: &Node<'a>,
166    code: &[u8],
167    ancestors: Ancestors<'a, '_>,
168) -> SpaceKind {
169    space_kind_lookups::record();
170    T::Getter::get_space_kind_with_code(node, code, ancestors)
171}
172
173/// Render a space's vocabulary: byte-lexicographically ordered, one
174/// owned `String` per distinct entry.
175///
176/// # Order
177///
178/// `HashMap`'s hasher is randomly seeded per instance, so key order
179/// differs between two runs — and even between two parses in one
180/// process. Without a canonical order the same input renders and
181/// serializes differently every time, which makes `bca ops` output
182/// undiffable and unusable as a cache key (#1091).
183///
184/// Byte-lexicographic, not first-appearance, order: `finalize` merges
185/// a child space's maps into its parent, so an insertion-ordered map
186/// would give the parent "what it saw directly, then whatever each
187/// child contributed" — a walk artifact that shifts when nesting
188/// changes. Sorting is also stable across platforms and hasher
189/// versions, which insertion order via a different map type is not.
190///
191/// # Why the sort runs before the `String`s exist
192///
193/// Every ancestor of a space re-renders that space's whole vocabulary:
194/// `finalize` merges each child's Halstead maps into its parent, so a
195/// parent's key set is a superset of every descendant's and an entry
196/// nested `D` spaces deep is rendered `D + 1` times. Sorting the
197/// borrowed keys first makes each swap a fat pointer rather than a
198/// 24-byte `String`, and — because the `String`s are then allocated in
199/// output order — it hands every later pass over them (the wire
200/// projection, serialization, the `dump_ops` tree) a heap laid out in
201/// the order it reads. Issue #1110 measured both effects.
202///
203/// # Non-UTF-8 keys
204///
205/// Tree-sitter sources are expected to be valid UTF-8; non-UTF-8 bytes
206/// are replaced with the Unicode replacement character to keep the entry
207/// visible (rather than silently dropping it or using a sentinel string
208/// that could collide with a real identifier). That rendering is not
209/// order-preserving — `b"\xffA"` sorts after `"\u{fffd}B"` by raw bytes
210/// and before it once both are rendered — so a vocabulary that actually
211/// lost bytes is re-sorted on the rendered text, which is the order the
212/// pre-#1110 render-then-sort code produced. Valid UTF-8, which is every
213/// key in practice, takes the single sort.
214fn sorted_vocabulary(mut keys: Vec<&[u8]>) -> Vec<String> {
215    keys.sort_unstable();
216    let mut lossy = false;
217    let mut rendered: Vec<String> = keys
218        .into_iter()
219        .map(|key| match String::from_utf8_lossy(key) {
220            Cow::Borrowed(text) => text.to_owned(),
221            Cow::Owned(text) => {
222                lossy = true;
223                text
224            }
225        })
226        .collect();
227    if lossy {
228        rendered.sort_unstable();
229    }
230    rendered
231}
232
233fn compute_operators_and_operands<T: MetricSuite>(state: &mut State) {
234    let maps = &state.halstead_maps;
235
236    // Primitive-type operators live in a second map (keyed by text rather
237    // than by token id), so the operator vocabulary is the concatenation
238    // of both key sets.
239    let operators = maps
240        .operators
241        .keys()
242        .map(|k| T::Getter::get_operator_id_as_str(*k).as_bytes())
243        .chain(maps.primitive_operators.keys().copied())
244        .collect();
245
246    state.ops.operators = sorted_vocabulary(operators);
247    state.ops.operands = sorted_vocabulary(maps.operands.keys().copied().collect());
248}
249
250/// Close up to `diff_level` open spaces, folding each into its parent.
251///
252/// Only the states this pops get their vocabularies computed. The
253/// bottom state is never popped here, so the root's vocabulary is built
254/// once by [`ops_inner`] after the final drain — computing it on every
255/// call would rebuild (and, since #1091, re-sort) the whole file's
256/// vocabulary once per level-drop in the walk, and every result but the
257/// last would be overwritten.
258fn finalize<T: MetricSuite>(state_stack: &mut Vec<State>, diff_level: usize) {
259    for _ in 0..diff_level {
260        if state_stack.len() < 2 {
261            break;
262        }
263        let mut state = state_stack
264            .pop()
265            .expect("state_stack verified to have len >= 2");
266        let last_state = state_stack
267            .last_mut()
268            .expect("state_stack verified to have len >= 1 after pop");
269
270        // Populate the child's ops from its HalsteadMaps before
271        // recording it as a sub-space of the parent.
272        compute_operators_and_operands::<T>(&mut state);
273
274        // Merge child's Halstead maps into parent and record child space.
275        last_state.halstead_maps.merge(&state.halstead_maps);
276        last_state.ops.spaces.push(state.ops);
277    }
278}
279
280/// Context the ops walk carries down the tree alongside each node.
281///
282/// A named pair rather than a `(usize, usize)`: the two counts advance
283/// on different events and swapping them is silent — `level` only moves
284/// at space boundaries while `depth` counts every AST step. Mirrors
285/// [`crate::spaces::compute`]'s `Walk`, minus the comment flag this walk
286/// has no use for.
287#[derive(Clone, Copy)]
288struct Walk {
289    /// Nesting level, used to close op-spaces on the way back up.
290    level: usize,
291    /// AST depth — the number of ancestors this node has, so the root
292    /// sits at `0`. Indexes the ancestor chain the walk maintains.
293    depth: usize,
294}
295
296/// Explicit-name core of the operator/operand walk backing the
297/// [`crate::Ast::ops`] `Source`-based seam. The top-level [`Ops::name`]
298/// is whatever the caller passes in `name`; `name_was_lossy` is left at
299/// its `false` default because an explicit `String` name is never lossy.
300/// Mirrors [`crate::spaces::metrics_inner`].
301pub(crate) fn ops_inner<T: MetricSuite>(
302    parser: &T,
303    name: Option<String>,
304) -> Result<Ops, MetricsError> {
305    // bca: suppress(abc) — the twin of `metrics_inner`'s single walk loop,
306    // which carries the same marker: each local and call it sets up keeps
307    // one walk invariant the two walks must share (chain, siblings, folds).
308    let code = parser.code();
309    let node = parser.root();
310    let mut cursor = node.cursor();
311    let mut stack = Vec::new();
312    // Ancestor chain of the node currently being visited, root first,
313    // maintained by the same truncate/push rule as
314    // `spaces::compute::metrics_inner` (#1084).
315    let mut chain: Vec<Node<'_>> = Vec::new();
316    // As in `metrics_inner`: the children of each wide node on `chain`,
317    // and the memoised ancestry folds, without which the Python
318    // parenthesised-docstring test climbs and re-descends a paren nest
319    // once per layer, quadratically in its depth.
320    let mut siblings = SiblingCache::default();
321    let folds = FoldCache::default();
322    let mut state_stack: Vec<State> = Vec::new();
323    let mut last_level = 0;
324
325    // Mirror `metrics_inner`: wrap a non-`Unit` (e.g. `ERROR`) root in a
326    // synthetic `Unit` frame so the walk always has a frame to populate.
327    // Without this, an `ERROR`-root parse drains the state stack and
328    // `ops_inner` returns `EmptyRoot` for inputs where `metrics()`
329    // succeeds (issue #789).
330    push_synthetic_unit_root::<T>(&mut state_stack, &node, code);
331
332    stack.push((node, Walk { level: 0, depth: 0 }));
333
334    while let Some((node, Walk { level, depth })) = stack.pop() {
335        chain.truncate(depth);
336
337        if level < last_level {
338            finalize::<T>(&mut state_stack, last_level - level);
339            last_level = level;
340        }
341
342        let ancestors = Ancestors::checked(&chain, &node)
343            .with_siblings(&siblings)
344            .with_folds(&folds);
345
346        // Same predicate `spaces::compute::metrics_inner` opens on, so
347        // the two walks agree on which nodes become spaces. The
348        // byte-less `is_func || is_func_space` this replaced could not
349        // see Elixir's macro-shaped declarations, which are `Call`
350        // nodes identified by their target text, so `bca ops` opened no
351        // space for a `defmodule` / `def` / `defp` / `defmacro` — only
352        // for `Source` and an explicit `fn … -> … end` (#1130).
353        let func_space = T::Checker::promotes_to_func_space_with_code(&node, code, ancestors);
354
355        let new_level = if func_space {
356            let kind = classify_space_kind::<T>(&node, code, ancestors);
357            let state = State {
358                ops: Ops::new::<T::Getter>(&node, code, ancestors, kind),
359                halstead_maps: HalsteadMaps::new(),
360            };
361            state_stack.push(state);
362            last_level = level + 1;
363            last_level
364        } else {
365            level
366        };
367
368        if let Some(state) = state_stack.last_mut() {
369            T::Halstead::compute(&node, code, ancestors, &mut state.halstead_maps);
370        }
371
372        chain.push(node);
373
374        // Shared with `metrics_inner` (issue #969): `push_children` is
375        // State-independent — it only moves the cursor over child nodes —
376        // so unlike the local `finalize` / `push_synthetic_unit_root`
377        // mirrors (which differ by `State` payload) it is reused directly
378        // rather than duplicated. The source-order-then-reverse ordering
379        // it encapsulates is load-bearing for suppression attribution.
380        let pushed = push_children(
381            &mut cursor,
382            &node,
383            Walk {
384                level: new_level,
385                depth: depth + 1,
386            },
387            &mut stack,
388        );
389        siblings.record(&node, depth, pushed.iter().rev().map(|(child, _)| *child));
390    }
391
392    finalize::<T>(&mut state_stack, usize::MAX);
393
394    // Reserved error path: `MetricsError::EmptyRoot` is unreachable
395    // today because the synthetic Unit push above (and every supported
396    // language's root being recognised as a `func_space`) keeps the
397    // state stack non-empty for every input, including ERROR-root,
398    // empty, whitespace-only, and comment-only sources — matching
399    // `metrics_inner`. The `ok_or` is retained so a future walker change
400    // that legitimately drains the stack surfaces a distinct error
401    // variant rather than a bare `None`. See `MetricsError::EmptyRoot`
402    // for the matching variant doc.
403    let mut state = state_stack.pop().ok_or(MetricsError::EmptyRoot)?;
404    // The root is the one state `finalize` never pops, so its vocabulary
405    // is built here — once, from the fully-merged maps.
406    compute_operators_and_operands::<T>(&mut state);
407    state.ops.name = name;
408    Ok(state.ops)
409}
410
411#[cfg(test)]
412#[allow(
413    clippy::float_cmp,
414    clippy::cast_precision_loss,
415    clippy::cast_possible_truncation,
416    clippy::cast_sign_loss,
417    clippy::similar_names,
418    clippy::doc_markdown,
419    clippy::needless_raw_string_hashes,
420    clippy::too_many_lines
421)]
422mod tests {
423    use super::Ops;
424    use crate::{Ast, LANG, Source};
425
426    #[cfg(any(
427        feature = "cpp",
428        feature = "java",
429        feature = "javascript",
430        feature = "mozjs",
431        feature = "perl",
432        feature = "python",
433        feature = "rust",
434        feature = "typescript",
435    ))]
436    #[inline]
437    fn check_ops(
438        lang: LANG,
439        source: &str,
440        file: &str,
441        correct_operators: &mut [&str],
442        correct_operands: &mut [&str],
443    ) {
444        let mut trimmed_bytes = source.trim_end().trim_matches('\n').as_bytes().to_vec();
445        trimmed_bytes.push(b'\n');
446        let ops = Ast::parse(Source::new(lang, &trimmed_bytes).with_name(Some(file.to_owned())))
447            .expect("language feature enabled")
448            .ops()
449            .expect("ops walk must yield a top-level Ops");
450
451        let operators_str: Vec<&str> = ops.operators.iter().map(AsRef::as_ref).collect();
452        let operands_str: Vec<&str> = ops.operands.iter().map(AsRef::as_ref).collect();
453
454        // Only the *expectations* are sorted here: `Ops` is documented to
455        // come back byte-lexicographically ordered (#1091), so comparing
456        // against a sorted expectation without re-sorting the actual value
457        // makes every `check_ops` caller an ordering regression test for
458        // its language.
459        correct_operators.sort_unstable();
460        assert_eq!(&operators_str[..], correct_operators);
461
462        correct_operands.sort_unstable();
463        assert_eq!(&operands_str[..], correct_operands);
464    }
465
466    #[cfg(feature = "python")]
467    #[test]
468    fn python_ops() {
469        check_ops(
470            LANG::Python,
471            "if True:
472                 a = 1 + 2",
473            "foo.py",
474            &mut ["if", ":", "=", "+"],
475            &mut ["True", "a", "1", "2"],
476        );
477    }
478
479    #[cfg(feature = "perl")]
480    #[test]
481    fn perl_pattern_operations_render_as_source_spellings() {
482        // #1314 classifies `s///` and `tr///` as Halstead operators.
483        // They are *named* nodes rather than punctuation tokens, so the
484        // `get_operator!` macro's fallback would render each kind's own
485        // name — `substitution_pattern_s`, `transliteration_tr_or_y` —
486        // into `bca ops`, which reads as a bug rather than as Perl.
487        // `PerlCode::get_operator_id_as_str` is hand-written to map
488        // them, and this is what pins that mapping: the counts alone
489        // cannot see it.
490        //
491        // `y///` is a synonym of `tr///` and shares one kind, so the
492        // two source spellings collapse to a single `tr///` entry —
493        // asserted here by the *absence* of a `y///` row in an
494        // exhaustive expectation, not by a negative assertion.
495        //
496        // The trailing `/pat/` keeps a pattern *value* in the fixture,
497        // so the test also shows the split: the operation spellings are
498        // operators while the value is an operand.
499        check_ops(
500            LANG::Perl,
501            "$s =~ s/a/b/;\n$s =~ tr/c/d/;\n$s =~ y/e/f/;\n$t = /pat/;",
502            "foo.pl",
503            &mut ["$", ";", "=", "=~", "s///", "tr///"],
504            &mut ["$s", "$t", "/pat/"],
505        );
506    }
507
508    /// The fat comma is a named node, so the generic fallback rendered
509    /// its kind name, `fat_comma`; it lists as the `=>` written.
510    #[cfg(feature = "perl")]
511    #[test]
512    fn perl_fat_comma_renders_as_written() {
513        check_ops(
514            LANG::Perl,
515            "my %h = (a => 1);",
516            "foo.pl",
517            &mut ["()", ";", "=", "=>", "my"],
518            &mut ["%h", "1", "a"],
519        );
520    }
521
522    /// A bareword key bills as the string it is (#1572): `-name` as one
523    /// operand with its sign, no `-` operator, and apart from the plain
524    /// `name` key. The expected vocabularies are the quoted twin's
525    /// (`'-name' => 1, 'name' => 2`, `$h{'-name'}`) with the quotes
526    /// dropped; before the fix `-name` billed `-` and folded into `name`.
527    /// The second fixture is the guard: a subtraction before a key and a
528    /// unary minus on a variable keep their `-`.
529    #[cfg(feature = "perl")]
530    #[test]
531    fn perl_bareword_keys_bill_as_written() {
532        check_ops(
533            LANG::Perl,
534            "my %h = (-name => 1, name => 2); my $v = $h{-name};",
535            "foo.pl",
536            &mut ["$", "()", ",", ";", "=", "=>", "my", "{}"],
537            &mut ["$h", "$v", "%h", "-name", "1", "2", "name"],
538        );
539        check_ops(
540            LANG::Perl,
541            "my @a = (1 - size => 3, -$x);",
542            "foo.pl",
543            &mut ["()", ",", "-", ";", "=", "=>", "my", "$"],
544            &mut ["$x", "1", "3", "@a", "size"],
545        );
546    }
547
548    #[cfg(feature = "python")]
549    #[test]
550    fn python_function_ops() {
551        check_ops(
552            LANG::Python,
553            "def foo():
554                 def bar():
555                     def toto():
556                        a = 1 + 1
557                     b = 2 + a
558                 c = 3 + 3",
559            "foo.py",
560            &mut ["def", "()", ":", "=", "+"],
561            &mut ["foo", "bar", "toto", "a", "b", "c", "1", "2", "3"],
562        );
563    }
564
565    /// #1486: Python bills a call's parentheses the way the JS family
566    /// does — one `()` entry for the pair, rendered as the pair. Before
567    /// it, Python reported no operator here at all.
568    #[cfg(all(feature = "javascript", feature = "python"))]
569    #[test]
570    fn python_call_parens_pair_like_javascript() {
571        check_ops(LANG::Python, "f(x)", "foo.py", &mut ["()"], &mut ["f", "x"]);
572        check_ops(
573            LANG::Javascript,
574            "f(x)",
575            "foo.js",
576            &mut ["()"],
577            &mut ["f", "x"],
578        );
579    }
580
581    #[cfg(feature = "cpp")]
582    #[test]
583    fn cpp_ops() {
584        check_ops(
585            LANG::Cpp,
586            "int a, b, c;
587             float avg;
588             avg = (a + b + c) / 3;",
589            "foo.c",
590            &mut ["int", "float", "()", "=", "+", "/", ",", ";"],
591            &mut ["a", "b", "c", "avg", "3"],
592        );
593    }
594
595    #[cfg(feature = "cpp")]
596    #[test]
597    fn cpp_function_ops() {
598        check_ops(
599            LANG::Cpp,
600            "main()
601            {
602              int a, b, c, avg;
603              scanf(\"%d %d %d\", &a, &b, &c);
604              avg = (a + b + c) / 3;
605              printf(\"avg = %d\", avg);
606            }",
607            "foo.c",
608            &mut ["()", "{}", "int", "&", "=", "+", "/", ",", ";"],
609            &mut [
610                "main",
611                "a",
612                "b",
613                "c",
614                "avg",
615                "scanf",
616                "\"%d %d %d\"",
617                "3",
618                "printf",
619                "\"avg = %d\"",
620            ],
621        );
622    }
623
624    #[cfg(feature = "rust")]
625    #[test]
626    fn rust_ops() {
627        check_ops(
628            LANG::Rust,
629            "let: usize a = 5; let b: f32 = 7.0; let c: i32 = 3;",
630            "foo.rs",
631            &mut ["let", ":", "usize", "=", ";", "f32", "i32"],
632            &mut ["a", "b", "c", "5", "7.0", "3"],
633        );
634    }
635
636    #[cfg(feature = "rust")]
637    #[test]
638    fn rust_function_ops() {
639        check_ops(
640            LANG::Rust,
641            "fn main() {
642              let a = 5; let b = 5; let c = 5;
643              let avg = (a + b + c) / 3;
644              println!(\"{}\", avg);
645            }",
646            "foo.rs",
647            &mut ["fn", "()", "{}", "let", "=", "+", "/", ";", "!", ","],
648            &mut ["main", "a", "b", "c", "avg", "5", "3", "println", "\"{}\""],
649        );
650    }
651
652    #[cfg(feature = "javascript")]
653    #[test]
654    fn javascript_ops() {
655        check_ops(
656            LANG::Javascript,
657            "var a, b, c, avg;
658             let x = 1;
659             a = 5; b = 5; c = 5;
660             avg = (a + b + c) / 3;
661             console.log(\"{}\", avg);",
662            "foo.js",
663            &mut ["()", "var", "let", "=", "+", "/", ",", ".", ";"],
664            &mut [
665                "a", "b", "c", "avg", "x", "1", "3", "5", "console", "log", "\"{}\"",
666            ],
667        );
668    }
669
670    #[cfg(feature = "javascript")]
671    #[test]
672    fn javascript_function_ops() {
673        check_ops(
674            LANG::Javascript,
675            "function main() {
676              var a, b, c, avg;
677              let x = 1;
678              a = 5; b = 5; c = 5;
679              avg = (a + b + c) / 3;
680              console.log(\"{}\", avg);
681            }",
682            "foo.js",
683            &mut [
684                "function", "()", "{}", "var", "let", "=", "+", "/", ",", ".", ";",
685            ],
686            &mut [
687                "main", "a", "b", "c", "avg", "x", "1", "3", "5", "console", "log", "\"{}\"",
688            ],
689        );
690    }
691
692    #[cfg(feature = "mozjs")]
693    #[test]
694    fn mozjs_ops() {
695        check_ops(
696            LANG::Mozjs,
697            "var a, b, c, avg;
698             let x = 1;
699             a = 5; b = 5; c = 5;
700             avg = (a + b + c) / 3;
701             console.log(\"{}\", avg);",
702            "foo.js",
703            &mut ["()", "var", "let", "=", "+", "/", ",", ".", ";"],
704            &mut [
705                "a", "b", "c", "avg", "x", "1", "3", "5", "console", "log", "\"{}\"",
706            ],
707        );
708    }
709
710    #[cfg(feature = "mozjs")]
711    #[test]
712    fn mozjs_function_ops() {
713        check_ops(
714            LANG::Mozjs,
715            "function main() {
716              var a, b, c, avg;
717              let x = 1;
718              a = 5; b = 5; c = 5;
719              avg = (a + b + c) / 3;
720              console.log(\"{}\", avg);
721            }",
722            "foo.js",
723            &mut [
724                "function", "()", "{}", "var", "let", "=", "+", "/", ",", ".", ";",
725            ],
726            &mut [
727                "main", "a", "b", "c", "avg", "x", "1", "3", "5", "console", "log", "\"{}\"",
728            ],
729        );
730    }
731
732    #[cfg(feature = "typescript")]
733    #[test]
734    fn typescript_ops() {
735        // Issue #1261: the `: string` annotation counts exactly once,
736        // as the text-keyed `string` operator (PredefinedType wrapper)
737        // — symmetric with `: number` / `: boolean`. Under #313 its
738        // `String2` child also emitted a `"string"` operand, so one
739        // source token tallied twice.
740        check_ops(
741            LANG::Typescript,
742            "var a, b, c, avg;
743             let age: number = 32;
744             let name: string = \"John\"; let isUpdated: boolean = true;
745             a = 5; b = 5; c = 5;
746             avg = (a + b + c) / 3;
747             console.log(\"{}\", avg);",
748            "foo.ts",
749            &mut [
750                "()", "var", "let", "string", "number", "boolean", ":", "=", "+", "/", ",", ".",
751                ";",
752            ],
753            &mut [
754                "a",
755                "b",
756                "c",
757                "avg",
758                "age",
759                "name",
760                "isUpdated",
761                "32",
762                "\"John\"",
763                "true",
764                "3",
765                "5",
766                "console",
767                "log",
768                "\"{}\"",
769            ],
770        );
771    }
772
773    #[cfg(feature = "typescript")]
774    #[test]
775    fn typescript_function_ops() {
776        // Issue #1261: see `typescript_ops` — the `string` type keyword
777        // contributes only the primitive-typed operator, never a
778        // `"string"` operand.
779        check_ops(
780            LANG::Typescript,
781            "function main() {
782              var a, b, c, avg;
783              let age: number = 32;
784              let name: string = \"John\"; let isUpdated: boolean = true;
785              a = 5; b = 5; c = 5;
786              avg = (a + b + c) / 3;
787              console.log(\"{}\", avg);
788            }",
789            "foo.ts",
790            &mut [
791                "function", "()", "{}", "var", "let", "string", "number", "boolean", ":", "=", "+",
792                "/", ",", ".", ";",
793            ],
794            &mut [
795                "main",
796                "a",
797                "b",
798                "c",
799                "avg",
800                "age",
801                "name",
802                "isUpdated",
803                "32",
804                "\"John\"",
805                "true",
806                "3",
807                "5",
808                "console",
809                "log",
810                "\"{}\"",
811            ],
812        );
813    }
814
815    #[cfg(feature = "typescript")]
816    #[test]
817    fn tsx_ops() {
818        // Issue #1261: TSX exposes the `: string` type-keyword child as
819        // `String3` (vs. TS's `String2`); like TS, the keyword counts
820        // only as the `string` operator, never as an operand.
821        check_ops(
822            LANG::Tsx,
823            "var a, b, c, avg;
824             let age: number = 32;
825             let name: string = \"John\"; let isUpdated: boolean = true;
826             a = 5; b = 5; c = 5;
827             avg = (a + b + c) / 3;
828             console.log(\"{}\", avg);",
829            "foo.ts",
830            &mut [
831                "()", "var", "let", "string", "number", "boolean", ":", "=", "+", "/", ",", ".",
832                ";",
833            ],
834            &mut [
835                "a",
836                "b",
837                "c",
838                "avg",
839                "age",
840                "name",
841                "isUpdated",
842                "32",
843                "\"John\"",
844                "true",
845                "3",
846                "5",
847                "console",
848                "log",
849                "\"{}\"",
850            ],
851        );
852    }
853
854    #[cfg(feature = "typescript")]
855    #[test]
856    fn tsx_function_ops() {
857        // Issue #1261: see `tsx_ops` — TSX::String3 (type-keyword
858        // `string`) is an operator only, never an operand.
859        check_ops(
860            LANG::Tsx,
861            "function main() {
862              var a, b, c, avg;
863              let age: number = 32;
864              let name: string = \"John\"; let isUpdated: boolean = true;
865              a = 5; b = 5; c = 5;
866              avg = (a + b + c) / 3;
867              console.log(\"{}\", avg);
868            }",
869            "foo.ts",
870            &mut [
871                "function", "()", "{}", "var", "let", "string", "number", "boolean", ":", "=", "+",
872                "/", ",", ".", ";",
873            ],
874            &mut [
875                "main",
876                "a",
877                "b",
878                "c",
879                "avg",
880                "age",
881                "name",
882                "isUpdated",
883                "32",
884                "\"John\"",
885                "true",
886                "3",
887                "5",
888                "console",
889                "log",
890                "\"{}\"",
891            ],
892        );
893    }
894
895    // Issue #453: a `void` return type (a `predefined_type` wrapper over a
896    // `void` token) and an expression `void` (`void 0`) must collapse to a
897    // single distinct `"void"` operator. `check_ops` asserts the exact
898    // operator list, so a duplicate `"void"` — the pre-fix symptom, where
899    // the wrapper keyed `primitive_operators["void"]` and the inner token
900    // keyed `operators[Void]` — trips the assertion. This pins the lesson-4
901    // `n1 == dedupe(ops.operators)` invariant for the two `void` forms in
902    // one file.
903    #[cfg(feature = "typescript")]
904    #[test]
905    fn typescript_void_return_and_expression_single_operator_453() {
906        check_ops(
907            LANG::Typescript,
908            "function f(): void { return void 0; }",
909            "foo.ts",
910            &mut ["function", "()", "{}", ":", "void", "return", ";"],
911            &mut ["f", "0"],
912        );
913    }
914
915    #[cfg(feature = "typescript")]
916    #[test]
917    fn tsx_void_return_and_expression_single_operator_453() {
918        check_ops(
919            LANG::Tsx,
920            "function f(): void { return void 0; }",
921            "foo.tsx",
922            &mut ["function", "()", "{}", ":", "void", "return", ";"],
923            &mut ["f", "0"],
924        );
925    }
926
927    #[cfg(feature = "java")]
928    #[test]
929    fn java_ops() {
930        check_ops(
931            LANG::Java,
932            "public class Main {
933                public static void main(string args[]) {
934                      int a, b, c, avg;
935                      a = 5; b = 5; c = 5;
936                      avg = (a + b + c) / 3;
937                      MessageFormat.format(\"{0}\", avg);
938                    }
939                }",
940            "foo.java",
941            // `public` and `static` are billed since #1565
942            &mut [
943                "public", "static", "class", "{}", "void", "()", "[]", ",", ".", ";", "int", "=",
944                "+", "/",
945            ],
946            &mut [
947                "Main",
948                "main",
949                // the parameter's type name (#1560)
950                "string",
951                "args",
952                "a",
953                "b",
954                "c",
955                "avg",
956                "5",
957                "3",
958                "MessageFormat",
959                "format",
960                "\"{0}\"",
961            ],
962        );
963    }
964
965    #[cfg(feature = "java")]
966    #[test]
967    fn java_primitive_ops() {
968        check_ops(
969            LANG::Java,
970            "public class Prims {
971                byte a = 1;
972                short b = 2;
973                int c = 3;
974                long d = 4;
975                char e = 'x';
976                float f = 1.0f;
977                double g = 2.0;
978                boolean h = true;
979                boolean i = false;
980            }",
981            "foo.java",
982            // All 8 primitive-type keywords must appear as distinct operators.
983            // true/false appear as operands; `class` is billed since #1552
984            // and `public` since #1565.
985            &mut [
986                "public", "class", "{}", ";", "=", "byte", "short", "int", "long", "char", "float",
987                "double", "boolean",
988            ],
989            &mut [
990                "Prims", "a", "b", "c", "d", "e", "f", "g", "h", "i", "1", "2", "3", "4", "'x'",
991                "1.0f", "2.0", "true", "false",
992            ],
993        );
994    }
995
996    /// A `Unit` space must never carry the synthetic `<anonymous>`
997    /// placeholder that the default getter invents for nodes without a
998    /// `name` field. The public docs describe `None` as the
999    /// "name could not be resolved" state, and the metrics-side
1000    /// `FuncSpace::new` already special-cases `SpaceKind::Unit` the same
1001    /// way; this pins the `Ops::new` mirror so a regression to the old
1002    /// `Some("<anonymous>")` initialisation fails here rather than only
1003    /// surfacing for a (currently unreachable) non-top-level `Unit`
1004    /// space, where `ops_inner`'s top-level override would not rescue it.
1005    /// See issue #755.
1006    #[cfg(feature = "rust")]
1007    #[test]
1008    fn unit_space_name_is_none_not_anonymous() {
1009        use crate::getter::Getter;
1010        use crate::node::Ancestors;
1011        use crate::traits::ParserTrait;
1012        use crate::{RustCode, RustParser, SpaceKind};
1013
1014        let code = b"fn f() {}\n";
1015        let parser = RustParser::new(code.to_vec(), std::path::Path::new("foo.rs"), None);
1016        let root = parser.root();
1017        // The Rust `source_file` root is a `Unit` and has no `name`/`type`
1018        // field, so the default getter would invent `<anonymous>`.
1019        assert_eq!(SpaceKind::Unit, RustCode::get_space_kind(&root));
1020
1021        let ops = super::Ops::new::<RustCode>(&root, code, Ancestors::unknown(), SpaceKind::Unit);
1022        assert_eq!(
1023            ops.name, None,
1024            "Unit space must preserve name = None, not invent <anonymous>"
1025        );
1026    }
1027
1028    /// Issue #789: an `ERROR`-root parse (here Lua partial input) where
1029    /// `metrics()` succeeds must make `ops()` succeed too — the two seams
1030    /// should agree. Before the synthetic-Unit-root mirror in `ops_inner`,
1031    /// `ops()` returned `Err(MetricsError::EmptyRoot)` because the ERROR
1032    /// root is not classified as a function space, so no frame was ever
1033    /// pushed. This pins their agreement: both succeed, and the resulting
1034    /// top-level `Ops` is a `Unit` whose name is the caller-supplied
1035    /// `Source::name` (the intrinsic Unit name stays `None` per #755 until
1036    /// `ops_inner` overrides the top-level name).
1037    #[cfg(feature = "lua")]
1038    #[test]
1039    fn lua_error_root_ops_agrees_with_metrics_789() {
1040        use crate::{MetricsOptions, SpaceKind};
1041
1042        // tree-sitter-lua surfaces an ERROR root for this partial input.
1043        let src = b"function foo(x)\n  return x +\n".to_vec();
1044        let name = "partial.lua".to_owned();
1045
1046        let ast = Ast::parse(Source::new(LANG::Lua, &src).with_name(Some(name.clone())))
1047            .expect("lua feature enabled");
1048
1049        // metrics() must succeed (it already wrapped a synthetic Unit root).
1050        let space = ast
1051            .metrics(MetricsOptions::default())
1052            .expect("metrics must yield a top-level space");
1053        assert_eq!(space.kind, SpaceKind::Unit);
1054
1055        // ops() must now succeed in the same case rather than returning
1056        // Err(EmptyRoot).
1057        let ops = ast
1058            .ops()
1059            .expect("ops must agree with metrics and yield a top-level Ops");
1060        assert_eq!(ops.kind, SpaceKind::Unit);
1061        assert_eq!(
1062            ops.name.as_deref(),
1063            Some(name.as_str()),
1064            "top-level Ops name is the caller-supplied Source::name"
1065        );
1066    }
1067
1068    /// Issue #790: `Ops::operands` / `Ops::operators` are the *distinct*
1069    /// (deduplicated) Halstead operand/operator vocabularies (`n2` / `n1`),
1070    /// not every occurrence. Pin the documented dedup semantics: each
1071    /// vector's length equals its unique-element count. The fixture
1072    /// repeats `+`, `;`, and `=` operators and the `a` operand so a
1073    /// regression to non-deduplicated collection would make `len` exceed
1074    /// the unique count.
1075    #[cfg(feature = "rust")]
1076    #[test]
1077    fn ops_vocabularies_are_distinct_790() {
1078        use std::collections::HashSet;
1079
1080        let src = b"fn main() { let a = 1 + 1; let b = a + a; }\n".to_vec();
1081        let ops = Ast::parse(Source::new(LANG::Rust, &src).with_name(Some("foo.rs".to_owned())))
1082            .expect("rust feature enabled")
1083            .ops()
1084            .expect("ops walk must yield a top-level Ops");
1085
1086        let unique_operators: HashSet<&String> = ops.operators.iter().collect();
1087        assert_eq!(
1088            ops.operators.len(),
1089            unique_operators.len(),
1090            "Ops::operators must be the distinct operator vocabulary (n1)"
1091        );
1092
1093        let unique_operands: HashSet<&String> = ops.operands.iter().collect();
1094        assert_eq!(
1095            ops.operands.len(),
1096            unique_operands.len(),
1097            "Ops::operands must be the distinct operand vocabulary (n2)"
1098        );
1099    }
1100
1101    /// Assert that every space in the tree carries sorted vocabularies,
1102    /// and return how many spaces were checked so a caller can prove the
1103    /// walk actually descended.
1104    ///
1105    /// The length floor is what keeps this from going quietly vacuous:
1106    /// `is_sorted` is trivially true for an empty or single-entry
1107    /// vector, so without it a fixture that stopped producing real
1108    /// vocabularies would keep passing while covering nothing.
1109    fn assert_sorted_spaces(ops: &Ops, lang: LANG) -> usize {
1110        /// Smallest vocabulary in which an ordering is observable.
1111        const MIN_OBSERVABLE: usize = 2;
1112
1113        let mut stack = vec![ops];
1114        let mut visited = 0;
1115
1116        while let Some(space) = stack.pop() {
1117            visited += 1;
1118            for (field, values) in [
1119                ("operators", &space.operators),
1120                ("operands", &space.operands),
1121            ] {
1122                assert!(
1123                    values.len() >= MIN_OBSERVABLE && values.is_sorted(),
1124                    "{lang:?} {field} of space {:?} (@{}) must hold at least \
1125                     {MIN_OBSERVABLE} entries and be sorted: {values:?}",
1126                    space.name,
1127                    space.start_line
1128                );
1129            }
1130            stack.extend(space.spaces.iter());
1131        }
1132
1133        visited
1134    }
1135
1136    /// Every space's vocabularies come back sorted, in every language.
1137    ///
1138    /// The operator vocabulary is the union of two maps — one keyed by
1139    /// token id, one keyed by text, which is where primitive types such
1140    /// as C++ `int` land — and sorting is what interleaves them rather
1141    /// than leaving the second concatenated onto the first (#1091). The
1142    /// `spans_both_maps` pair per case names one entry from each map, so
1143    /// a fixture that stopped exercising the text-keyed map fails here
1144    /// instead of silently narrowing the test's reach.
1145    #[test]
1146    fn ops_vocabularies_are_sorted_1091() {
1147        /// `(language, file name, source, (text-keyed operator,
1148        /// token-id-keyed operator that must sort after it))`.
1149        type Case = (
1150            LANG,
1151            &'static str,
1152            &'static str,
1153            (&'static str, &'static str),
1154        );
1155
1156        let cases: &[Case] = &[
1157            #[cfg(feature = "rust")]
1158            (
1159                LANG::Rust,
1160                "rust.rs",
1161                "fn zeta(quux: u32) -> u32 { let mid = quux + 1; \
1162                 let alpha = |beta: u32| beta * mid; alpha(mid) - quux }\n",
1163                ("u32", "|"),
1164            ),
1165            #[cfg(feature = "cpp")]
1166            (
1167                LANG::Cpp,
1168                "cpp.cpp",
1169                "int zeta(int quux) { double mid = quux + 1; \
1170                 char alpha = 'z'; return quux - mid + alpha; }\n",
1171                ("int", "return"),
1172            ),
1173            #[cfg(feature = "java")]
1174            (
1175                LANG::Java,
1176                "Java.java",
1177                "class Zeta { int quux(int mid) { long alpha = mid + 1; \
1178                 boolean beta = alpha > 2; return beta ? mid : 0; } }\n",
1179                ("long", "return"),
1180            ),
1181            #[cfg(feature = "python")]
1182            (
1183                LANG::Python,
1184                "python.py",
1185                "def zeta(quux):\n    mid = quux + 1\n    \
1186                 def alpha(beta):\n        return beta * mid\n    return alpha(mid) - quux\n",
1187                // Python has no primitive-type operators; both entries
1188                // come from the token-id map, so this pair only pins the
1189                // ordering, not the interleaving.
1190                ("def", "return"),
1191            ),
1192            #[cfg(feature = "typescript")]
1193            (
1194                LANG::Typescript,
1195                "ts.ts",
1196                "function zeta(quux: number): number { const mid: number = quux + 1; \
1197                 const alpha = (beta: number) => beta * mid; return alpha(mid) - quux; }\n",
1198                ("number", "return"),
1199            ),
1200        ];
1201
1202        for (lang, file, source, (from_text_map, sorts_after)) in cases {
1203            let ops = Ast::parse(
1204                Source::new(*lang, source.as_bytes()).with_name(Some((*file).to_owned())),
1205            )
1206            .expect("language feature enabled")
1207            .ops()
1208            .expect("ops walk must yield a top-level Ops");
1209
1210            let position = |needle: &str| {
1211                ops.operators
1212                    .iter()
1213                    .position(|op| op == needle)
1214                    .unwrap_or_else(|| {
1215                        panic!(
1216                            "{lang:?} operators must contain {needle:?}: {:?}",
1217                            ops.operators
1218                        )
1219                    })
1220            };
1221            assert!(
1222                position(from_text_map) < position(sorts_after),
1223                "{lang:?} must order {from_text_map:?} before {sorts_after:?}: {:?}",
1224                ops.operators
1225            );
1226
1227            assert!(
1228                assert_sorted_spaces(&ops, *lang) > 1,
1229                "{lang:?} sample must nest at least one sub-space"
1230            );
1231        }
1232    }
1233
1234    /// Two parses of the same bytes in one process must agree exactly.
1235    ///
1236    /// `RandomState` bumps its thread-local seed per instance, so the
1237    /// pre-fix code could — and did — order two `HashMap`s built from
1238    /// identical keys differently within a single run. This is the
1239    /// in-process form of the cross-run churn in #1091.
1240    #[test]
1241    #[cfg(feature = "rust")]
1242    fn ops_are_stable_across_repeated_parses_1091() {
1243        use std::fmt::Write as _;
1244
1245        // Enough distinct operands, in enough distinct spaces, that
1246        // agreement by coincidence is not a plausible explanation for a
1247        // pass.
1248        let mut src = String::new();
1249        for i in 0..40 {
1250            writeln!(src, "fn name{i}(arg{i}: u32) -> u32 {{ arg{i} + {i} }}")
1251                .expect("writing to a String cannot fail");
1252        }
1253
1254        let parse = || {
1255            Ast::parse(Source::new(LANG::Rust, src.as_bytes()).with_name(Some("foo.rs".to_owned())))
1256                .expect("rust feature enabled")
1257                .ops()
1258                .expect("ops walk must yield a top-level Ops")
1259        };
1260
1261        let (first, second) = (parse(), parse());
1262        assert!(
1263            first.operands.len() >= 40 && first.spaces.len() >= 40,
1264            "sample must have a wide vocabulary across many spaces, got {} operands \
1265             in {} spaces",
1266            first.operands.len(),
1267            first.spaces.len()
1268        );
1269        // `Ops` has no `PartialEq`, and the nested spaces are the half a
1270        // top-level vector comparison would miss, so compare the whole
1271        // serialized tree.
1272        let render =
1273            |ops: &Ops| serde_json::to_string(&ops.to_wire()).expect("wire Ops serializes to JSON");
1274        assert_eq!(render(&first), render(&second));
1275    }
1276
1277    /// A vocabulary that lost bytes is ordered by its *rendered* text.
1278    ///
1279    /// #1110 moved the sort ahead of the lossy UTF-8 rendering, which is
1280    /// only order-preserving while every key is valid UTF-8. Here it is
1281    /// not: the two string operands are `"\xffA"` and `"\u{fffd}B"`, so
1282    /// by raw bytes the first sorts *after* the second (`0xff` > `0xef`)
1283    /// and by rendered text — both start `U+FFFD`, then `A` before `B` —
1284    /// it sorts before. The rendered order is what the pre-#1110
1285    /// render-then-sort code produced and what `Ops` documents, so
1286    /// dropping the fallback re-sort flips this pair and fails here.
1287    #[test]
1288    #[cfg(feature = "rust")]
1289    fn ops_vocabulary_orders_lossy_entries_by_rendered_text_1110() {
1290        let mut src = b"fn f() { let a = \"".to_vec();
1291        src.push(0xff);
1292        src.extend_from_slice(b"A\"; let b = \"");
1293        src.extend_from_slice(&[0xef, 0xbf, 0xbd]);
1294        src.extend_from_slice(b"B\"; }\n");
1295
1296        let ops = Ast::parse(Source::new(LANG::Rust, &src).with_name(Some("foo.rs".to_owned())))
1297            .expect("rust feature enabled")
1298            .ops()
1299            .expect("ops walk must yield a top-level Ops");
1300
1301        let position = |needle: &str| {
1302            ops.operands
1303                .iter()
1304                .position(|operand| operand == needle)
1305                .unwrap_or_else(|| panic!("operands must contain {needle:?}: {:?}", ops.operands))
1306        };
1307        assert!(
1308            position("\"\u{fffd}A\"") < position("\"\u{fffd}B\""),
1309            "lossy entries must be ordered by rendered text, got {:?}",
1310            ops.operands
1311        );
1312        assert!(
1313            ops.operands.is_sorted(),
1314            "the whole vocabulary must be sorted as rendered, got {:?}",
1315            ops.operands
1316        );
1317    }
1318
1319    /// The walk classifies a space kind once per space, not once per node.
1320    ///
1321    /// Nothing in the output distinguishes the two: the classification
1322    /// only ever reaches `Ops::new`, so running it on every node produces
1323    /// the same tree and merely throws the extra answers away. #1110
1324    /// moved the call inside the `func_space` branch, mirroring
1325    /// `spaces::compute::open_func_space`; the counter is what makes
1326    /// hoisting it back out a failure. The node-count assertion is what
1327    /// makes the counts distinguishable — a fixture whose nodes and
1328    /// spaces were equal in number could not tell the two apart.
1329    // test-lang-gates: hand-written(rust) — the one grammar that
1330    //     makes the fixture list non-empty, so the non-vacuity
1331    //     assertion cannot fire on a minimal build
1332    #[test]
1333    // Gated on the language that guarantees a non-empty case list, so
1334    // the emptiness assertion below cannot fire on a minimal build.
1335    #[cfg(feature = "rust")]
1336    fn ops_classifies_space_kind_once_per_space_1110() {
1337        let cases: &[(LANG, &str, &str)] = &[
1338            #[cfg(feature = "rust")]
1339            (
1340                LANG::Rust,
1341                "foo.rs",
1342                "fn outer(a: u32) -> u32 { fn inner(b: u32) -> u32 { b + 1 } inner(a) * 2 }\n",
1343            ),
1344            #[cfg(feature = "python")]
1345            (
1346                LANG::Python,
1347                "foo.py",
1348                "def outer(a):\n    def inner(b):\n        return b + 1\n    return inner(a) * 2\n",
1349            ),
1350            #[cfg(feature = "cpp")]
1351            (
1352                LANG::Cpp,
1353                "foo.cpp",
1354                "struct S { int m(int a) { return a + 1; } }; int f(int b) { return b * 2; }\n",
1355            ),
1356            #[cfg(feature = "java")]
1357            (
1358                LANG::Java,
1359                "Foo.java",
1360                "class C { int m(int a) { return a + 1; } int n(int b) { return b * 2; } }\n",
1361            ),
1362            #[cfg(feature = "javascript")]
1363            (
1364                LANG::Javascript,
1365                "foo.js",
1366                "function outer(a) { function inner(b) { return b + 1; } return inner(a) * 2; }\n",
1367            ),
1368        ];
1369        crate::test_support::assert_fixtures_present(cases);
1370
1371        for (lang, file, source) in cases {
1372            let ast = crate::test_support::parse_named(*lang, file, source);
1373
1374            let before = super::space_kind_lookups::observed();
1375            let ops = ast.ops().expect("ops walk must yield a top-level Ops");
1376            let lookups = super::space_kind_lookups::observed() - before;
1377
1378            let mut spaces = 0;
1379            let mut stack = vec![&ops];
1380            while let Some(space) = stack.pop() {
1381                spaces += 1;
1382                stack.extend(space.spaces.iter());
1383            }
1384
1385            let mut nodes = 0;
1386            let mut cursor = vec![ast.as_tree_sitter().root_node()];
1387            while let Some(node) = cursor.pop() {
1388                nodes += 1;
1389                let mut walker = node.walk();
1390                cursor.extend(node.children(&mut walker));
1391            }
1392
1393            assert!(
1394                nodes > spaces * 4,
1395                "{lang:?} fixture must have many more nodes ({nodes}) than spaces ({spaces}) \
1396                 for the two counts to be distinguishable"
1397            );
1398            assert_eq!(
1399                lookups, spaces,
1400                "{lang:?} must classify once per space, not once per node ({nodes} nodes)"
1401            );
1402        }
1403    }
1404
1405    /// One flattened space: `(depth, kind, name, start_line, end_line)`.
1406    #[cfg(feature = "elixir")]
1407    type FlatSpace = (usize, crate::SpaceKind, String, usize, usize);
1408
1409    /// Flattens an `Ops` tree in preorder, so a test can pin the whole
1410    /// tree in one `assert_eq!` and see the surrounding spaces when one
1411    /// is wrong.
1412    ///
1413    /// `end_line` is carried as well as `start_line` because a change to
1414    /// the promote predicate can move a space's *extent* without moving
1415    /// its head — a `def` that swallows its sibling would keep the same
1416    /// start line.
1417    #[cfg(feature = "elixir")]
1418    fn flatten(ops: &Ops, depth: usize, out: &mut Vec<FlatSpace>) {
1419        out.push((
1420            depth,
1421            ops.kind,
1422            ops.name.clone().unwrap_or_else(|| "<none>".to_owned()),
1423            ops.start_line,
1424            ops.end_line,
1425        ));
1426        for child in &ops.spaces {
1427            flatten(child, depth + 1, out);
1428        }
1429    }
1430
1431    #[cfg(feature = "elixir")]
1432    fn elixir_ops_tree(source: &str) -> Vec<FlatSpace> {
1433        let ops = crate::test_support::parse_named(LANG::Elixir, "foo.ex", source)
1434            .ops()
1435            .expect("ops walk must yield a top-level Ops");
1436        let mut flat = Vec::new();
1437        flatten(&ops, 0, &mut flat);
1438        flat
1439    }
1440
1441    /// Issue #1130: Elixir's `defmodule` / `def` are `Call` nodes whose
1442    /// target identifier text spells the keyword, so only the
1443    /// source-aware promote predicate can recognise them. Before the
1444    /// fix `ops()` returned the bare file-level `Unit` for this input
1445    /// while `metrics()` returned the full module/function tree.
1446    #[cfg(feature = "elixir")]
1447    #[test]
1448    fn elixir_ops_opens_module_and_function_spaces_1130() {
1449        use crate::SpaceKind::{Class, Function, Unit};
1450
1451        assert_eq!(
1452            elixir_ops_tree("defmodule Foo do\n  def bar(x) do\n    x + 1\n  end\nend\n"),
1453            vec![
1454                (0, Unit, "foo.ex".to_owned(), 1, 5),
1455                (1, Class, "Foo".to_owned(), 1, 5),
1456                (2, Function, "bar".to_owned(), 2, 4),
1457            ],
1458        );
1459    }
1460
1461    /// An `AnonymousFunction` is the one Elixir space the byte-less
1462    /// predicate could already see, so this pins that the source-aware
1463    /// predicate did not lose it — and that it nests under the `def`
1464    /// space rather than being reparented to the file root.
1465    #[cfg(feature = "elixir")]
1466    #[test]
1467    fn elixir_ops_opens_anonymous_function_space() {
1468        use crate::SpaceKind::{Class, Function, Unit};
1469
1470        assert_eq!(
1471            elixir_ops_tree(
1472                "defmodule Foo do\n  def bar(list) do\n    \
1473                 Enum.map(list, fn x -> x * 2 end)\n  end\nend\n"
1474            ),
1475            vec![
1476                (0, Unit, "foo.ex".to_owned(), 1, 5),
1477                (1, Class, "Foo".to_owned(), 1, 5),
1478                (2, Function, "bar".to_owned(), 2, 4),
1479                (3, Function, "<anonymous>".to_owned(), 3, 3),
1480            ],
1481        );
1482    }
1483
1484    /// Issue #310: a `def` inside `quote do … end` is a code *template*
1485    /// emitted later by macro expansion, not a declaration of the
1486    /// enclosing module, so it opens no space. This is the case only the
1487    /// source-aware predicate can get right — the byte-less one never
1488    /// saw any `def` at all, so it was accidentally "correct" here while
1489    /// being wrong everywhere else.
1490    #[cfg(feature = "elixir")]
1491    #[test]
1492    fn elixir_ops_skips_def_inside_quote_block_310() {
1493        use crate::SpaceKind::{Class, Function, Unit};
1494
1495        // The quoted `def` heads line 4. Its name resolves to the
1496        // `<anonymous>` placeholder (the head is `unquote(name)`, not a
1497        // literal identifier), so the absence of a fourth entry — not a
1498        // name match — is what pins it out of the tree.
1499        assert_eq!(
1500            elixir_ops_tree(
1501                "defmodule Foo do\n  defmacro gen(name) do\n    quote do\n      \
1502                 def unquote(name)(x) do\n        x + 1\n      end\n    end\n  end\nend\n",
1503            ),
1504            vec![
1505                (0, Unit, "foo.ex".to_owned(), 1, 9),
1506                (1, Class, "Foo".to_owned(), 1, 9),
1507                (2, Function, "gen".to_owned(), 2, 8),
1508            ],
1509        );
1510    }
1511
1512    /// #1559: a generic list's angle brackets are one `<>` bracket pair,
1513    /// billed on the opener as `()` / `[]` / `{}` are, with the closer
1514    /// billing nothing. Every fixture also spells a comparison `<` and
1515    /// `>`, which must stay their own vocabulary entries: the grammars
1516    /// give the list brackets the comparison operators' kind ids, so the
1517    /// vocabulary is what shows the two apart and `N1` is what shows the
1518    /// closer is not billed.
1519    #[cfg(any(
1520        feature = "cpp",
1521        feature = "csharp",
1522        feature = "groovy",
1523        feature = "java",
1524        feature = "kotlin",
1525        feature = "mozcpp",
1526        feature = "objc",
1527        feature = "rust",
1528        feature = "typescript",
1529    ))]
1530    fn check_generic_pair(lang: LANG, file: &str, source: &str, operators: &[&str], n1: [u64; 2]) {
1531        let ast = crate::test_support::parse_named(lang, file, source);
1532        let ops = ast.ops().expect("ops walk must yield a top-level Ops");
1533        let mut expected = operators.to_vec();
1534        expected.sort_unstable();
1535        assert_eq!(ops.operators, expected, "{lang:?} vocabulary");
1536
1537        let space = ast
1538            .metrics(crate::MetricsOptions::default().with_only(&[crate::Metric::Halstead]))
1539            .expect("metrics walk");
1540        let halstead = &space.metrics.halstead;
1541        assert_eq!(
1542            [halstead.unique_operators(), halstead.total_operators()],
1543            n1,
1544            "{lang:?} [n1, N1]"
1545        );
1546    }
1547
1548    // expected: `<>` three times (one parameter list, two nested
1549    // argument lists), never `>`; `<` and `>` once each, the comparisons.
1550    #[cfg(feature = "rust")]
1551    #[test]
1552    fn rust_generic_angles_are_one_pair_1559() {
1553        check_generic_pair(
1554            LANG::Rust,
1555            "foo.rs",
1556            "fn f<T>(v: Vec<Vec<T>>) -> bool { a < b && c > d }\n",
1557            &["fn", "<>", "()", ":", "->", "bool", "{}", "<", "&&", ">"],
1558            [10, 12],
1559        );
1560        // A turbofish and a higher-ranked `for<'a>` (`for_lifetimes`)
1561        // are lists too, `<>` twice (`fn` is twice as well); the `>>` shift
1562        // is its own operator.
1563        check_generic_pair(
1564            LANG::Rust,
1565            "foo.rs",
1566            "fn g(f: for<'a> fn(&'a u8)) { h::<u8>(x >> 1); }\n",
1567            &[
1568                "fn", "()", ":", "for", "<>", "&", "u8", "{}", "::", ">>", ";",
1569            ],
1570            [11, 16],
1571        );
1572        // A precise-capturing `use<T>` (`use_bounds`) is a list as well.
1573        check_generic_pair(
1574            LANG::Rust,
1575            "foo.rs",
1576            "fn u<T>(x: T) -> impl Sized + use<T> { x }\n",
1577            &["fn", "<>", "()", ":", "->", "impl", "+", "use", "{}"],
1578            [9, 10],
1579        );
1580        // A qualified path's `bracketed_type` encloses a type, `<>` with
1581        // or without `as` (#1567). `<>` three times (two qualified paths
1582        // and `Vec<u8>`), `()` three times, `::` and `;` twice; `as` still
1583        // bills and the comparison `>` stays. Unfixed: [12, 19].
1584        check_generic_pair(
1585            LANG::Rust,
1586            "foo.rs",
1587            "fn q() -> bool { <Vec<u8> as Default>::default(); <T>::g(); a > b }\n",
1588            &[
1589                "fn", "()", "->", "bool", "{}", "<>", "u8", "as", "::", ";", ">",
1590            ],
1591            [11, 17],
1592        );
1593    }
1594
1595    // expected: `<>` three times, `;` and `{}` twice each.
1596    #[cfg(feature = "java")]
1597    #[test]
1598    fn java_generic_angles_are_one_pair_1559() {
1599        check_generic_pair(
1600            LANG::Java,
1601            "foo.java",
1602            "class A<T> { List<List<T>> v; boolean m() { return a < b && c > d; } }\n",
1603            &[
1604                "class", "<>", ";", "{}", "boolean", "()", "return", "<", "&&", ">",
1605            ],
1606            [10, 14],
1607        );
1608    }
1609
1610    // expected: as Java, with `bool` for `boolean`.
1611    #[cfg(feature = "csharp")]
1612    #[test]
1613    fn csharp_generic_angles_are_one_pair_1559() {
1614        check_generic_pair(
1615            LANG::Csharp,
1616            "foo.cs",
1617            "class A<T> { List<List<T>> v; bool M() { return a < b && c > d; } }\n",
1618            &[
1619                "class", "<>", ";", "{}", "bool", "()", "return", "<", "&&", ">",
1620            ],
1621            [10, 14],
1622        );
1623        // A function pointer type's brackets enclose its parameter and
1624        // return types, `<>` with or without a calling convention (#1567).
1625        // `;` three times; `{}`, `delegate`, `*`, `<>`, `int`, `,` and
1626        // `void` twice. Unfixed: [16, 28], `<` and `>` three times each.
1627        check_generic_pair(
1628            LANG::Csharp,
1629            "foo.cs",
1630            "unsafe class C { delegate*<int, void> p; delegate* unmanaged[Cdecl]<int, void> q; \
1631             bool M() { return a < b && c > d; } }\n",
1632            &[
1633                "unsafe", "class", "{}", "delegate", "*", "<>", "int", ",", "void", ";", "[]",
1634                "bool", "()", "return", "<", "&&", ">",
1635            ],
1636            [17, 26],
1637        );
1638    }
1639
1640    // expected: `<>` three times, `:` twice. TSX shares the getter macro
1641    // and the same arm, so it is checked on the same input.
1642    #[cfg(feature = "typescript")]
1643    #[test]
1644    fn typescript_generic_angles_are_one_pair_1559() {
1645        const SOURCE: &str =
1646            "function f<T>(v: Array<Array<T>>): boolean { return a < b && c > d; }\n";
1647        const OPERATORS: &[&str] = &[
1648            "function", "<>", "()", ":", "boolean", "{}", "return", "<", "&&", ">", ";",
1649        ];
1650        check_generic_pair(LANG::Typescript, "foo.ts", SOURCE, OPERATORS, [11, 14]);
1651        check_generic_pair(LANG::Tsx, "foo.tsx", SOURCE, OPERATORS, [11, 14]);
1652    }
1653
1654    // expected: `<>` three times (the `template` parameter list and two
1655    // nested argument lists), `::` twice. The closer of the outer list
1656    // is spelled `>>` and still parses as two `>` tokens.
1657    #[cfg(feature = "cpp")]
1658    #[test]
1659    fn cpp_generic_angles_are_one_pair_1559() {
1660        check_generic_pair(
1661            LANG::Cpp,
1662            "foo.cpp",
1663            "template <class T> bool f(std::vector<std::vector<T>> v) { return a < b && c > d; }\n",
1664            &[
1665                "template", "<>", "class", "bool", "()", "::", "{}", "return", "<", "&&", ">", ";",
1666            ],
1667            [12, 15],
1668        );
1669    }
1670
1671    // expected: the Cpp row, through the Mozcpp grammar, which owns no
1672    // file extension and so has no corpus coverage of its own.
1673    #[cfg(feature = "mozcpp")]
1674    #[test]
1675    fn mozcpp_generic_angles_are_one_pair_1559() {
1676        check_generic_pair(
1677            LANG::Mozcpp,
1678            "foo.cpp",
1679            "template <class T> bool f(std::vector<std::vector<T>> v) { return a < b && c > d; }\n",
1680            &[
1681                "template", "<>", "class", "bool", "()", "::", "{}", "return", "<", "&&", ">", ";",
1682            ],
1683            [12, 15],
1684        );
1685    }
1686
1687    // expected: `<>` three times, `:` twice.
1688    #[cfg(feature = "kotlin")]
1689    #[test]
1690    fn kotlin_generic_angles_are_one_pair_1559() {
1691        check_generic_pair(
1692            LANG::Kotlin,
1693            "foo.kt",
1694            "fun <T> f(v: List<List<T>>): Boolean { return a < b && c > d }\n",
1695            &["fun", "<>", "()", ":", "{}", "return", "<", "&&", ">"],
1696            [9, 12],
1697        );
1698        // A qualified `super<A>` encloses the supertype it names (#1567).
1699        // `()` three times (`A()`, `f()`, `.f()`), `:` and `{}` twice.
1700        // Unfixed: [10, 16], `<` and `>` twice each.
1701        check_generic_pair(
1702            LANG::Kotlin,
1703            "foo.kt",
1704            "class C : A() { fun f(): Boolean { super<A>.f(); return x < y && x > y } }\n",
1705            &[
1706                "class", ":", "()", "{}", "fun", "<>", ".", "return", "<", "&&", ">",
1707            ],
1708            [11, 15],
1709        );
1710    }
1711
1712    // expected: `<>` four times — Groovy spells a method's parameter
1713    // list as its own `method_type_parameters` node.
1714    #[cfg(feature = "groovy")]
1715    #[test]
1716    fn groovy_generic_angles_are_one_pair_1559() {
1717        check_generic_pair(
1718            LANG::Groovy,
1719            "foo.groovy",
1720            "class A<T> { List<List<T>> v; def <U> boolean m() { return a < b && c > d } }\n",
1721            &[
1722                "class", "<>", ";", "{}", "def", "boolean", "()", "return", "<", "&&", ">",
1723            ],
1724            [11, 15],
1725        );
1726    }
1727
1728    // expected: `<>` five times, once per list — `parameterized_arguments`
1729    // (`: NSObject <NSCopying>`), `protocol_reference_list`
1730    // (`id<NSCopying>`), two nested `generic_specifier`s, and the
1731    // `argument_list` type form `g(NSArray<NSString *>)` — and never `>`.
1732    // `*` four times, `()` and `||` twice; `@end` and `id` bill nothing.
1733    // Without the pair, `<` and `>` would each bill six times: [10, 25].
1734    // expected: the spaced twin `g(a < b, c > d)`, two comparisons.
1735    // Unspaced, tree-sitter-objc reads the arguments as an
1736    // `argument_list`'s `Type<…>` form with an `ERROR` after the `>`, and
1737    // they must keep billing `<` and `>`; the clean `g(NSArray<NSString *>)`
1738    // stays one pair.
1739    #[cfg(feature = "objc")]
1740    #[test]
1741    fn objc_misparsed_comparisons_stay_comparisons_1559() {
1742        const COMPARISONS: &[&str] = &["()", ",", ";", "<", ">", "int", "return", "void", "{}"];
1743        check_generic_pair(
1744            LANG::Objc,
1745            "foo.m",
1746            "int f(void) { return g(a < b, c > d); }\n",
1747            COMPARISONS,
1748            [9, 10],
1749        );
1750        check_generic_pair(
1751            LANG::Objc,
1752            "foo.m",
1753            "int f(void) { return g(a<b, c>d); }\n",
1754            COMPARISONS,
1755            [9, 10],
1756        );
1757        check_generic_pair(
1758            LANG::Objc,
1759            "foo.m",
1760            "int f(void) { return g(NSArray<NSString *>); }\n",
1761            &["()", "*", ";", "<>", "int", "return", "void", "{}"],
1762            [8, 9],
1763        );
1764    }
1765
1766    // expected: the reading before #1559, which billed the two
1767    // comparisons as `<` and `>`. By the C# specification the identifier
1768    // after the `>` makes `F(a < b, c > d)` two arguments, though the
1769    // grammar parses a declaration typed `a<b, c>`. An `out` declaration
1770    // is a real generic and stays one pair.
1771    #[cfg(feature = "csharp")]
1772    #[test]
1773    fn csharp_misparsed_comparisons_stay_comparisons_1559() {
1774        check_generic_pair(
1775            LANG::Csharp,
1776            "foo.cs",
1777            "class A { void M() { F(Foo<T>.a < b, c > d); } }\n",
1778            &["()", ",", ".", ";", "<", "<>", ">", "class", "void", "{}"],
1779            [10, 12],
1780        );
1781        check_generic_pair(
1782            LANG::Csharp,
1783            "foo.cs",
1784            "class A { void M() { F(a < b, c > d); } }\n",
1785            &["()", ",", ";", "<", ">", "class", "void", "{}"],
1786            [8, 10],
1787        );
1788        // A member access before the `<` wraps the generic name in a
1789        // `qualified_name`; the comparisons are the same.
1790        check_generic_pair(
1791            LANG::Csharp,
1792            "foo.cs",
1793            "class A { void M() { F(x.a < b, c > d); } }\n",
1794            &["()", ",", ".", ";", "<", ">", "class", "void", "{}"],
1795            [9, 11],
1796        );
1797        check_generic_pair(
1798            LANG::Csharp,
1799            "foo.cs",
1800            "class A { void M() { F(out List<int> x); } }\n",
1801            &["()", ";", "<>", "class", "int", "out", "void", "{}"],
1802            [8, 10],
1803        );
1804        // A deconstruction's declarations are `argument`s of a
1805        // `tuple_expression`, not of an argument list: a real generic.
1806        check_generic_pair(
1807            LANG::Csharp,
1808            "foo.cs",
1809            "class A { void M() { (List<int> p, int q) = (1, 2); } }\n",
1810            &["()", ",", ";", "<>", "=", "class", "int", "void", "{}"],
1811            [9, 14],
1812        );
1813        // A using alias's generic name sits two levels under the root, too
1814        // shallow for the argument-list shape.
1815        check_generic_pair(
1816            LANG::Csharp,
1817            "foo.cs",
1818            "using X = List<int>;\n",
1819            &[";", "<>", "=", "int", "using"],
1820            [5, 5],
1821        );
1822    }
1823
1824    #[cfg(feature = "objc")]
1825    #[test]
1826    fn objc_generic_angles_are_one_pair_1559() {
1827        check_generic_pair(
1828            LANG::Objc,
1829            "foo.m",
1830            "@interface A : NSObject <NSCopying>\n@end\n\
1831             id<NSCopying> f(NSArray<NSArray<NSString *> *> *v) \
1832             { return g(NSArray<NSString *>) || a < b || c > d; }\n",
1833            &[
1834                "@interface",
1835                ":",
1836                "<>",
1837                "()",
1838                "*",
1839                "{}",
1840                "return",
1841                "||",
1842                "<",
1843                ">",
1844                ";",
1845            ],
1846            [11, 20],
1847        );
1848    }
1849}