1#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
8#![allow(
15 clippy::cast_precision_loss,
16 clippy::cast_possible_truncation,
17 clippy::cast_sign_loss
18)]
19
20use std::fmt;
21
22use crate::checker::Checker;
23use crate::macros::implement_metric_trait;
24
25use crate::*;
26
27#[derive(Clone, Debug, PartialEq)]
40#[non_exhaustive]
41pub struct Stats {
42 tokens: usize,
43 tokens_sum: usize,
44 tokens_min: usize,
45 tokens_max: usize,
46 space_count: usize,
47}
48
49impl Default for Stats {
50 fn default() -> Self {
51 Self {
52 tokens: 0,
53 tokens_sum: 0,
54 tokens_min: usize::MAX,
55 tokens_max: 0,
56 space_count: 1,
57 }
58 }
59}
60
61impl fmt::Display for Stats {
62 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
63 write!(
64 f,
65 "tokens: {}, \
66 tokens_average: {}, \
67 tokens_min: {}, \
68 tokens_max: {}",
69 self.tokens_sum(),
70 self.tokens_average(),
71 self.tokens_min(),
72 self.tokens_max(),
73 )
74 }
75}
76
77impl Stats {
78 pub fn merge(&mut self, other: &Stats) {
80 self.tokens_min = self.tokens_min.min(other.tokens_min);
81 self.tokens_max = self.tokens_max.max(other.tokens_max);
82 self.tokens_sum += other.tokens_sum;
83 self.space_count += other.space_count;
84 }
85
86 #[inline]
88 #[must_use]
89 pub fn tokens_sum(&self) -> u64 {
90 self.tokens_sum as u64
91 }
92
93 #[inline]
95 #[must_use]
96 pub fn tokens_average(&self) -> f64 {
97 crate::metrics::average(self.tokens_sum() as f64, self.space_count)
98 }
99
100 #[inline]
108 #[must_use]
109 pub fn tokens_min(&self) -> u64 {
110 if self.tokens_min == usize::MAX {
111 0
112 } else {
113 self.tokens_min as u64
114 }
115 }
116
117 #[inline]
119 #[must_use]
120 pub fn tokens_max(&self) -> u64 {
121 self.tokens_max as u64
122 }
123
124 #[inline]
125 pub(crate) fn compute_sum(&mut self) {
126 self.tokens_sum += self.tokens;
127 }
128
129 #[inline]
130 pub(crate) fn compute_minmax(&mut self) {
131 self.tokens_min = self.tokens_min.min(self.tokens);
132 self.tokens_max = self.tokens_max.max(self.tokens);
133 self.compute_sum();
134 }
135}
136
137#[doc(hidden)]
138pub(crate) trait Tokens
140where
141 Self: Checker,
142{
143 fn compute(node: &Node, stats: &mut Stats, in_comment: bool) {
155 if in_comment || node.child_count() != 0 {
156 return;
157 }
158 stats.tokens += 1;
159 }
160}
161
162implement_metric_trait!(
163 [Tokens],
164 PythonCode,
165 MozjsCode,
166 JavascriptCode,
167 TypescriptCode,
168 TsxCode,
169 CppCode,
170 MozcppCode,
171 CCode,
172 ObjcCode,
173 RustCode,
174 PreprocCode,
175 CcommentCode,
176 JavaCode,
177 KotlinCode,
178 GoCode,
179 PerlCode,
180 BashCode,
181 LuaCode,
182 TclCode,
183 PhpCode,
184 CsharpCode,
185 ElixirCode,
186 RubyCode,
187 GroovyCode,
188 IrulesCode
189);
190
191#[cfg(test)]
192#[allow(
193 clippy::float_cmp,
194 clippy::cast_precision_loss,
195 clippy::cast_possible_truncation,
196 clippy::cast_sign_loss,
197 clippy::similar_names,
198 clippy::doc_markdown,
199 clippy::needless_raw_string_hashes,
200 clippy::too_many_lines
201)]
202mod tests {
203 use crate::test_support::{check_metrics_only_shim, metrics_verbatim};
204
205 use super::*;
206
207 check_metrics_only_shim!(check_metrics, Tokens);
208 check_metrics_only_shim!(check_tokens_and_halstead, Tokens, Halstead);
213
214 #[cfg(feature = "python")]
217 #[test]
218 fn python_tokens_exact_count() {
219 check_metrics::<PythonParser>("def foo(x): return x", "foo.py", |metric| {
220 assert_eq!(metric.tokens.tokens_sum(), 8);
221 assert!(metric.tokens.tokens_max() >= 7);
222 });
223 }
224
225 #[cfg(feature = "python")]
227 #[test]
228 fn python_tokens_comments_excluded() {
229 check_metrics::<PythonParser>(
230 "def foo(x): return x # explanation\n# header\n",
231 "foo.py",
232 |metric| {
233 assert_eq!(metric.tokens.tokens_sum(), 8);
234 },
235 );
236 }
237
238 #[cfg(feature = "python")]
240 #[test]
241 fn python_tokens_whitespace_excluded() {
242 check_metrics::<PythonParser>(
243 "\n\n def foo(x):\n return x\n\n",
244 "foo.py",
245 |metric| {
246 assert_eq!(metric.tokens.tokens_sum(), 8);
247 },
248 );
249 }
250
251 #[cfg(feature = "python")]
255 #[test]
256 fn python_tokens_distinct_from_halstead() {
257 check_tokens_and_halstead::<PythonParser>(
258 "def foo(x): return (x + 1)",
259 "foo.py",
260 |metric| {
261 let halstead_total =
262 metric.halstead.total_operators() + metric.halstead.total_operands();
263 assert!(
264 metric.tokens.tokens_sum() > halstead_total,
265 "expected tokens ({}) > halstead N1+N2 ({}); punctuation \
266 like `(`, `)`, `:` should contribute to tokens but not Halstead",
267 metric.tokens.tokens_sum(),
268 halstead_total,
269 );
270 },
271 );
272 }
273
274 #[cfg(feature = "python")]
282 #[test]
283 fn python_tokens_nested_attribution() {
284 check_metrics::<PythonParser>(
285 "def outer():\n def inner():\n return 1\n",
286 "foo.py",
287 |metric| {
288 assert_eq!(metric.tokens.tokens_sum(), 12);
289 assert_eq!(metric.tokens.tokens_max(), 7);
290 assert_eq!(metric.tokens.tokens_min(), 0);
291 },
292 );
293 }
294
295 #[cfg(feature = "cpp")]
298 #[test]
299 fn cpp_tokens_block_comments_excluded() {
300 check_metrics::<CppParser>(
301 "int foo(int x) { /* multi\n line */ return x; }",
302 "foo.cpp",
303 |m| {
304 assert_eq!(m.tokens.tokens_sum(), 11);
307 },
308 );
309 check_metrics::<CppParser>("int foo(int x) { return x; }", "foo.cpp", |m| {
310 assert_eq!(m.tokens.tokens_sum(), 11);
311 });
312 }
313
314 #[cfg(feature = "cpp")]
318 #[test]
319 fn cpp_tokens_line_comments_excluded() {
320 check_metrics::<CppParser>("int x = 1; // a one-line comment\n", "foo.cpp", |m| {
321 assert_eq!(m.tokens.tokens_sum(), 5);
322 });
323 check_metrics::<CppParser>("int x = 1;\n", "foo.cpp", |m| {
324 assert_eq!(m.tokens.tokens_sum(), 5);
325 });
326 }
327
328 #[cfg(feature = "cpp")]
331 #[test]
332 fn cpp_tokens_whitespace_excluded() {
333 check_metrics::<CppParser>("\n\nint foo(int x) {\n return x;\n}\n", "foo.cpp", |m| {
334 assert_eq!(m.tokens.tokens_sum(), 11);
336 });
337 }
338
339 #[cfg(feature = "cpp")]
344 #[test]
345 fn cpp_tokens_distinct_from_halstead() {
346 check_tokens_and_halstead::<CppParser>(
347 "int foo(int x) { return (x + 1); }",
348 "foo.cpp",
349 |m| {
350 let halstead_total = m.halstead.total_operators() + m.halstead.total_operands();
351 assert!(
352 m.tokens.tokens_sum() > halstead_total,
353 "expected tokens ({}) > halstead N1+N2 ({}); punctuation like \
354 `(`, `)`, `{{`, `}}` and `;` should contribute to tokens but not Halstead",
355 m.tokens.tokens_sum(),
356 halstead_total,
357 );
358 },
359 );
360 }
361
362 #[cfg(feature = "cpp")]
373 #[test]
374 fn cpp_tokens_nested_attribution() {
375 check_metrics::<CppParser>(
376 "struct S {\n int method(int a) { return a + 1; }\n};\nint outer() { return 2; }\n",
377 "foo.cpp",
378 |m| {
379 assert_eq!(m.tokens.tokens_sum(), 27);
380 assert_eq!(m.tokens.tokens_max(), 13);
381 assert_eq!(m.tokens.tokens_min(), 1);
382 },
383 );
384 }
385
386 #[cfg(feature = "java")]
388 #[test]
389 fn java_tokens_line_comments_excluded() {
390 check_metrics::<JavaParser>(
391 "class A { void foo() { // hi\n return; } }",
392 "A.java",
393 |m| {
394 assert_eq!(m.tokens.tokens_sum(), 12);
396 },
397 );
398 check_metrics::<JavaParser>("class A { void foo() { return; } }", "A.java", |m| {
399 assert_eq!(m.tokens.tokens_sum(), 12);
400 });
401 }
402
403 #[cfg(feature = "groovy")]
404 #[test]
405 fn groovy_tokens_line_comments_excluded() {
406 check_metrics::<GroovyParser>(
408 "class A { void foo() { // hi\n return\n } }",
409 "A.groovy",
410 |m| {
411 assert_eq!(m.tokens.tokens_sum(), 11);
415 },
416 );
417 }
418
419 #[cfg(feature = "javascript")]
423 #[test]
424 fn javascript_tokens_html_comment_excluded() {
425 check_metrics::<JavascriptParser>("<!-- hi -->\nlet x = 1;\n", "foo.js", |m| {
426 assert_eq!(m.tokens.tokens_sum(), 5);
428 });
429 check_metrics::<JavascriptParser>("let x = 1;\n", "foo.js", |m| {
430 assert_eq!(m.tokens.tokens_sum(), 5);
431 });
432 }
433
434 #[cfg(feature = "groovy")]
438 #[test]
439 fn groovy_tokens_groovydoc_excluded() {
440 check_metrics::<GroovyParser>(
441 "/** doc */\nclass A { void f() { return\n } }\n",
442 "A.groovy",
443 |m| {
444 assert_eq!(m.tokens.tokens_sum(), 11);
447 },
448 );
449 check_metrics::<GroovyParser>("class A { void f() { return\n } }\n", "A.groovy", |m| {
450 assert_eq!(m.tokens.tokens_sum(), 11);
451 });
452 }
453
454 #[cfg(feature = "rust")]
463 fn tokens_of(source: &str) -> u64 {
464 metrics_verbatim(
465 crate::LANG::Rust,
466 source.as_bytes(),
467 crate::MetricsOptions::default().with_only(&[crate::Metric::Tokens]),
468 )
469 .tokens
470 .tokens_sum()
471 }
472
473 #[cfg(feature = "rust")]
474 fn nested_parens(depth: usize) -> String {
475 format!(
476 "fn f() -> i32 {{ {}1{} }}\n",
477 "(".repeat(depth),
478 ")".repeat(depth)
479 )
480 }
481
482 #[cfg(feature = "rust")]
506 #[test]
507 fn tokens_count_holds_at_depth() {
508 let shallow = tokens_of(&nested_parens(1));
509 assert_eq!(tokens_of(&nested_parens(2)), shallow + 2);
510 assert_eq!(tokens_of(&nested_parens(2000)), shallow + 2 * 1999);
511 }
512
513 #[cfg(feature = "rust")]
523 #[test]
524 fn rust_tokens_comment_excluded_at_depth() {
525 let deep_block = format!(
526 "fn f() {{ {}let x = 1;{} }}\n",
527 "{ ".repeat(50),
528 " }".repeat(50)
529 );
530 let with_doc = deep_block.replace("let x = 1;", "/// doc\nlet x = 1;");
531 let baseline = tokens_of(&deep_block);
532 assert_eq!(
535 baseline, 111,
536 "expected 111 tokens for the comment-free baseline"
539 );
540 assert_eq!(
541 tokens_of(&with_doc),
542 baseline,
543 "a doc comment 50 blocks deep must contribute no tokens, \
544 including its structured inner leaves"
545 );
546 }
547
548 #[cfg(feature = "rust")]
553 #[test]
554 fn rust_tokens_doc_comments_excluded() {
555 check_metrics::<RustParser>(
556 "/// outer doc\n/// more doc\nfn f() { let x = 1; }",
557 "foo.rs",
558 |m| {
559 assert_eq!(m.tokens.tokens_sum(), 11);
561 },
562 );
563 check_metrics::<RustParser>("fn f() { let x = 1; }", "foo.rs", |m| {
564 assert_eq!(m.tokens.tokens_sum(), 11);
565 });
566 }
567
568 #[cfg(feature = "python")]
577 #[test]
578 fn smoke_python() {
579 check_metrics::<PythonParser>("x = 1\n", "foo.py", |m| {
580 assert!(m.tokens.tokens_sum() > 0);
581 });
582 }
583
584 #[cfg(feature = "rust")]
585 #[test]
586 fn smoke_rust() {
587 check_metrics::<RustParser>("fn f() { let x = 1; }", "foo.rs", |m| {
588 assert!(m.tokens.tokens_sum() > 0);
589 });
590 }
591
592 #[cfg(feature = "cpp")]
593 #[test]
594 fn smoke_cpp() {
595 check_metrics::<CppParser>("int x = 1;", "foo.cpp", |m| {
596 assert!(m.tokens.tokens_sum() > 0);
597 });
598 }
599
600 #[cfg(feature = "java")]
601 #[test]
602 fn smoke_java() {
603 check_metrics::<JavaParser>("class A { int x = 1; }", "A.java", |m| {
604 assert!(m.tokens.tokens_sum() > 0);
605 });
606 }
607
608 #[cfg(feature = "csharp")]
609 #[test]
610 fn smoke_csharp() {
611 check_metrics::<CsharpParser>("class A { int X = 1; }", "A.cs", |m| {
612 assert!(m.tokens.tokens_sum() > 0);
613 });
614 }
615
616 #[cfg(feature = "javascript")]
617 #[test]
618 fn smoke_javascript() {
619 check_metrics::<JavascriptParser>("let x = 1;", "foo.js", |m| {
620 assert!(m.tokens.tokens_sum() > 0);
621 });
622 }
623
624 #[cfg(feature = "mozjs")]
625 #[test]
626 fn smoke_mozjs() {
627 check_metrics::<MozjsParser>("let x = 1;", "foo.js", |m| {
628 assert!(m.tokens.tokens_sum() > 0);
629 });
630 }
631
632 #[cfg(feature = "typescript")]
633 #[test]
634 fn smoke_typescript() {
635 check_metrics::<TypescriptParser>("const x: number = 1;", "foo.ts", |m| {
636 assert!(m.tokens.tokens_sum() > 0);
637 });
638 }
639
640 #[cfg(feature = "typescript")]
641 #[test]
642 fn smoke_tsx() {
643 check_metrics::<TsxParser>("const x: number = 1;", "foo.tsx", |m| {
644 assert!(m.tokens.tokens_sum() > 0);
645 });
646 }
647
648 #[cfg(feature = "go")]
649 #[test]
650 fn smoke_go() {
651 check_metrics::<GoParser>("package main\nfunc f() {}", "foo.go", |m| {
652 assert!(m.tokens.tokens_sum() > 0);
653 });
654 }
655
656 #[cfg(feature = "kotlin")]
657 #[test]
658 fn smoke_kotlin() {
659 check_metrics::<KotlinParser>("fun f(): Int = 1", "foo.kt", |m| {
660 assert!(m.tokens.tokens_sum() > 0);
661 });
662 }
663
664 #[cfg(feature = "lua")]
665 #[test]
666 fn smoke_lua() {
667 check_metrics::<LuaParser>("local x = 1", "foo.lua", |m| {
668 assert!(m.tokens.tokens_sum() > 0);
669 });
670 }
671
672 #[cfg(feature = "bash")]
673 #[test]
674 fn smoke_bash() {
675 check_metrics::<BashParser>("x=1", "foo.sh", |m| {
676 assert!(m.tokens.tokens_sum() > 0);
677 });
678 }
679
680 #[cfg(feature = "tcl")]
681 #[test]
682 fn smoke_tcl() {
683 check_metrics::<TclParser>("set x 1", "foo.tcl", |m| {
684 assert!(m.tokens.tokens_sum() > 0);
685 });
686 }
687
688 #[cfg(feature = "perl")]
689 #[test]
690 fn smoke_perl() {
691 check_metrics::<PerlParser>("my $x = 1;", "foo.pl", |m| {
692 assert!(m.tokens.tokens_sum() > 0);
693 });
694 }
695
696 #[cfg(feature = "php")]
697 #[test]
698 fn smoke_php() {
699 check_metrics::<PhpParser>("<?php $x = 1;", "foo.php", |m| {
700 assert!(m.tokens.tokens_sum() > 0);
701 });
702 }
703
704 #[cfg(any(
705 feature = "c",
706 feature = "c-family-helpers",
707 feature = "cpp",
708 feature = "mozcpp"
709 ))]
710 #[test]
711 fn smoke_preproc() {
712 check_metrics::<PreprocParser>("#define FOO 1\n", "foo.h", |m| {
713 assert!(m.tokens.tokens_sum() > 0);
714 });
715 }
716
717 #[cfg(any(
718 feature = "c",
719 feature = "c-family-helpers",
720 feature = "cpp",
721 feature = "mozcpp"
722 ))]
723 #[test]
724 fn smoke_ccomment() {
725 check_metrics::<CcommentParser>("int x = 1;", "foo.c", |m| {
728 assert!(m.tokens.tokens_sum() > 0);
729 });
730 }
731
732 #[cfg(feature = "c")]
733 #[test]
734 fn smoke_c() {
735 check_metrics::<CParser>("int x = 1;\n", "foo.c", |m| {
736 assert!(m.tokens.tokens_sum() > 0);
737 });
738 }
739
740 #[cfg(feature = "objc")]
741 #[test]
742 fn smoke_objc() {
743 check_metrics::<ObjcParser>("int x = 1;\n", "foo.m", |m| {
744 assert!(m.tokens.tokens_sum() > 0);
745 });
746 }
747
748 #[cfg(feature = "elixir")]
749 #[test]
750 fn smoke_elixir() {
751 check_metrics::<ElixirParser>("defmodule Foo do\n :ok\nend\n", "foo.ex", |m| {
752 assert!(m.tokens.tokens_sum() > 0);
753 });
754 }
755
756 #[cfg(feature = "ruby")]
757 #[test]
758 fn smoke_ruby() {
759 check_metrics::<RubyParser>("def foo\n a = 1\nend\n", "foo.rb", |m| {
760 assert!(m.tokens.tokens_sum() > 0);
761 });
762 }
763
764 #[cfg(feature = "irules")]
765 #[test]
766 fn smoke_irules() {
767 check_metrics::<IrulesParser>("when X {\n set x 1\n}\n", "foo.irule", |m| {
768 assert!(m.tokens.tokens_sum() > 0);
769 });
770 }
771}