1#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
8
9use std::path::{Path, PathBuf};
10
11use serde::Serialize;
12
13use crate::checker::Checker;
14use crate::error::MetricsError;
15use crate::getter::Getter;
16use crate::node::Node;
17use crate::spaces::SpaceKind;
18
19use crate::halstead::{Halstead, HalsteadMaps};
20
21use crate::output::dump_ops::*;
22use crate::traits::*;
23
24#[derive(Debug, Clone, Serialize)]
26pub struct Ops {
27 pub name: Option<String>,
40 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
46 pub name_was_lossy: bool,
47 pub start_line: usize,
49 pub end_line: usize,
51 pub kind: SpaceKind,
53 pub spaces: Vec<Ops>,
55 pub operands: Vec<String>,
57 pub operators: Vec<String>,
59}
60
61impl Ops {
62 fn new<T: Getter>(node: &Node, code: &[u8], kind: SpaceKind) -> Self {
63 let (start_position, end_position) = match kind {
64 SpaceKind::Unit => {
65 if node.child_count() == 0 {
66 (0, 0)
67 } else {
68 (node.start_row() + 1, node.end_row())
69 }
70 }
71 _ => (node.start_row() + 1, node.end_row() + 1),
72 };
73 Self {
74 name: T::get_func_space_name(node, code).map(str::to_owned),
75 name_was_lossy: false,
76 spaces: Vec::new(),
77 kind,
78 start_line: start_position,
79 end_line: end_position,
80 operators: Vec::new(),
81 operands: Vec::new(),
82 }
83 }
84}
85
86#[derive(Debug, Clone)]
87struct State<'a> {
88 ops: Ops,
89 halstead_maps: HalsteadMaps<'a>,
90}
91
92fn bytes_to_string(b: &[u8]) -> String {
98 String::from_utf8_lossy(b).into_owned()
99}
100
101fn compute_operators_and_operands<T: ParserTrait>(state: &mut State) {
102 state.ops.operators = state
103 .halstead_maps
104 .operators
105 .keys()
106 .map(|k| T::Getter::get_operator_id_as_str(*k).to_owned())
107 .collect();
108
109 state.ops.operators.extend(
111 state
112 .halstead_maps
113 .primitive_operators
114 .keys()
115 .map(|k| bytes_to_string(k)),
116 );
117
118 state.ops.operands = state
119 .halstead_maps
120 .operands
121 .keys()
122 .map(|k| bytes_to_string(k))
123 .collect();
124}
125
126fn finalize<T: ParserTrait>(state_stack: &mut Vec<State>, diff_level: usize) {
127 if state_stack.is_empty() {
128 return;
129 }
130
131 for _ in 0..diff_level {
132 if state_stack.len() == 1 {
133 break;
134 }
135 let mut state = state_stack
136 .pop()
137 .expect("state_stack verified to have len >= 2");
138 let last_state = state_stack
139 .last_mut()
140 .expect("state_stack verified to have len >= 1 after pop");
141
142 compute_operators_and_operands::<T>(&mut state);
145
146 last_state.halstead_maps.merge(&state.halstead_maps);
148 last_state.ops.spaces.push(state.ops);
149 }
150
151 if let Some(last_state) = state_stack.last_mut() {
155 compute_operators_and_operands::<T>(last_state);
156 }
157}
158
159#[doc(hidden)]
163pub fn operands_and_operators<'a, T: ParserTrait>(
194 parser: &'a T,
195 path: &'a Path,
196) -> Result<Ops, MetricsError> {
197 let code = parser.get_code();
198 let node = parser.get_root();
199 let mut cursor = node.cursor();
200 let mut stack = Vec::new();
201 let mut children = Vec::new();
202 let mut state_stack: Vec<State> = Vec::new();
203 let mut last_level = 0;
204
205 stack.push((node, 0));
206
207 while let Some((node, level)) = stack.pop() {
208 if level < last_level {
209 finalize::<T>(&mut state_stack, last_level - level);
210 last_level = level;
211 }
212
213 let kind = T::Getter::get_space_kind(&node);
214
215 let func_space = T::Checker::is_func(&node) || T::Checker::is_func_space(&node);
216
217 let new_level = if func_space {
218 let state = State {
219 ops: Ops::new::<T::Getter>(&node, code, kind),
220 halstead_maps: HalsteadMaps::new(),
221 };
222 state_stack.push(state);
223 last_level = level + 1;
224 last_level
225 } else {
226 level
227 };
228
229 if let Some(state) = state_stack.last_mut() {
230 T::Halstead::compute(&node, code, &mut state.halstead_maps);
231 }
232
233 cursor.reset(&node);
234 if cursor.goto_first_child() {
235 loop {
236 children.push((cursor.node(), new_level));
237 if !cursor.goto_next_sibling() {
238 break;
239 }
240 }
241 for child in children.drain(..).rev() {
242 stack.push(child);
243 }
244 }
245 }
246
247 finalize::<T>(&mut state_stack, usize::MAX);
248
249 let mut state = state_stack.pop().ok_or(MetricsError::EmptyRoot)?;
256 let was_lossy = path.to_str().is_none();
262 state.ops.name = Some(path.to_string_lossy().into_owned());
263 state.ops.name_was_lossy = was_lossy;
264 Ok(state.ops)
265}
266
267#[derive(Debug)]
270pub struct OpsCfg {
271 pub path: PathBuf,
273}
274
275pub struct OpsCode {
277 _guard: (),
278}
279
280impl Callback for OpsCode {
281 type Res = std::io::Result<()>;
282 type Cfg = OpsCfg;
283
284 fn call<T: ParserTrait>(cfg: Self::Cfg, parser: &T) -> Self::Res {
285 if let Ok(ops) = operands_and_operators(parser, &cfg.path) {
286 dump_ops(&ops)
287 } else {
288 Ok(())
289 }
290 }
291}
292
293#[cfg(test)]
294#[allow(
295 clippy::float_cmp,
296 clippy::cast_precision_loss,
297 clippy::cast_possible_truncation,
298 clippy::cast_sign_loss,
299 clippy::similar_names,
300 clippy::doc_markdown,
301 clippy::needless_raw_string_hashes,
302 clippy::too_many_lines
303)]
304mod tests {
305 use std::path::PathBuf;
306
307 use crate::{LANG, get_ops};
308
309 #[inline]
310 fn check_ops(
311 lang: LANG,
312 source: &str,
313 file: &str,
314 correct_operators: &mut [&str],
315 correct_operands: &mut [&str],
316 ) {
317 let path = PathBuf::from(file);
318 let mut trimmed_bytes = source.trim_end().trim_matches('\n').as_bytes().to_vec();
319 trimmed_bytes.push(b'\n');
320 let ops = get_ops(&lang, trimmed_bytes, &path, None).unwrap();
321
322 let mut operators_str: Vec<&str> = ops.operators.iter().map(AsRef::as_ref).collect();
323 let mut operands_str: Vec<&str> = ops.operands.iter().map(AsRef::as_ref).collect();
324
325 operators_str.sort_unstable();
327 correct_operators.sort_unstable();
328
329 assert_eq!(&operators_str[..], correct_operators);
330
331 operands_str.sort_unstable();
333 correct_operands.sort_unstable();
334
335 assert_eq!(&operands_str[..], correct_operands);
336 }
337
338 #[test]
339 fn python_ops() {
340 check_ops(
341 LANG::Python,
342 "if True:
343 a = 1 + 2",
344 "foo.py",
345 &mut ["if", "=", "+"],
346 &mut ["True", "a", "1", "2"],
347 );
348 }
349
350 #[test]
351 fn python_function_ops() {
352 check_ops(
353 LANG::Python,
354 "def foo():
355 def bar():
356 def toto():
357 a = 1 + 1
358 b = 2 + a
359 c = 3 + 3",
360 "foo.py",
361 &mut ["def", "=", "+"],
362 &mut ["foo", "bar", "toto", "a", "b", "c", "1", "2", "3"],
363 );
364 }
365
366 #[test]
367 fn cpp_ops() {
368 check_ops(
369 LANG::Cpp,
370 "int a, b, c;
371 float avg;
372 avg = (a + b + c) / 3;",
373 "foo.c",
374 &mut ["int", "float", "()", "=", "+", "/", ",", ";"],
375 &mut ["a", "b", "c", "avg", "3"],
376 );
377 }
378
379 #[test]
380 fn cpp_function_ops() {
381 check_ops(
382 LANG::Cpp,
383 "main()
384 {
385 int a, b, c, avg;
386 scanf(\"%d %d %d\", &a, &b, &c);
387 avg = (a + b + c) / 3;
388 printf(\"avg = %d\", avg);
389 }",
390 "foo.c",
391 &mut ["()", "{}", "int", "&", "=", "+", "/", ",", ";"],
392 &mut [
393 "main",
394 "a",
395 "b",
396 "c",
397 "avg",
398 "scanf",
399 "\"%d %d %d\"",
400 "3",
401 "printf",
402 "\"avg = %d\"",
403 ],
404 );
405 }
406
407 #[test]
408 fn rust_ops() {
409 check_ops(
410 LANG::Rust,
411 "let: usize a = 5; let b: f32 = 7.0; let c: i32 = 3;",
412 "foo.rs",
413 &mut ["let", "usize", "=", ";", "f32", "i32"],
414 &mut ["a", "b", "c", "5", "7.0", "3"],
415 );
416 }
417
418 #[test]
419 fn rust_function_ops() {
420 check_ops(
421 LANG::Rust,
422 "fn main() {
423 let a = 5; let b = 5; let c = 5;
424 let avg = (a + b + c) / 3;
425 println!(\"{}\", avg);
426 }",
427 "foo.rs",
428 &mut ["fn", "()", "{}", "let", "=", "+", "/", ";", "!", ","],
429 &mut ["main", "a", "b", "c", "avg", "5", "3", "println", "\"{}\""],
430 );
431 }
432
433 #[test]
434 fn javascript_ops() {
435 check_ops(
436 LANG::Javascript,
437 "var a, b, c, avg;
438 let x = 1;
439 a = 5; b = 5; c = 5;
440 avg = (a + b + c) / 3;
441 console.log(\"{}\", avg);",
442 "foo.js",
443 &mut ["()", "var", "let", "=", "+", "/", ",", ".", ";"],
444 &mut [
445 "a",
446 "b",
447 "c",
448 "avg",
449 "x",
450 "1",
451 "3",
452 "5",
453 "console.log",
454 "console",
455 "log",
456 "\"{}\"",
457 ],
458 );
459 }
460
461 #[test]
462 fn javascript_function_ops() {
463 check_ops(
464 LANG::Javascript,
465 "function main() {
466 var a, b, c, avg;
467 let x = 1;
468 a = 5; b = 5; c = 5;
469 avg = (a + b + c) / 3;
470 console.log(\"{}\", avg);
471 }",
472 "foo.js",
473 &mut [
474 "function", "()", "{}", "var", "let", "=", "+", "/", ",", ".", ";",
475 ],
476 &mut [
477 "main",
478 "a",
479 "b",
480 "c",
481 "avg",
482 "x",
483 "1",
484 "3",
485 "5",
486 "console.log",
487 "console",
488 "log",
489 "\"{}\"",
490 ],
491 );
492 }
493
494 #[test]
495 fn mozjs_ops() {
496 check_ops(
497 LANG::Mozjs,
498 "var a, b, c, avg;
499 let x = 1;
500 a = 5; b = 5; c = 5;
501 avg = (a + b + c) / 3;
502 console.log(\"{}\", avg);",
503 "foo.js",
504 &mut ["()", "var", "let", "=", "+", "/", ",", ".", ";"],
505 &mut [
506 "a",
507 "b",
508 "c",
509 "avg",
510 "x",
511 "1",
512 "3",
513 "5",
514 "console.log",
515 "console",
516 "log",
517 "\"{}\"",
518 ],
519 );
520 }
521
522 #[test]
523 fn mozjs_function_ops() {
524 check_ops(
525 LANG::Mozjs,
526 "function main() {
527 var a, b, c, avg;
528 let x = 1;
529 a = 5; b = 5; c = 5;
530 avg = (a + b + c) / 3;
531 console.log(\"{}\", avg);
532 }",
533 "foo.js",
534 &mut [
535 "function", "()", "{}", "var", "let", "=", "+", "/", ",", ".", ";",
536 ],
537 &mut [
538 "main",
539 "a",
540 "b",
541 "c",
542 "avg",
543 "x",
544 "1",
545 "3",
546 "5",
547 "console.log",
548 "console",
549 "log",
550 "\"{}\"",
551 ],
552 );
553 }
554
555 #[test]
556 fn typescript_ops() {
557 check_ops(
563 LANG::Typescript,
564 "var a, b, c, avg;
565 let age: number = 32;
566 let name: string = \"John\"; let isUpdated: boolean = true;
567 a = 5; b = 5; c = 5;
568 avg = (a + b + c) / 3;
569 console.log(\"{}\", avg);",
570 "foo.ts",
571 &mut [
572 "()", "var", "let", "string", "number", "boolean", ":", "=", "+", "/", ",", ".",
573 ";",
574 ],
575 &mut [
576 "a",
577 "b",
578 "c",
579 "avg",
580 "age",
581 "name",
582 "isUpdated",
583 "32",
584 "\"John\"",
585 "true",
586 "3",
587 "5",
588 "console.log",
589 "console",
590 "log",
591 "\"{}\"",
592 "string",
593 ],
594 );
595 }
596
597 #[test]
598 fn typescript_function_ops() {
599 check_ops(
603 LANG::Typescript,
604 "function main() {
605 var a, b, c, avg;
606 let age: number = 32;
607 let name: string = \"John\"; let isUpdated: boolean = true;
608 a = 5; b = 5; c = 5;
609 avg = (a + b + c) / 3;
610 console.log(\"{}\", avg);
611 }",
612 "foo.ts",
613 &mut [
614 "function", "()", "{}", "var", "let", "string", "number", "boolean", ":", "=", "+",
615 "/", ",", ".", ";",
616 ],
617 &mut [
618 "main",
619 "a",
620 "b",
621 "c",
622 "avg",
623 "age",
624 "name",
625 "isUpdated",
626 "32",
627 "\"John\"",
628 "true",
629 "3",
630 "5",
631 "console.log",
632 "console",
633 "log",
634 "\"{}\"",
635 "string",
636 ],
637 );
638 }
639
640 #[test]
641 fn tsx_ops() {
642 check_ops(
647 LANG::Tsx,
648 "var a, b, c, avg;
649 let age: number = 32;
650 let name: string = \"John\"; let isUpdated: boolean = true;
651 a = 5; b = 5; c = 5;
652 avg = (a + b + c) / 3;
653 console.log(\"{}\", avg);",
654 "foo.ts",
655 &mut [
656 "()", "var", "let", "string", "number", "boolean", ":", "=", "+", "/", ",", ".",
657 ";",
658 ],
659 &mut [
660 "a",
661 "b",
662 "c",
663 "avg",
664 "age",
665 "name",
666 "isUpdated",
667 "32",
668 "\"John\"",
669 "true",
670 "3",
671 "5",
672 "console.log",
673 "console",
674 "log",
675 "\"{}\"",
676 "string",
677 ],
678 );
679 }
680
681 #[test]
682 fn tsx_function_ops() {
683 check_ops(
686 LANG::Tsx,
687 "function main() {
688 var a, b, c, avg;
689 let age: number = 32;
690 let name: string = \"John\"; let isUpdated: boolean = true;
691 a = 5; b = 5; c = 5;
692 avg = (a + b + c) / 3;
693 console.log(\"{}\", avg);
694 }",
695 "foo.ts",
696 &mut [
697 "function", "()", "{}", "var", "let", "string", "number", "boolean", ":", "=", "+",
698 "/", ",", ".", ";",
699 ],
700 &mut [
701 "main",
702 "a",
703 "b",
704 "c",
705 "avg",
706 "age",
707 "name",
708 "isUpdated",
709 "32",
710 "\"John\"",
711 "true",
712 "3",
713 "5",
714 "console.log",
715 "console",
716 "log",
717 "\"{}\"",
718 "string",
719 ],
720 );
721 }
722
723 #[test]
724 fn java_ops() {
725 check_ops(
726 LANG::Java,
727 "public class Main {
728 public static void main(string args[]) {
729 int a, b, c, avg;
730 a = 5; b = 5; c = 5;
731 avg = (a + b + c) / 3;
732 MessageFormat.format(\"{0}\", avg);
733 }
734 }",
735 "foo.java",
736 &mut [
737 "{}", "void", "()", "[]", ",", ".", ";", "int", "=", "+", "/",
738 ],
739 &mut [
740 "Main",
741 "main",
742 "args",
743 "a",
744 "b",
745 "c",
746 "avg",
747 "5",
748 "3",
749 "MessageFormat",
750 "format",
751 "\"{0}\"",
752 ],
753 );
754 }
755
756 #[test]
757 fn java_primitive_ops() {
758 check_ops(
759 LANG::Java,
760 "public class Prims {
761 byte a = 1;
762 short b = 2;
763 int c = 3;
764 long d = 4;
765 char e = 'x';
766 float f = 1.0f;
767 double g = 2.0;
768 boolean h = true;
769 boolean i = false;
770 }",
771 "foo.java",
772 &mut [
775 "{}",
776 ";",
777 "=",
778 "byte",
779 "short",
780 "int",
781 "long",
782 "char",
783 "float",
784 "double",
785 "boolean_type",
786 ],
787 &mut [
788 "Prims", "a", "b", "c", "d", "e", "f", "g", "h", "i", "1", "2", "3", "4", "'x'",
789 "1.0f", "2.0", "true", "false",
790 ],
791 );
792 }
793
794 #[cfg(unix)]
798 #[test]
799 fn non_utf8_path_yields_lossy_top_level_name() {
800 use std::ffi::OsStr;
801 use std::os::unix::ffi::OsStrExt;
802
803 let raw_bytes: &[u8] = b"foo_\xFF\xFE_bar.py";
804 let path = PathBuf::from(OsStr::from_bytes(raw_bytes));
805 assert!(
806 path.to_str().is_none(),
807 "test premise broken: path must be non-UTF-8 for this test to be meaningful"
808 );
809
810 let ops = get_ops(&LANG::Python, b"a = 1\n".to_vec(), &path, None)
811 .expect("get_ops must yield a top-level Ops");
812
813 let name = ops
814 .name
815 .as_deref()
816 .expect("top-level Ops name must be Some, not the parse-error sentinel None");
817 assert!(
818 name.contains('\u{FFFD}'),
819 "expected U+FFFD replacement char in lossy name, got {name:?}"
820 );
821 assert!(
822 name.starts_with("foo_") && name.ends_with("_bar.py"),
823 "lossy name must preserve the surrounding ASCII bytes, got {name:?}"
824 );
825 assert!(
826 ops.name_was_lossy,
827 "name_was_lossy must be true when the source path was non-UTF-8"
828 );
829 }
830
831 #[test]
834 fn utf8_path_does_not_set_name_was_lossy() {
835 let path = PathBuf::from("foo.py");
836 let ops = get_ops(&LANG::Python, b"a = 1\n".to_vec(), &path, None)
837 .expect("get_ops must yield a top-level Ops");
838 assert!(
839 !ops.name_was_lossy,
840 "name_was_lossy must be false for valid-UTF-8 paths"
841 );
842 }
843}