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}