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 crate::checker::Checker;
10use crate::error::MetricsError;
11use crate::getter::Getter;
12use crate::node::Node;
13use crate::spaces::{SpaceKind, push_children};
14
15use crate::halstead::{Halstead, HalsteadMaps};
16
17use crate::traits::ParserTrait;
18
19/// All operands and operators of a space.
20#[derive(Debug, Clone)]
21pub struct Ops {
22    /// The name of a function space.
23    ///
24    /// For the top-level (file-level) `Ops` the value is whatever
25    /// `Source::name` the caller supplied to the [`crate::Ast::ops`]
26    /// seam — `Some` or `None`.
27    ///
28    /// For nested spaces, `None` means an error occurred in parsing the
29    /// name of the function space from the AST.
30    pub name: Option<String>,
31    /// `true` when [`Ops::name`] was produced by lossy conversion (the
32    /// original path contained non-UTF-8 bytes and was rendered using
33    /// U+FFFD replacement characters). The explicit-name
34    /// [`crate::Ast::ops`] seam never sets it, since a caller-supplied
35    /// `String` name is UTF-8 by construction, so it is always `false`
36    /// in current code paths. Retained as a wire field for forward
37    /// compatibility; skipped from JSON output when `false` so existing
38    /// schemas keep their shape.
39    pub name_was_lossy: bool,
40    /// The first line of a function space.
41    pub start_line: usize,
42    /// The last line of a function space.
43    pub end_line: usize,
44    /// The space kind.
45    pub kind: SpaceKind,
46    /// All subspaces contained in a function space.
47    pub spaces: Vec<Ops>,
48    /// The **distinct** operands of a space — the deduplicated Halstead
49    /// operand vocabulary (`n2`), one entry per unique operand, not every
50    /// occurrence. Backed by the keys of a `HashMap`, so entries are
51    /// unique and returned in arbitrary order.
52    pub operands: Vec<String>,
53    /// The **distinct** operators of a space — the deduplicated Halstead
54    /// operator vocabulary (`n1`), one entry per unique operator, not
55    /// every occurrence. Backed by the keys of a `HashMap`, so entries
56    /// are unique and returned in arbitrary order.
57    pub operators: Vec<String>,
58}
59
60impl Ops {
61    /// Project this tree into its [`crate::wire::Ops`] form — the
62    /// plain, `Deserialize`-capable record that defines the serialized
63    /// shape.
64    #[must_use]
65    pub fn to_wire(&self) -> crate::wire::Ops {
66        crate::wire::Ops::from(self)
67    }
68
69    fn new<T: Getter>(node: &Node, code: &[u8], kind: SpaceKind) -> Self {
70        let (start_position, end_position) = match kind {
71            SpaceKind::Unit => {
72                if node.child_count() == 0 {
73                    (0, 0)
74                } else {
75                    (node.start_row() + 1, node.end_row())
76                }
77            }
78            _ => (node.start_row() + 1, node.end_row() + 1),
79        };
80        // The top-level Unit's name is overwritten by `ops_inner` with the
81        // caller-supplied name before returning, so computing it here is
82        // wasted work. Non-top-level Unit spaces have no resolvable name, so
83        // leaving `None` matches the documented "could not be resolved"
84        // semantics rather than inventing the `<anonymous>` placeholder the
85        // default getter returns. Other kinds keep the AST-derived name.
86        // Mirrors the `SpaceKind::Unit` handling in `FuncSpace::new`.
87        let name = (kind != SpaceKind::Unit)
88            .then(|| T::get_func_space_name(node, code).map(str::to_owned))
89            .flatten();
90        Self {
91            name,
92            name_was_lossy: false,
93            spaces: Vec::new(),
94            kind,
95            start_line: start_position,
96            end_line: end_position,
97            operators: Vec::new(),
98            operands: Vec::new(),
99        }
100    }
101}
102
103#[derive(Debug, Clone)]
104struct State<'a> {
105    ops: Ops,
106    halstead_maps: HalsteadMaps<'a>,
107}
108
109/// Pushes a synthetic `Unit` root onto the state stack when the grammar
110/// hands us a non-`Unit` root.
111///
112/// Mirrors [`crate::spaces::push_synthetic_unit_root`] on the metrics
113/// seam: some grammars (e.g. tree-sitter-lua / tree-sitter-mozcpp on
114/// unparseable input) return an `ERROR` root that is not classified as a
115/// function space, so without this push the walk would never open a
116/// frame and `ops_inner` would return [`MetricsError::EmptyRoot`] for an
117/// input where `metrics()` succeeds (issue #789). A `Unit` root needs no
118/// wrapper, so nothing is pushed in that case.
119fn push_synthetic_unit_root<T: ParserTrait>(
120    state_stack: &mut Vec<State>,
121    node: &Node,
122    code: &[u8],
123) {
124    if T::Getter::get_space_kind_with_code(node, code) != SpaceKind::Unit {
125        state_stack.push(State {
126            ops: Ops::new::<T::Getter>(node, code, SpaceKind::Unit),
127            halstead_maps: HalsteadMaps::new(),
128        });
129    }
130}
131
132/// Convert `&[u8]` source text to an owned `String`.
133/// Tree-sitter sources are expected to be valid UTF-8; non-UTF-8 bytes
134/// are replaced with the Unicode replacement character to keep the entry
135/// visible (rather than silently dropping it or using a sentinel string
136/// that could collide with a real identifier).
137fn bytes_to_string(b: &[u8]) -> String {
138    String::from_utf8_lossy(b).into_owned()
139}
140
141fn compute_operators_and_operands<T: ParserTrait>(state: &mut State) {
142    state.ops.operators = state
143        .halstead_maps
144        .operators
145        .keys()
146        .map(|k| T::Getter::get_operator_id_as_str(*k).to_owned())
147        .collect();
148
149    // Add primitive-type operators (stored by text in HalsteadMaps)
150    state.ops.operators.extend(
151        state
152            .halstead_maps
153            .primitive_operators
154            .keys()
155            .map(|k| bytes_to_string(k)),
156    );
157
158    state.ops.operands = state
159        .halstead_maps
160        .operands
161        .keys()
162        .map(|k| bytes_to_string(k))
163        .collect();
164}
165
166fn finalize<T: ParserTrait>(state_stack: &mut Vec<State>, diff_level: usize) {
167    if state_stack.is_empty() {
168        return;
169    }
170
171    for _ in 0..diff_level {
172        if state_stack.len() == 1 {
173            break;
174        }
175        let mut state = state_stack
176            .pop()
177            .expect("state_stack verified to have len >= 2");
178        let last_state = state_stack
179            .last_mut()
180            .expect("state_stack verified to have len >= 1 after pop");
181
182        // Populate the child's ops from its HalsteadMaps before
183        // recording it as a sub-space of the parent.
184        compute_operators_and_operands::<T>(&mut state);
185
186        // Merge child's Halstead maps into parent and record child space.
187        last_state.halstead_maps.merge(&state.halstead_maps);
188        last_state.ops.spaces.push(state.ops);
189    }
190
191    // Compute ops for the remaining parent from its fully-merged
192    // HalsteadMaps. This runs once instead of per-iteration, and
193    // produces the deduplicated union of all operators/operands.
194    if let Some(last_state) = state_stack.last_mut() {
195        compute_operators_and_operands::<T>(last_state);
196    }
197}
198
199/// Explicit-name core of the operator/operand walk backing the
200/// [`crate::Ast::ops`] `Source`-based seam. The top-level [`Ops::name`]
201/// is whatever the caller passes in `name`; `name_was_lossy` is left at
202/// its `false` default because an explicit `String` name is never lossy.
203/// Mirrors [`crate::spaces::metrics_inner`].
204pub(crate) fn ops_inner<T: ParserTrait>(
205    parser: &T,
206    name: Option<String>,
207) -> Result<Ops, MetricsError> {
208    let code = parser.code();
209    let node = parser.root();
210    let mut cursor = node.cursor();
211    let mut stack = Vec::new();
212    let mut children = Vec::new();
213    let mut state_stack: Vec<State> = Vec::new();
214    let mut last_level = 0;
215
216    // Mirror `metrics_inner`: wrap a non-`Unit` (e.g. `ERROR`) root in a
217    // synthetic `Unit` frame so the walk always has a frame to populate.
218    // Without this, an `ERROR`-root parse drains the state stack and
219    // `ops_inner` returns `EmptyRoot` for inputs where `metrics()`
220    // succeeds (issue #789).
221    push_synthetic_unit_root::<T>(&mut state_stack, &node, code);
222
223    stack.push((node, 0));
224
225    while let Some((node, level)) = stack.pop() {
226        if level < last_level {
227            finalize::<T>(&mut state_stack, last_level - level);
228            last_level = level;
229        }
230
231        let kind = T::Getter::get_space_kind(&node);
232
233        let func_space = T::Checker::is_func(&node) || T::Checker::is_func_space(&node);
234
235        let new_level = if func_space {
236            let state = State {
237                ops: Ops::new::<T::Getter>(&node, code, kind),
238                halstead_maps: HalsteadMaps::new(),
239            };
240            state_stack.push(state);
241            last_level = level + 1;
242            last_level
243        } else {
244            level
245        };
246
247        if let Some(state) = state_stack.last_mut() {
248            T::Halstead::compute(&node, code, &mut state.halstead_maps);
249        }
250
251        // Shared with `metrics_inner` (issue #969): `push_children` is
252        // State-independent — it only moves the cursor over child nodes —
253        // so unlike the local `finalize` / `push_synthetic_unit_root`
254        // mirrors (which differ by `State` payload) it is reused directly
255        // rather than duplicated. The `children.drain(..).rev()` ordering
256        // it encapsulates is load-bearing for suppression attribution.
257        push_children(&mut cursor, &node, new_level, &mut children, &mut stack);
258    }
259
260    finalize::<T>(&mut state_stack, usize::MAX);
261
262    // Reserved error path: `MetricsError::EmptyRoot` is unreachable
263    // today because the synthetic Unit push above (and every supported
264    // language's root being recognised as a `func_space`) keeps the
265    // state stack non-empty for every input, including ERROR-root,
266    // empty, whitespace-only, and comment-only sources — matching
267    // `metrics_inner`. The `ok_or` is retained so a future walker change
268    // that legitimately drains the stack surfaces a distinct error
269    // variant rather than a bare `None`. See `MetricsError::EmptyRoot`
270    // for the matching variant doc.
271    let mut state = state_stack.pop().ok_or(MetricsError::EmptyRoot)?;
272    state.ops.name = name;
273    Ok(state.ops)
274}
275
276#[cfg(test)]
277#[allow(
278    clippy::float_cmp,
279    clippy::cast_precision_loss,
280    clippy::cast_possible_truncation,
281    clippy::cast_sign_loss,
282    clippy::similar_names,
283    clippy::doc_markdown,
284    clippy::needless_raw_string_hashes,
285    clippy::too_many_lines
286)]
287mod tests {
288    use crate::{Ast, LANG, Source};
289
290    #[inline]
291    fn check_ops(
292        lang: LANG,
293        source: &str,
294        file: &str,
295        correct_operators: &mut [&str],
296        correct_operands: &mut [&str],
297    ) {
298        let mut trimmed_bytes = source.trim_end().trim_matches('\n').as_bytes().to_vec();
299        trimmed_bytes.push(b'\n');
300        let ops = Ast::parse(Source::new(lang, &trimmed_bytes).with_name(Some(file.to_owned())))
301            .expect("language feature enabled")
302            .ops()
303            .expect("ops walk must yield a top-level Ops");
304
305        let mut operators_str: Vec<&str> = ops.operators.iter().map(AsRef::as_ref).collect();
306        let mut operands_str: Vec<&str> = ops.operands.iter().map(AsRef::as_ref).collect();
307
308        // Sorting out operators because they are returned in arbitrary order
309        operators_str.sort_unstable();
310        correct_operators.sort_unstable();
311
312        assert_eq!(&operators_str[..], correct_operators);
313
314        // Sorting out operands because they are returned in arbitrary order
315        operands_str.sort_unstable();
316        correct_operands.sort_unstable();
317
318        assert_eq!(&operands_str[..], correct_operands);
319    }
320
321    #[test]
322    fn python_ops() {
323        check_ops(
324            LANG::Python,
325            "if True:
326                 a = 1 + 2",
327            "foo.py",
328            &mut ["if", "=", "+"],
329            &mut ["True", "a", "1", "2"],
330        );
331    }
332
333    #[test]
334    fn python_function_ops() {
335        check_ops(
336            LANG::Python,
337            "def foo():
338                 def bar():
339                     def toto():
340                        a = 1 + 1
341                     b = 2 + a
342                 c = 3 + 3",
343            "foo.py",
344            &mut ["def", "=", "+"],
345            &mut ["foo", "bar", "toto", "a", "b", "c", "1", "2", "3"],
346        );
347    }
348
349    #[test]
350    fn cpp_ops() {
351        check_ops(
352            LANG::Cpp,
353            "int a, b, c;
354             float avg;
355             avg = (a + b + c) / 3;",
356            "foo.c",
357            &mut ["int", "float", "()", "=", "+", "/", ",", ";"],
358            &mut ["a", "b", "c", "avg", "3"],
359        );
360    }
361
362    #[test]
363    fn cpp_function_ops() {
364        check_ops(
365            LANG::Cpp,
366            "main()
367            {
368              int a, b, c, avg;
369              scanf(\"%d %d %d\", &a, &b, &c);
370              avg = (a + b + c) / 3;
371              printf(\"avg = %d\", avg);
372            }",
373            "foo.c",
374            &mut ["()", "{}", "int", "&", "=", "+", "/", ",", ";"],
375            &mut [
376                "main",
377                "a",
378                "b",
379                "c",
380                "avg",
381                "scanf",
382                "\"%d %d %d\"",
383                "3",
384                "printf",
385                "\"avg = %d\"",
386            ],
387        );
388    }
389
390    #[test]
391    fn rust_ops() {
392        check_ops(
393            LANG::Rust,
394            "let: usize a = 5; let b: f32 = 7.0; let c: i32 = 3;",
395            "foo.rs",
396            &mut ["let", "usize", "=", ";", "f32", "i32"],
397            &mut ["a", "b", "c", "5", "7.0", "3"],
398        );
399    }
400
401    #[test]
402    fn rust_function_ops() {
403        check_ops(
404            LANG::Rust,
405            "fn main() {
406              let a = 5; let b = 5; let c = 5;
407              let avg = (a + b + c) / 3;
408              println!(\"{}\", avg);
409            }",
410            "foo.rs",
411            &mut ["fn", "()", "{}", "let", "=", "+", "/", ";", "!", ","],
412            &mut ["main", "a", "b", "c", "avg", "5", "3", "println", "\"{}\""],
413        );
414    }
415
416    #[test]
417    fn javascript_ops() {
418        check_ops(
419            LANG::Javascript,
420            "var a, b, c, avg;
421             let x = 1;
422             a = 5; b = 5; c = 5;
423             avg = (a + b + c) / 3;
424             console.log(\"{}\", avg);",
425            "foo.js",
426            &mut ["()", "var", "let", "=", "+", "/", ",", ".", ";"],
427            &mut [
428                "a",
429                "b",
430                "c",
431                "avg",
432                "x",
433                "1",
434                "3",
435                "5",
436                "console.log",
437                "console",
438                "log",
439                "\"{}\"",
440            ],
441        );
442    }
443
444    #[test]
445    fn javascript_function_ops() {
446        check_ops(
447            LANG::Javascript,
448            "function main() {
449              var a, b, c, avg;
450              let x = 1;
451              a = 5; b = 5; c = 5;
452              avg = (a + b + c) / 3;
453              console.log(\"{}\", avg);
454            }",
455            "foo.js",
456            &mut [
457                "function", "()", "{}", "var", "let", "=", "+", "/", ",", ".", ";",
458            ],
459            &mut [
460                "main",
461                "a",
462                "b",
463                "c",
464                "avg",
465                "x",
466                "1",
467                "3",
468                "5",
469                "console.log",
470                "console",
471                "log",
472                "\"{}\"",
473            ],
474        );
475    }
476
477    #[test]
478    fn mozjs_ops() {
479        check_ops(
480            LANG::Mozjs,
481            "var a, b, c, avg;
482             let x = 1;
483             a = 5; b = 5; c = 5;
484             avg = (a + b + c) / 3;
485             console.log(\"{}\", avg);",
486            "foo.js",
487            &mut ["()", "var", "let", "=", "+", "/", ",", ".", ";"],
488            &mut [
489                "a",
490                "b",
491                "c",
492                "avg",
493                "x",
494                "1",
495                "3",
496                "5",
497                "console.log",
498                "console",
499                "log",
500                "\"{}\"",
501            ],
502        );
503    }
504
505    #[test]
506    fn mozjs_function_ops() {
507        check_ops(
508            LANG::Mozjs,
509            "function main() {
510              var a, b, c, avg;
511              let x = 1;
512              a = 5; b = 5; c = 5;
513              avg = (a + b + c) / 3;
514              console.log(\"{}\", avg);
515            }",
516            "foo.js",
517            &mut [
518                "function", "()", "{}", "var", "let", "=", "+", "/", ",", ".", ";",
519            ],
520            &mut [
521                "main",
522                "a",
523                "b",
524                "c",
525                "avg",
526                "x",
527                "1",
528                "3",
529                "5",
530                "console.log",
531                "console",
532                "log",
533                "\"{}\"",
534            ],
535        );
536    }
537
538    #[test]
539    fn typescript_ops() {
540        // Issue #313: the `: string` annotation's `String2` child now
541        // emits a `"string"` operand alongside the `string`
542        // primitive-typed operator (PredefinedType wrapper). Other
543        // type-keyword annotations (`: number`, `: boolean`) are not
544        // string-named kinds, so they only contribute an operator.
545        check_ops(
546            LANG::Typescript,
547            "var a, b, c, avg;
548             let age: number = 32;
549             let name: string = \"John\"; let isUpdated: boolean = true;
550             a = 5; b = 5; c = 5;
551             avg = (a + b + c) / 3;
552             console.log(\"{}\", avg);",
553            "foo.ts",
554            &mut [
555                "()", "var", "let", "string", "number", "boolean", ":", "=", "+", "/", ",", ".",
556                ";",
557            ],
558            &mut [
559                "a",
560                "b",
561                "c",
562                "avg",
563                "age",
564                "name",
565                "isUpdated",
566                "32",
567                "\"John\"",
568                "true",
569                "3",
570                "5",
571                "console.log",
572                "console",
573                "log",
574                "\"{}\"",
575                "string",
576            ],
577        );
578    }
579
580    #[test]
581    fn typescript_function_ops() {
582        // Issue #313: see `typescript_ops` — the `string` type keyword
583        // appears as both an operator (primitive-typed) and an operand
584        // (text `"string"`) once Checker/Getter parity is enforced.
585        check_ops(
586            LANG::Typescript,
587            "function main() {
588              var a, b, c, avg;
589              let age: number = 32;
590              let name: string = \"John\"; let isUpdated: boolean = true;
591              a = 5; b = 5; c = 5;
592              avg = (a + b + c) / 3;
593              console.log(\"{}\", avg);
594            }",
595            "foo.ts",
596            &mut [
597                "function", "()", "{}", "var", "let", "string", "number", "boolean", ":", "=", "+",
598                "/", ",", ".", ";",
599            ],
600            &mut [
601                "main",
602                "a",
603                "b",
604                "c",
605                "avg",
606                "age",
607                "name",
608                "isUpdated",
609                "32",
610                "\"John\"",
611                "true",
612                "3",
613                "5",
614                "console.log",
615                "console",
616                "log",
617                "\"{}\"",
618                "string",
619            ],
620        );
621    }
622
623    #[test]
624    fn tsx_ops() {
625        // Issue #313: TSX exposes the `: string` type-keyword child as
626        // `String3` (vs. TS's `String2`); both are now in the operand
627        // classification, so `"string"` appears as a TSX operand for
628        // the same reason as the TS case above.
629        check_ops(
630            LANG::Tsx,
631            "var a, b, c, avg;
632             let age: number = 32;
633             let name: string = \"John\"; let isUpdated: boolean = true;
634             a = 5; b = 5; c = 5;
635             avg = (a + b + c) / 3;
636             console.log(\"{}\", avg);",
637            "foo.ts",
638            &mut [
639                "()", "var", "let", "string", "number", "boolean", ":", "=", "+", "/", ",", ".",
640                ";",
641            ],
642            &mut [
643                "a",
644                "b",
645                "c",
646                "avg",
647                "age",
648                "name",
649                "isUpdated",
650                "32",
651                "\"John\"",
652                "true",
653                "3",
654                "5",
655                "console.log",
656                "console",
657                "log",
658                "\"{}\"",
659                "string",
660            ],
661        );
662    }
663
664    #[test]
665    fn tsx_function_ops() {
666        // Issue #313: see `tsx_ops` — TSX::String3 (type-keyword
667        // `string`) is now an operand.
668        check_ops(
669            LANG::Tsx,
670            "function main() {
671              var a, b, c, avg;
672              let age: number = 32;
673              let name: string = \"John\"; let isUpdated: boolean = true;
674              a = 5; b = 5; c = 5;
675              avg = (a + b + c) / 3;
676              console.log(\"{}\", avg);
677            }",
678            "foo.ts",
679            &mut [
680                "function", "()", "{}", "var", "let", "string", "number", "boolean", ":", "=", "+",
681                "/", ",", ".", ";",
682            ],
683            &mut [
684                "main",
685                "a",
686                "b",
687                "c",
688                "avg",
689                "age",
690                "name",
691                "isUpdated",
692                "32",
693                "\"John\"",
694                "true",
695                "3",
696                "5",
697                "console.log",
698                "console",
699                "log",
700                "\"{}\"",
701                "string",
702            ],
703        );
704    }
705
706    // Issue #453: a `void` return type (a `predefined_type` wrapper over a
707    // `void` token) and an expression `void` (`void 0`) must collapse to a
708    // single distinct `"void"` operator. `check_ops` asserts the exact
709    // operator list, so a duplicate `"void"` — the pre-fix symptom, where
710    // the wrapper keyed `primitive_operators["void"]` and the inner token
711    // keyed `operators[Void]` — trips the assertion. This pins the lesson-4
712    // `n1 == dedupe(ops.operators)` invariant for the two `void` forms in
713    // one file.
714    #[test]
715    fn typescript_void_return_and_expression_single_operator_453() {
716        check_ops(
717            LANG::Typescript,
718            "function f(): void { return void 0; }",
719            "foo.ts",
720            &mut ["function", "()", "{}", ":", "void", "return", ";"],
721            &mut ["f", "0"],
722        );
723    }
724
725    #[test]
726    fn tsx_void_return_and_expression_single_operator_453() {
727        check_ops(
728            LANG::Tsx,
729            "function f(): void { return void 0; }",
730            "foo.tsx",
731            &mut ["function", "()", "{}", ":", "void", "return", ";"],
732            &mut ["f", "0"],
733        );
734    }
735
736    #[test]
737    fn java_ops() {
738        check_ops(
739            LANG::Java,
740            "public class Main {
741                public static void main(string args[]) {
742                      int a, b, c, avg;
743                      a = 5; b = 5; c = 5;
744                      avg = (a + b + c) / 3;
745                      MessageFormat.format(\"{0}\", avg);
746                    }
747                }",
748            "foo.java",
749            &mut [
750                "{}", "void", "()", "[]", ",", ".", ";", "int", "=", "+", "/",
751            ],
752            &mut [
753                "Main",
754                "main",
755                "args",
756                "a",
757                "b",
758                "c",
759                "avg",
760                "5",
761                "3",
762                "MessageFormat",
763                "format",
764                "\"{0}\"",
765            ],
766        );
767    }
768
769    #[test]
770    fn java_primitive_ops() {
771        check_ops(
772            LANG::Java,
773            "public class Prims {
774                byte a = 1;
775                short b = 2;
776                int c = 3;
777                long d = 4;
778                char e = 'x';
779                float f = 1.0f;
780                double g = 2.0;
781                boolean h = true;
782                boolean i = false;
783            }",
784            "foo.java",
785            // All 8 primitive-type keywords must appear as distinct operators.
786            // true/false appear as operands.
787            &mut [
788                "{}",
789                ";",
790                "=",
791                "byte",
792                "short",
793                "int",
794                "long",
795                "char",
796                "float",
797                "double",
798                "boolean_type",
799            ],
800            &mut [
801                "Prims", "a", "b", "c", "d", "e", "f", "g", "h", "i", "1", "2", "3", "4", "'x'",
802                "1.0f", "2.0", "true", "false",
803            ],
804        );
805    }
806
807    /// A `Unit` space must never carry the synthetic `<anonymous>`
808    /// placeholder that the default getter invents for nodes without a
809    /// `name` field. The public docs describe `None` as the
810    /// "name could not be resolved" state, and the metrics-side
811    /// `FuncSpace::new` already special-cases `SpaceKind::Unit` the same
812    /// way; this pins the `Ops::new` mirror so a regression to the old
813    /// `Some("<anonymous>")` initialisation fails here rather than only
814    /// surfacing for a (currently unreachable) non-top-level `Unit`
815    /// space, where `ops_inner`'s top-level override would not rescue it.
816    /// See issue #755.
817    #[cfg(feature = "rust")]
818    #[test]
819    fn unit_space_name_is_none_not_anonymous() {
820        use crate::getter::Getter;
821        use crate::traits::ParserTrait;
822        use crate::{RustCode, RustParser, SpaceKind};
823
824        let code = b"fn f() {}\n";
825        let parser = RustParser::new(code.to_vec(), std::path::Path::new("foo.rs"), None);
826        let root = parser.root();
827        // The Rust `source_file` root is a `Unit` and has no `name`/`type`
828        // field, so the default getter would invent `<anonymous>`.
829        assert_eq!(SpaceKind::Unit, RustCode::get_space_kind(&root));
830
831        let ops = super::Ops::new::<RustCode>(&root, code, SpaceKind::Unit);
832        assert_eq!(
833            ops.name, None,
834            "Unit space must preserve name = None, not invent <anonymous>"
835        );
836    }
837
838    /// Issue #789: an `ERROR`-root parse (here Lua partial input) where
839    /// `metrics()` succeeds must make `ops()` succeed too — the two seams
840    /// should agree. Before the synthetic-Unit-root mirror in `ops_inner`,
841    /// `ops()` returned `Err(MetricsError::EmptyRoot)` because the ERROR
842    /// root is not classified as a function space, so no frame was ever
843    /// pushed. This pins their agreement: both succeed, and the resulting
844    /// top-level `Ops` is a `Unit` whose name is the caller-supplied
845    /// `Source::name` (the intrinsic Unit name stays `None` per #755 until
846    /// `ops_inner` overrides the top-level name).
847    #[cfg(feature = "lua")]
848    #[test]
849    fn lua_error_root_ops_agrees_with_metrics_789() {
850        use crate::{MetricsOptions, SpaceKind};
851
852        // tree-sitter-lua surfaces an ERROR root for this partial input.
853        let src = b"function foo(x)\n  return x +\n".to_vec();
854        let name = "partial.lua".to_owned();
855
856        let ast = Ast::parse(Source::new(LANG::Lua, &src).with_name(Some(name.clone())))
857            .expect("lua feature enabled");
858
859        // metrics() must succeed (it already wrapped a synthetic Unit root).
860        let space = ast
861            .metrics(MetricsOptions::default())
862            .expect("metrics must yield a top-level space");
863        assert_eq!(space.kind, SpaceKind::Unit);
864
865        // ops() must now succeed in the same case rather than returning
866        // Err(EmptyRoot).
867        let ops = ast
868            .ops()
869            .expect("ops must agree with metrics and yield a top-level Ops");
870        assert_eq!(ops.kind, SpaceKind::Unit);
871        assert_eq!(
872            ops.name.as_deref(),
873            Some(name.as_str()),
874            "top-level Ops name is the caller-supplied Source::name"
875        );
876    }
877
878    /// Issue #790: `Ops::operands` / `Ops::operators` are the *distinct*
879    /// (deduplicated) Halstead operand/operator vocabularies (`n2` / `n1`),
880    /// not every occurrence. Pin the documented dedup semantics: each
881    /// vector's length equals its unique-element count. The fixture
882    /// repeats `+`, `;`, and `=` operators and the `a` operand so a
883    /// regression to non-deduplicated collection would make `len` exceed
884    /// the unique count.
885    #[cfg(feature = "rust")]
886    #[test]
887    fn ops_vocabularies_are_distinct_790() {
888        use std::collections::HashSet;
889
890        let src = b"fn main() { let a = 1 + 1; let b = a + a; }\n".to_vec();
891        let ops = Ast::parse(Source::new(LANG::Rust, &src).with_name(Some("foo.rs".to_owned())))
892            .expect("rust feature enabled")
893            .ops()
894            .expect("ops walk must yield a top-level Ops");
895
896        let unique_operators: HashSet<&String> = ops.operators.iter().collect();
897        assert_eq!(
898            ops.operators.len(),
899            unique_operators.len(),
900            "Ops::operators must be the distinct operator vocabulary (n1)"
901        );
902
903        let unique_operands: HashSet<&String> = ops.operands.iter().collect();
904        assert_eq!(
905            ops.operands.len(),
906            unique_operands.len(),
907            "Ops::operands must be the distinct operand vocabulary (n2)"
908        );
909    }
910}