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, Node};
15use crate::spaces::{SpaceKind, line_span, push_children};
16
17use crate::halstead::{Halstead, HalsteadMaps};
18
19use crate::traits::ParserTrait;
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: ParserTrait>(
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: ParserTrait>(
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: ParserTrait>(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: ParserTrait>(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: ParserTrait>(
302    parser: &T,
303    name: Option<String>,
304) -> Result<Ops, MetricsError> {
305    let code = parser.code();
306    let node = parser.root();
307    let mut cursor = node.cursor();
308    let mut stack = Vec::new();
309    // Ancestor chain of the node currently being visited, root first,
310    // maintained by the same truncate/push rule as
311    // `spaces::compute::metrics_inner` (#1084).
312    let mut chain: Vec<Node<'_>> = Vec::new();
313    let mut state_stack: Vec<State> = Vec::new();
314    let mut last_level = 0;
315
316    // Mirror `metrics_inner`: wrap a non-`Unit` (e.g. `ERROR`) root in a
317    // synthetic `Unit` frame so the walk always has a frame to populate.
318    // Without this, an `ERROR`-root parse drains the state stack and
319    // `ops_inner` returns `EmptyRoot` for inputs where `metrics()`
320    // succeeds (issue #789).
321    push_synthetic_unit_root::<T>(&mut state_stack, &node, code);
322
323    stack.push((node, Walk { level: 0, depth: 0 }));
324
325    while let Some((node, Walk { level, depth })) = stack.pop() {
326        chain.truncate(depth);
327
328        if level < last_level {
329            finalize::<T>(&mut state_stack, last_level - level);
330            last_level = level;
331        }
332
333        let ancestors = Ancestors::checked(&chain, &node);
334
335        // Same predicate `spaces::compute::metrics_inner` opens on, so
336        // the two walks agree on which nodes become spaces. The
337        // byte-less `is_func || is_func_space` this replaced could not
338        // see Elixir's macro-shaped declarations, which are `Call`
339        // nodes identified by their target text, so `bca ops` opened no
340        // space for a `defmodule` / `def` / `defp` / `defmacro` — only
341        // for `Source` and an explicit `fn … -> … end` (#1130).
342        let func_space = T::Checker::promotes_to_func_space_with_code(&node, code, ancestors);
343
344        let new_level = if func_space {
345            let kind = classify_space_kind::<T>(&node, code, ancestors);
346            let state = State {
347                ops: Ops::new::<T::Getter>(&node, code, ancestors, kind),
348                halstead_maps: HalsteadMaps::new(),
349            };
350            state_stack.push(state);
351            last_level = level + 1;
352            last_level
353        } else {
354            level
355        };
356
357        if let Some(state) = state_stack.last_mut() {
358            T::Halstead::compute(&node, code, ancestors, &mut state.halstead_maps);
359        }
360
361        chain.push(node);
362
363        // Shared with `metrics_inner` (issue #969): `push_children` is
364        // State-independent — it only moves the cursor over child nodes —
365        // so unlike the local `finalize` / `push_synthetic_unit_root`
366        // mirrors (which differ by `State` payload) it is reused directly
367        // rather than duplicated. The source-order-then-reverse ordering
368        // it encapsulates is load-bearing for suppression attribution.
369        // The returned child slice is only useful to `metrics_inner`,
370        // which seeds their cognitive nesting; `ops` just walks them.
371        push_children(
372            &mut cursor,
373            &node,
374            Walk {
375                level: new_level,
376                depth: depth + 1,
377            },
378            &mut stack,
379        );
380    }
381
382    finalize::<T>(&mut state_stack, usize::MAX);
383
384    // Reserved error path: `MetricsError::EmptyRoot` is unreachable
385    // today because the synthetic Unit push above (and every supported
386    // language's root being recognised as a `func_space`) keeps the
387    // state stack non-empty for every input, including ERROR-root,
388    // empty, whitespace-only, and comment-only sources — matching
389    // `metrics_inner`. The `ok_or` is retained so a future walker change
390    // that legitimately drains the stack surfaces a distinct error
391    // variant rather than a bare `None`. See `MetricsError::EmptyRoot`
392    // for the matching variant doc.
393    let mut state = state_stack.pop().ok_or(MetricsError::EmptyRoot)?;
394    // The root is the one state `finalize` never pops, so its vocabulary
395    // is built here — once, from the fully-merged maps.
396    compute_operators_and_operands::<T>(&mut state);
397    state.ops.name = name;
398    Ok(state.ops)
399}
400
401#[cfg(test)]
402#[allow(
403    clippy::float_cmp,
404    clippy::cast_precision_loss,
405    clippy::cast_possible_truncation,
406    clippy::cast_sign_loss,
407    clippy::similar_names,
408    clippy::doc_markdown,
409    clippy::needless_raw_string_hashes,
410    clippy::too_many_lines
411)]
412mod tests {
413    use super::Ops;
414    use crate::{Ast, LANG, Source};
415
416    #[inline]
417    fn check_ops(
418        lang: LANG,
419        source: &str,
420        file: &str,
421        correct_operators: &mut [&str],
422        correct_operands: &mut [&str],
423    ) {
424        let mut trimmed_bytes = source.trim_end().trim_matches('\n').as_bytes().to_vec();
425        trimmed_bytes.push(b'\n');
426        let ops = Ast::parse(Source::new(lang, &trimmed_bytes).with_name(Some(file.to_owned())))
427            .expect("language feature enabled")
428            .ops()
429            .expect("ops walk must yield a top-level Ops");
430
431        let operators_str: Vec<&str> = ops.operators.iter().map(AsRef::as_ref).collect();
432        let operands_str: Vec<&str> = ops.operands.iter().map(AsRef::as_ref).collect();
433
434        // Only the *expectations* are sorted here: `Ops` is documented to
435        // come back byte-lexicographically ordered (#1091), so comparing
436        // against a sorted expectation without re-sorting the actual value
437        // makes every `check_ops` caller an ordering regression test for
438        // its language.
439        correct_operators.sort_unstable();
440        assert_eq!(&operators_str[..], correct_operators);
441
442        correct_operands.sort_unstable();
443        assert_eq!(&operands_str[..], correct_operands);
444    }
445
446    #[test]
447    fn python_ops() {
448        check_ops(
449            LANG::Python,
450            "if True:
451                 a = 1 + 2",
452            "foo.py",
453            &mut ["if", "=", "+"],
454            &mut ["True", "a", "1", "2"],
455        );
456    }
457
458    #[test]
459    fn python_function_ops() {
460        check_ops(
461            LANG::Python,
462            "def foo():
463                 def bar():
464                     def toto():
465                        a = 1 + 1
466                     b = 2 + a
467                 c = 3 + 3",
468            "foo.py",
469            &mut ["def", "=", "+"],
470            &mut ["foo", "bar", "toto", "a", "b", "c", "1", "2", "3"],
471        );
472    }
473
474    #[test]
475    fn cpp_ops() {
476        check_ops(
477            LANG::Cpp,
478            "int a, b, c;
479             float avg;
480             avg = (a + b + c) / 3;",
481            "foo.c",
482            &mut ["int", "float", "()", "=", "+", "/", ",", ";"],
483            &mut ["a", "b", "c", "avg", "3"],
484        );
485    }
486
487    #[test]
488    fn cpp_function_ops() {
489        check_ops(
490            LANG::Cpp,
491            "main()
492            {
493              int a, b, c, avg;
494              scanf(\"%d %d %d\", &a, &b, &c);
495              avg = (a + b + c) / 3;
496              printf(\"avg = %d\", avg);
497            }",
498            "foo.c",
499            &mut ["()", "{}", "int", "&", "=", "+", "/", ",", ";"],
500            &mut [
501                "main",
502                "a",
503                "b",
504                "c",
505                "avg",
506                "scanf",
507                "\"%d %d %d\"",
508                "3",
509                "printf",
510                "\"avg = %d\"",
511            ],
512        );
513    }
514
515    #[test]
516    fn rust_ops() {
517        check_ops(
518            LANG::Rust,
519            "let: usize a = 5; let b: f32 = 7.0; let c: i32 = 3;",
520            "foo.rs",
521            &mut ["let", "usize", "=", ";", "f32", "i32"],
522            &mut ["a", "b", "c", "5", "7.0", "3"],
523        );
524    }
525
526    #[test]
527    fn rust_function_ops() {
528        check_ops(
529            LANG::Rust,
530            "fn main() {
531              let a = 5; let b = 5; let c = 5;
532              let avg = (a + b + c) / 3;
533              println!(\"{}\", avg);
534            }",
535            "foo.rs",
536            &mut ["fn", "()", "{}", "let", "=", "+", "/", ";", "!", ","],
537            &mut ["main", "a", "b", "c", "avg", "5", "3", "println", "\"{}\""],
538        );
539    }
540
541    #[test]
542    fn javascript_ops() {
543        check_ops(
544            LANG::Javascript,
545            "var a, b, c, avg;
546             let x = 1;
547             a = 5; b = 5; c = 5;
548             avg = (a + b + c) / 3;
549             console.log(\"{}\", avg);",
550            "foo.js",
551            &mut ["()", "var", "let", "=", "+", "/", ",", ".", ";"],
552            &mut [
553                "a",
554                "b",
555                "c",
556                "avg",
557                "x",
558                "1",
559                "3",
560                "5",
561                "console.log",
562                "console",
563                "log",
564                "\"{}\"",
565            ],
566        );
567    }
568
569    #[test]
570    fn javascript_function_ops() {
571        check_ops(
572            LANG::Javascript,
573            "function main() {
574              var a, b, c, avg;
575              let x = 1;
576              a = 5; b = 5; c = 5;
577              avg = (a + b + c) / 3;
578              console.log(\"{}\", avg);
579            }",
580            "foo.js",
581            &mut [
582                "function", "()", "{}", "var", "let", "=", "+", "/", ",", ".", ";",
583            ],
584            &mut [
585                "main",
586                "a",
587                "b",
588                "c",
589                "avg",
590                "x",
591                "1",
592                "3",
593                "5",
594                "console.log",
595                "console",
596                "log",
597                "\"{}\"",
598            ],
599        );
600    }
601
602    #[test]
603    fn mozjs_ops() {
604        check_ops(
605            LANG::Mozjs,
606            "var a, b, c, avg;
607             let x = 1;
608             a = 5; b = 5; c = 5;
609             avg = (a + b + c) / 3;
610             console.log(\"{}\", avg);",
611            "foo.js",
612            &mut ["()", "var", "let", "=", "+", "/", ",", ".", ";"],
613            &mut [
614                "a",
615                "b",
616                "c",
617                "avg",
618                "x",
619                "1",
620                "3",
621                "5",
622                "console.log",
623                "console",
624                "log",
625                "\"{}\"",
626            ],
627        );
628    }
629
630    #[test]
631    fn mozjs_function_ops() {
632        check_ops(
633            LANG::Mozjs,
634            "function main() {
635              var a, b, c, avg;
636              let x = 1;
637              a = 5; b = 5; c = 5;
638              avg = (a + b + c) / 3;
639              console.log(\"{}\", avg);
640            }",
641            "foo.js",
642            &mut [
643                "function", "()", "{}", "var", "let", "=", "+", "/", ",", ".", ";",
644            ],
645            &mut [
646                "main",
647                "a",
648                "b",
649                "c",
650                "avg",
651                "x",
652                "1",
653                "3",
654                "5",
655                "console.log",
656                "console",
657                "log",
658                "\"{}\"",
659            ],
660        );
661    }
662
663    #[test]
664    fn typescript_ops() {
665        // Issue #313: the `: string` annotation's `String2` child now
666        // emits a `"string"` operand alongside the `string`
667        // primitive-typed operator (PredefinedType wrapper). Other
668        // type-keyword annotations (`: number`, `: boolean`) are not
669        // string-named kinds, so they only contribute an operator.
670        check_ops(
671            LANG::Typescript,
672            "var a, b, c, avg;
673             let age: number = 32;
674             let name: string = \"John\"; let isUpdated: boolean = true;
675             a = 5; b = 5; c = 5;
676             avg = (a + b + c) / 3;
677             console.log(\"{}\", avg);",
678            "foo.ts",
679            &mut [
680                "()", "var", "let", "string", "number", "boolean", ":", "=", "+", "/", ",", ".",
681                ";",
682            ],
683            &mut [
684                "a",
685                "b",
686                "c",
687                "avg",
688                "age",
689                "name",
690                "isUpdated",
691                "32",
692                "\"John\"",
693                "true",
694                "3",
695                "5",
696                "console.log",
697                "console",
698                "log",
699                "\"{}\"",
700                "string",
701            ],
702        );
703    }
704
705    #[test]
706    fn typescript_function_ops() {
707        // Issue #313: see `typescript_ops` — the `string` type keyword
708        // appears as both an operator (primitive-typed) and an operand
709        // (text `"string"`) once Checker/Getter parity is enforced.
710        check_ops(
711            LANG::Typescript,
712            "function main() {
713              var a, b, c, avg;
714              let age: number = 32;
715              let name: string = \"John\"; let isUpdated: boolean = true;
716              a = 5; b = 5; c = 5;
717              avg = (a + b + c) / 3;
718              console.log(\"{}\", avg);
719            }",
720            "foo.ts",
721            &mut [
722                "function", "()", "{}", "var", "let", "string", "number", "boolean", ":", "=", "+",
723                "/", ",", ".", ";",
724            ],
725            &mut [
726                "main",
727                "a",
728                "b",
729                "c",
730                "avg",
731                "age",
732                "name",
733                "isUpdated",
734                "32",
735                "\"John\"",
736                "true",
737                "3",
738                "5",
739                "console.log",
740                "console",
741                "log",
742                "\"{}\"",
743                "string",
744            ],
745        );
746    }
747
748    #[test]
749    fn tsx_ops() {
750        // Issue #313: TSX exposes the `: string` type-keyword child as
751        // `String3` (vs. TS's `String2`); both are now in the operand
752        // classification, so `"string"` appears as a TSX operand for
753        // the same reason as the TS case above.
754        check_ops(
755            LANG::Tsx,
756            "var a, b, c, avg;
757             let age: number = 32;
758             let name: string = \"John\"; let isUpdated: boolean = true;
759             a = 5; b = 5; c = 5;
760             avg = (a + b + c) / 3;
761             console.log(\"{}\", avg);",
762            "foo.ts",
763            &mut [
764                "()", "var", "let", "string", "number", "boolean", ":", "=", "+", "/", ",", ".",
765                ";",
766            ],
767            &mut [
768                "a",
769                "b",
770                "c",
771                "avg",
772                "age",
773                "name",
774                "isUpdated",
775                "32",
776                "\"John\"",
777                "true",
778                "3",
779                "5",
780                "console.log",
781                "console",
782                "log",
783                "\"{}\"",
784                "string",
785            ],
786        );
787    }
788
789    #[test]
790    fn tsx_function_ops() {
791        // Issue #313: see `tsx_ops` — TSX::String3 (type-keyword
792        // `string`) is now an operand.
793        check_ops(
794            LANG::Tsx,
795            "function main() {
796              var a, b, c, avg;
797              let age: number = 32;
798              let name: string = \"John\"; let isUpdated: boolean = true;
799              a = 5; b = 5; c = 5;
800              avg = (a + b + c) / 3;
801              console.log(\"{}\", avg);
802            }",
803            "foo.ts",
804            &mut [
805                "function", "()", "{}", "var", "let", "string", "number", "boolean", ":", "=", "+",
806                "/", ",", ".", ";",
807            ],
808            &mut [
809                "main",
810                "a",
811                "b",
812                "c",
813                "avg",
814                "age",
815                "name",
816                "isUpdated",
817                "32",
818                "\"John\"",
819                "true",
820                "3",
821                "5",
822                "console.log",
823                "console",
824                "log",
825                "\"{}\"",
826                "string",
827            ],
828        );
829    }
830
831    // Issue #453: a `void` return type (a `predefined_type` wrapper over a
832    // `void` token) and an expression `void` (`void 0`) must collapse to a
833    // single distinct `"void"` operator. `check_ops` asserts the exact
834    // operator list, so a duplicate `"void"` — the pre-fix symptom, where
835    // the wrapper keyed `primitive_operators["void"]` and the inner token
836    // keyed `operators[Void]` — trips the assertion. This pins the lesson-4
837    // `n1 == dedupe(ops.operators)` invariant for the two `void` forms in
838    // one file.
839    #[test]
840    fn typescript_void_return_and_expression_single_operator_453() {
841        check_ops(
842            LANG::Typescript,
843            "function f(): void { return void 0; }",
844            "foo.ts",
845            &mut ["function", "()", "{}", ":", "void", "return", ";"],
846            &mut ["f", "0"],
847        );
848    }
849
850    #[test]
851    fn tsx_void_return_and_expression_single_operator_453() {
852        check_ops(
853            LANG::Tsx,
854            "function f(): void { return void 0; }",
855            "foo.tsx",
856            &mut ["function", "()", "{}", ":", "void", "return", ";"],
857            &mut ["f", "0"],
858        );
859    }
860
861    #[test]
862    fn java_ops() {
863        check_ops(
864            LANG::Java,
865            "public class Main {
866                public static void main(string args[]) {
867                      int a, b, c, avg;
868                      a = 5; b = 5; c = 5;
869                      avg = (a + b + c) / 3;
870                      MessageFormat.format(\"{0}\", avg);
871                    }
872                }",
873            "foo.java",
874            &mut [
875                "{}", "void", "()", "[]", ",", ".", ";", "int", "=", "+", "/",
876            ],
877            &mut [
878                "Main",
879                "main",
880                "args",
881                "a",
882                "b",
883                "c",
884                "avg",
885                "5",
886                "3",
887                "MessageFormat",
888                "format",
889                "\"{0}\"",
890            ],
891        );
892    }
893
894    #[test]
895    fn java_primitive_ops() {
896        check_ops(
897            LANG::Java,
898            "public class Prims {
899                byte a = 1;
900                short b = 2;
901                int c = 3;
902                long d = 4;
903                char e = 'x';
904                float f = 1.0f;
905                double g = 2.0;
906                boolean h = true;
907                boolean i = false;
908            }",
909            "foo.java",
910            // All 8 primitive-type keywords must appear as distinct operators.
911            // true/false appear as operands.
912            &mut [
913                "{}",
914                ";",
915                "=",
916                "byte",
917                "short",
918                "int",
919                "long",
920                "char",
921                "float",
922                "double",
923                "boolean_type",
924            ],
925            &mut [
926                "Prims", "a", "b", "c", "d", "e", "f", "g", "h", "i", "1", "2", "3", "4", "'x'",
927                "1.0f", "2.0", "true", "false",
928            ],
929        );
930    }
931
932    /// A `Unit` space must never carry the synthetic `<anonymous>`
933    /// placeholder that the default getter invents for nodes without a
934    /// `name` field. The public docs describe `None` as the
935    /// "name could not be resolved" state, and the metrics-side
936    /// `FuncSpace::new` already special-cases `SpaceKind::Unit` the same
937    /// way; this pins the `Ops::new` mirror so a regression to the old
938    /// `Some("<anonymous>")` initialisation fails here rather than only
939    /// surfacing for a (currently unreachable) non-top-level `Unit`
940    /// space, where `ops_inner`'s top-level override would not rescue it.
941    /// See issue #755.
942    #[cfg(feature = "rust")]
943    #[test]
944    fn unit_space_name_is_none_not_anonymous() {
945        use crate::getter::Getter;
946        use crate::node::Ancestors;
947        use crate::traits::ParserTrait;
948        use crate::{RustCode, RustParser, SpaceKind};
949
950        let code = b"fn f() {}\n";
951        let parser = RustParser::new(code.to_vec(), std::path::Path::new("foo.rs"), None);
952        let root = parser.root();
953        // The Rust `source_file` root is a `Unit` and has no `name`/`type`
954        // field, so the default getter would invent `<anonymous>`.
955        assert_eq!(SpaceKind::Unit, RustCode::get_space_kind(&root));
956
957        let ops = super::Ops::new::<RustCode>(&root, code, Ancestors::unknown(), SpaceKind::Unit);
958        assert_eq!(
959            ops.name, None,
960            "Unit space must preserve name = None, not invent <anonymous>"
961        );
962    }
963
964    /// Issue #789: an `ERROR`-root parse (here Lua partial input) where
965    /// `metrics()` succeeds must make `ops()` succeed too — the two seams
966    /// should agree. Before the synthetic-Unit-root mirror in `ops_inner`,
967    /// `ops()` returned `Err(MetricsError::EmptyRoot)` because the ERROR
968    /// root is not classified as a function space, so no frame was ever
969    /// pushed. This pins their agreement: both succeed, and the resulting
970    /// top-level `Ops` is a `Unit` whose name is the caller-supplied
971    /// `Source::name` (the intrinsic Unit name stays `None` per #755 until
972    /// `ops_inner` overrides the top-level name).
973    #[cfg(feature = "lua")]
974    #[test]
975    fn lua_error_root_ops_agrees_with_metrics_789() {
976        use crate::{MetricsOptions, SpaceKind};
977
978        // tree-sitter-lua surfaces an ERROR root for this partial input.
979        let src = b"function foo(x)\n  return x +\n".to_vec();
980        let name = "partial.lua".to_owned();
981
982        let ast = Ast::parse(Source::new(LANG::Lua, &src).with_name(Some(name.clone())))
983            .expect("lua feature enabled");
984
985        // metrics() must succeed (it already wrapped a synthetic Unit root).
986        let space = ast
987            .metrics(MetricsOptions::default())
988            .expect("metrics must yield a top-level space");
989        assert_eq!(space.kind, SpaceKind::Unit);
990
991        // ops() must now succeed in the same case rather than returning
992        // Err(EmptyRoot).
993        let ops = ast
994            .ops()
995            .expect("ops must agree with metrics and yield a top-level Ops");
996        assert_eq!(ops.kind, SpaceKind::Unit);
997        assert_eq!(
998            ops.name.as_deref(),
999            Some(name.as_str()),
1000            "top-level Ops name is the caller-supplied Source::name"
1001        );
1002    }
1003
1004    /// Issue #790: `Ops::operands` / `Ops::operators` are the *distinct*
1005    /// (deduplicated) Halstead operand/operator vocabularies (`n2` / `n1`),
1006    /// not every occurrence. Pin the documented dedup semantics: each
1007    /// vector's length equals its unique-element count. The fixture
1008    /// repeats `+`, `;`, and `=` operators and the `a` operand so a
1009    /// regression to non-deduplicated collection would make `len` exceed
1010    /// the unique count.
1011    #[cfg(feature = "rust")]
1012    #[test]
1013    fn ops_vocabularies_are_distinct_790() {
1014        use std::collections::HashSet;
1015
1016        let src = b"fn main() { let a = 1 + 1; let b = a + a; }\n".to_vec();
1017        let ops = Ast::parse(Source::new(LANG::Rust, &src).with_name(Some("foo.rs".to_owned())))
1018            .expect("rust feature enabled")
1019            .ops()
1020            .expect("ops walk must yield a top-level Ops");
1021
1022        let unique_operators: HashSet<&String> = ops.operators.iter().collect();
1023        assert_eq!(
1024            ops.operators.len(),
1025            unique_operators.len(),
1026            "Ops::operators must be the distinct operator vocabulary (n1)"
1027        );
1028
1029        let unique_operands: HashSet<&String> = ops.operands.iter().collect();
1030        assert_eq!(
1031            ops.operands.len(),
1032            unique_operands.len(),
1033            "Ops::operands must be the distinct operand vocabulary (n2)"
1034        );
1035    }
1036
1037    /// Assert that every space in the tree carries sorted vocabularies,
1038    /// and return how many spaces were checked so a caller can prove the
1039    /// walk actually descended.
1040    ///
1041    /// The length floor is what keeps this from going quietly vacuous:
1042    /// `is_sorted` is trivially true for an empty or single-entry
1043    /// vector, so without it a fixture that stopped producing real
1044    /// vocabularies would keep passing while covering nothing.
1045    fn assert_sorted_spaces(ops: &Ops, lang: LANG) -> usize {
1046        /// Smallest vocabulary in which an ordering is observable.
1047        const MIN_OBSERVABLE: usize = 2;
1048
1049        let mut stack = vec![ops];
1050        let mut visited = 0;
1051
1052        while let Some(space) = stack.pop() {
1053            visited += 1;
1054            for (field, values) in [
1055                ("operators", &space.operators),
1056                ("operands", &space.operands),
1057            ] {
1058                assert!(
1059                    values.len() >= MIN_OBSERVABLE && values.is_sorted(),
1060                    "{lang:?} {field} of space {:?} (@{}) must hold at least \
1061                     {MIN_OBSERVABLE} entries and be sorted: {values:?}",
1062                    space.name,
1063                    space.start_line
1064                );
1065            }
1066            stack.extend(space.spaces.iter());
1067        }
1068
1069        visited
1070    }
1071
1072    /// Every space's vocabularies come back sorted, in every language.
1073    ///
1074    /// The operator vocabulary is the union of two maps — one keyed by
1075    /// token id, one keyed by text, which is where primitive types such
1076    /// as C++ `int` land — and sorting is what interleaves them rather
1077    /// than leaving the second concatenated onto the first (#1091). The
1078    /// `spans_both_maps` pair per case names one entry from each map, so
1079    /// a fixture that stopped exercising the text-keyed map fails here
1080    /// instead of silently narrowing the test's reach.
1081    #[test]
1082    fn ops_vocabularies_are_sorted_1091() {
1083        /// `(language, file name, source, (text-keyed operator,
1084        /// token-id-keyed operator that must sort after it))`.
1085        type Case = (
1086            LANG,
1087            &'static str,
1088            &'static str,
1089            (&'static str, &'static str),
1090        );
1091
1092        let cases: &[Case] = &[
1093            #[cfg(feature = "rust")]
1094            (
1095                LANG::Rust,
1096                "rust.rs",
1097                "fn zeta(quux: u32) -> u32 { let mid = quux + 1; \
1098                 let alpha = |beta: u32| beta * mid; alpha(mid) - quux }\n",
1099                ("u32", "|"),
1100            ),
1101            #[cfg(feature = "cpp")]
1102            (
1103                LANG::Cpp,
1104                "cpp.cpp",
1105                "int zeta(int quux) { double mid = quux + 1; \
1106                 char alpha = 'z'; return quux - mid + alpha; }\n",
1107                ("int", "return"),
1108            ),
1109            #[cfg(feature = "java")]
1110            (
1111                LANG::Java,
1112                "Java.java",
1113                "class Zeta { int quux(int mid) { long alpha = mid + 1; \
1114                 boolean beta = alpha > 2; return beta ? mid : 0; } }\n",
1115                ("long", "return"),
1116            ),
1117            #[cfg(feature = "python")]
1118            (
1119                LANG::Python,
1120                "python.py",
1121                "def zeta(quux):\n    mid = quux + 1\n    \
1122                 def alpha(beta):\n        return beta * mid\n    return alpha(mid) - quux\n",
1123                // Python has no primitive-type operators; both entries
1124                // come from the token-id map, so this pair only pins the
1125                // ordering, not the interleaving.
1126                ("def", "return"),
1127            ),
1128            #[cfg(feature = "typescript")]
1129            (
1130                LANG::Typescript,
1131                "ts.ts",
1132                "function zeta(quux: number): number { const mid: number = quux + 1; \
1133                 const alpha = (beta: number) => beta * mid; return alpha(mid) - quux; }\n",
1134                ("number", "return"),
1135            ),
1136        ];
1137
1138        for (lang, file, source, (from_text_map, sorts_after)) in cases {
1139            let ops = Ast::parse(
1140                Source::new(*lang, source.as_bytes()).with_name(Some((*file).to_owned())),
1141            )
1142            .expect("language feature enabled")
1143            .ops()
1144            .expect("ops walk must yield a top-level Ops");
1145
1146            let position = |needle: &str| {
1147                ops.operators
1148                    .iter()
1149                    .position(|op| op == needle)
1150                    .unwrap_or_else(|| {
1151                        panic!(
1152                            "{lang:?} operators must contain {needle:?}: {:?}",
1153                            ops.operators
1154                        )
1155                    })
1156            };
1157            assert!(
1158                position(from_text_map) < position(sorts_after),
1159                "{lang:?} must order {from_text_map:?} before {sorts_after:?}: {:?}",
1160                ops.operators
1161            );
1162
1163            assert!(
1164                assert_sorted_spaces(&ops, *lang) > 1,
1165                "{lang:?} sample must nest at least one sub-space"
1166            );
1167        }
1168    }
1169
1170    /// Two parses of the same bytes in one process must agree exactly.
1171    ///
1172    /// `RandomState` bumps its thread-local seed per instance, so the
1173    /// pre-fix code could — and did — order two `HashMap`s built from
1174    /// identical keys differently within a single run. This is the
1175    /// in-process form of the cross-run churn in #1091.
1176    #[test]
1177    #[cfg(feature = "rust")]
1178    fn ops_are_stable_across_repeated_parses_1091() {
1179        use std::fmt::Write as _;
1180
1181        // Enough distinct operands, in enough distinct spaces, that
1182        // agreement by coincidence is not a plausible explanation for a
1183        // pass.
1184        let mut src = String::new();
1185        for i in 0..40 {
1186            writeln!(src, "fn name{i}(arg{i}: u32) -> u32 {{ arg{i} + {i} }}")
1187                .expect("writing to a String cannot fail");
1188        }
1189
1190        let parse = || {
1191            Ast::parse(Source::new(LANG::Rust, src.as_bytes()).with_name(Some("foo.rs".to_owned())))
1192                .expect("rust feature enabled")
1193                .ops()
1194                .expect("ops walk must yield a top-level Ops")
1195        };
1196
1197        let (first, second) = (parse(), parse());
1198        assert!(
1199            first.operands.len() >= 40 && first.spaces.len() >= 40,
1200            "sample must have a wide vocabulary across many spaces, got {} operands \
1201             in {} spaces",
1202            first.operands.len(),
1203            first.spaces.len()
1204        );
1205        // `Ops` has no `PartialEq`, and the nested spaces are the half a
1206        // top-level vector comparison would miss, so compare the whole
1207        // serialized tree.
1208        let render =
1209            |ops: &Ops| serde_json::to_string(&ops.to_wire()).expect("wire Ops serializes to JSON");
1210        assert_eq!(render(&first), render(&second));
1211    }
1212
1213    /// A vocabulary that lost bytes is ordered by its *rendered* text.
1214    ///
1215    /// #1110 moved the sort ahead of the lossy UTF-8 rendering, which is
1216    /// only order-preserving while every key is valid UTF-8. Here it is
1217    /// not: the two string operands are `"\xffA"` and `"\u{fffd}B"`, so
1218    /// by raw bytes the first sorts *after* the second (`0xff` > `0xef`)
1219    /// and by rendered text — both start `U+FFFD`, then `A` before `B` —
1220    /// it sorts before. The rendered order is what the pre-#1110
1221    /// render-then-sort code produced and what `Ops` documents, so
1222    /// dropping the fallback re-sort flips this pair and fails here.
1223    #[test]
1224    #[cfg(feature = "rust")]
1225    fn ops_vocabulary_orders_lossy_entries_by_rendered_text_1110() {
1226        let mut src = b"fn f() { let a = \"".to_vec();
1227        src.push(0xff);
1228        src.extend_from_slice(b"A\"; let b = \"");
1229        src.extend_from_slice(&[0xef, 0xbf, 0xbd]);
1230        src.extend_from_slice(b"B\"; }\n");
1231
1232        let ops = Ast::parse(Source::new(LANG::Rust, &src).with_name(Some("foo.rs".to_owned())))
1233            .expect("rust feature enabled")
1234            .ops()
1235            .expect("ops walk must yield a top-level Ops");
1236
1237        let position = |needle: &str| {
1238            ops.operands
1239                .iter()
1240                .position(|operand| operand == needle)
1241                .unwrap_or_else(|| panic!("operands must contain {needle:?}: {:?}", ops.operands))
1242        };
1243        assert!(
1244            position("\"\u{fffd}A\"") < position("\"\u{fffd}B\""),
1245            "lossy entries must be ordered by rendered text, got {:?}",
1246            ops.operands
1247        );
1248        assert!(
1249            ops.operands.is_sorted(),
1250            "the whole vocabulary must be sorted as rendered, got {:?}",
1251            ops.operands
1252        );
1253    }
1254
1255    /// The walk classifies a space kind once per space, not once per node.
1256    ///
1257    /// Nothing in the output distinguishes the two: the classification
1258    /// only ever reaches `Ops::new`, so running it on every node produces
1259    /// the same tree and merely throws the extra answers away. #1110
1260    /// moved the call inside the `func_space` branch, mirroring
1261    /// `spaces::compute::open_func_space`; the counter is what makes
1262    /// hoisting it back out a failure. The node-count assertion is what
1263    /// makes the counts distinguishable — a fixture whose nodes and
1264    /// spaces were equal in number could not tell the two apart.
1265    #[test]
1266    // Gated on the language that guarantees a non-empty case list, so
1267    // the emptiness assertion below cannot fire on a minimal build.
1268    #[cfg(feature = "rust")]
1269    fn ops_classifies_space_kind_once_per_space_1110() {
1270        let cases: &[(LANG, &str, &str)] = &[
1271            #[cfg(feature = "rust")]
1272            (
1273                LANG::Rust,
1274                "foo.rs",
1275                "fn outer(a: u32) -> u32 { fn inner(b: u32) -> u32 { b + 1 } inner(a) * 2 }\n",
1276            ),
1277            #[cfg(feature = "python")]
1278            (
1279                LANG::Python,
1280                "foo.py",
1281                "def outer(a):\n    def inner(b):\n        return b + 1\n    return inner(a) * 2\n",
1282            ),
1283            #[cfg(feature = "cpp")]
1284            (
1285                LANG::Cpp,
1286                "foo.cpp",
1287                "struct S { int m(int a) { return a + 1; } }; int f(int b) { return b * 2; }\n",
1288            ),
1289            #[cfg(feature = "java")]
1290            (
1291                LANG::Java,
1292                "Foo.java",
1293                "class C { int m(int a) { return a + 1; } int n(int b) { return b * 2; } }\n",
1294            ),
1295            #[cfg(feature = "javascript")]
1296            (
1297                LANG::Javascript,
1298                "foo.js",
1299                "function outer(a) { function inner(b) { return b + 1; } return inner(a) * 2; }\n",
1300            ),
1301        ];
1302        crate::test_support::assert_fixtures_present(cases);
1303
1304        for (lang, file, source) in cases {
1305            let ast = crate::test_support::parse_named(*lang, file, source);
1306
1307            let before = super::space_kind_lookups::observed();
1308            let ops = ast.ops().expect("ops walk must yield a top-level Ops");
1309            let lookups = super::space_kind_lookups::observed() - before;
1310
1311            let mut spaces = 0;
1312            let mut stack = vec![&ops];
1313            while let Some(space) = stack.pop() {
1314                spaces += 1;
1315                stack.extend(space.spaces.iter());
1316            }
1317
1318            let mut nodes = 0;
1319            let mut cursor = vec![ast.as_tree_sitter().root_node()];
1320            while let Some(node) = cursor.pop() {
1321                nodes += 1;
1322                let mut walker = node.walk();
1323                cursor.extend(node.children(&mut walker));
1324            }
1325
1326            assert!(
1327                nodes > spaces * 4,
1328                "{lang:?} fixture must have many more nodes ({nodes}) than spaces ({spaces}) \
1329                 for the two counts to be distinguishable"
1330            );
1331            assert_eq!(
1332                lookups, spaces,
1333                "{lang:?} must classify once per space, not once per node ({nodes} nodes)"
1334            );
1335        }
1336    }
1337
1338    /// One flattened space: `(depth, kind, name, start_line, end_line)`.
1339    #[cfg(feature = "elixir")]
1340    type FlatSpace = (usize, crate::SpaceKind, String, usize, usize);
1341
1342    /// Flattens an `Ops` tree in preorder, so a test can pin the whole
1343    /// tree in one `assert_eq!` and see the surrounding spaces when one
1344    /// is wrong.
1345    ///
1346    /// `end_line` is carried as well as `start_line` because a change to
1347    /// the promote predicate can move a space's *extent* without moving
1348    /// its head — a `def` that swallows its sibling would keep the same
1349    /// start line.
1350    #[cfg(feature = "elixir")]
1351    fn flatten(ops: &Ops, depth: usize, out: &mut Vec<FlatSpace>) {
1352        out.push((
1353            depth,
1354            ops.kind,
1355            ops.name.clone().unwrap_or_else(|| "<none>".to_owned()),
1356            ops.start_line,
1357            ops.end_line,
1358        ));
1359        for child in &ops.spaces {
1360            flatten(child, depth + 1, out);
1361        }
1362    }
1363
1364    #[cfg(feature = "elixir")]
1365    fn elixir_ops_tree(source: &str) -> Vec<FlatSpace> {
1366        let ops = crate::test_support::parse_named(LANG::Elixir, "foo.ex", source)
1367            .ops()
1368            .expect("ops walk must yield a top-level Ops");
1369        let mut flat = Vec::new();
1370        flatten(&ops, 0, &mut flat);
1371        flat
1372    }
1373
1374    /// Issue #1130: Elixir's `defmodule` / `def` are `Call` nodes whose
1375    /// target identifier text spells the keyword, so only the
1376    /// source-aware promote predicate can recognise them. Before the
1377    /// fix `ops()` returned the bare file-level `Unit` for this input
1378    /// while `metrics()` returned the full module/function tree.
1379    #[cfg(feature = "elixir")]
1380    #[test]
1381    fn elixir_ops_opens_module_and_function_spaces_1130() {
1382        use crate::SpaceKind::{Class, Function, Unit};
1383
1384        assert_eq!(
1385            elixir_ops_tree("defmodule Foo do\n  def bar(x) do\n    x + 1\n  end\nend\n"),
1386            vec![
1387                (0, Unit, "foo.ex".to_owned(), 1, 5),
1388                (1, Class, "Foo".to_owned(), 1, 5),
1389                (2, Function, "bar".to_owned(), 2, 4),
1390            ],
1391        );
1392    }
1393
1394    /// An `AnonymousFunction` is the one Elixir space the byte-less
1395    /// predicate could already see, so this pins that the source-aware
1396    /// predicate did not lose it — and that it nests under the `def`
1397    /// space rather than being reparented to the file root.
1398    #[cfg(feature = "elixir")]
1399    #[test]
1400    fn elixir_ops_opens_anonymous_function_space() {
1401        use crate::SpaceKind::{Class, Function, Unit};
1402
1403        assert_eq!(
1404            elixir_ops_tree(
1405                "defmodule Foo do\n  def bar(list) do\n    \
1406                 Enum.map(list, fn x -> x * 2 end)\n  end\nend\n"
1407            ),
1408            vec![
1409                (0, Unit, "foo.ex".to_owned(), 1, 5),
1410                (1, Class, "Foo".to_owned(), 1, 5),
1411                (2, Function, "bar".to_owned(), 2, 4),
1412                (3, Function, "<anonymous>".to_owned(), 3, 3),
1413            ],
1414        );
1415    }
1416
1417    /// Issue #310: a `def` inside `quote do … end` is a code *template*
1418    /// emitted later by macro expansion, not a declaration of the
1419    /// enclosing module, so it opens no space. This is the case only the
1420    /// source-aware predicate can get right — the byte-less one never
1421    /// saw any `def` at all, so it was accidentally "correct" here while
1422    /// being wrong everywhere else.
1423    #[cfg(feature = "elixir")]
1424    #[test]
1425    fn elixir_ops_skips_def_inside_quote_block_310() {
1426        use crate::SpaceKind::{Class, Function, Unit};
1427
1428        // The quoted `def` heads line 4. Its name resolves to the
1429        // `<anonymous>` placeholder (the head is `unquote(name)`, not a
1430        // literal identifier), so the absence of a fourth entry — not a
1431        // name match — is what pins it out of the tree.
1432        assert_eq!(
1433            elixir_ops_tree(
1434                "defmodule Foo do\n  defmacro gen(name) do\n    quote do\n      \
1435                 def unquote(name)(x) do\n        x + 1\n      end\n    end\n  end\nend\n",
1436            ),
1437            vec![
1438                (0, Unit, "foo.ex".to_owned(), 1, 9),
1439                (1, Class, "Foo".to_owned(), 1, 9),
1440                (2, Function, "gen".to_owned(), 2, 8),
1441            ],
1442        );
1443    }
1444}