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}