#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
#![allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
use serde::Serialize;
use serde::ser::{SerializeStruct, Serializer};
use std::fmt;
use crate::checker::Checker;
use crate::macros::implement_metric_trait;
use crate::*;
#[derive(Clone, Debug)]
pub struct Stats {
tokens: usize,
tokens_sum: usize,
tokens_min: usize,
tokens_max: usize,
space_count: usize,
}
impl Default for Stats {
fn default() -> Self {
Self {
tokens: 0,
tokens_sum: 0,
tokens_min: usize::MAX,
tokens_max: 0,
space_count: 1,
}
}
}
impl Serialize for Stats {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut st = serializer.serialize_struct("tokens", 4)?;
st.serialize_field("tokens", &self.tokens_sum())?;
st.serialize_field("tokens_average", &self.tokens_average())?;
st.serialize_field("tokens_min", &self.tokens_min())?;
st.serialize_field("tokens_max", &self.tokens_max())?;
st.end()
}
}
impl fmt::Display for Stats {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"tokens: {}, \
tokens_average: {}, \
tokens_min: {}, \
tokens_max: {}",
self.tokens_sum(),
self.tokens_average(),
self.tokens_min(),
self.tokens_max(),
)
}
}
impl Stats {
pub fn merge(&mut self, other: &Stats) {
self.tokens_min = self.tokens_min.min(other.tokens_min);
self.tokens_max = self.tokens_max.max(other.tokens_max);
self.tokens_sum += other.tokens_sum;
self.space_count += other.space_count;
}
#[inline]
#[must_use]
pub fn tokens_sum(&self) -> f64 {
self.tokens_sum as f64
}
#[inline]
#[must_use]
pub fn tokens_average(&self) -> f64 {
self.tokens_sum() / self.space_count as f64
}
#[inline]
#[must_use]
pub fn tokens_min(&self) -> f64 {
if self.tokens_min == usize::MAX {
0.0
} else {
self.tokens_min as f64
}
}
#[inline]
#[must_use]
pub fn tokens_max(&self) -> f64 {
self.tokens_max as f64
}
#[inline]
pub(crate) fn compute_sum(&mut self) {
self.tokens_sum += self.tokens;
}
#[inline]
pub(crate) fn compute_minmax(&mut self) {
self.tokens_min = self.tokens_min.min(self.tokens);
self.tokens_max = self.tokens_max.max(self.tokens);
self.compute_sum();
}
}
#[doc(hidden)]
pub trait Tokens
where
Self: Checker,
{
fn compute(node: &Node, stats: &mut Stats) {
if node.child_count() != 0 {
return;
}
let in_comment =
std::iter::successors(Some(*node), Node::parent).any(|n| Self::is_comment(&n));
if !in_comment {
stats.tokens += 1;
}
}
}
implement_metric_trait!(
[Tokens],
PythonCode,
MozjsCode,
JavascriptCode,
TypescriptCode,
TsxCode,
CppCode,
RustCode,
PreprocCode,
CcommentCode,
JavaCode,
KotlinCode,
GoCode,
PerlCode,
BashCode,
LuaCode,
TclCode,
PhpCode,
CsharpCode,
ElixirCode,
RubyCode,
GroovyCode
);
#[cfg(test)]
#[allow(
clippy::float_cmp,
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::similar_names,
clippy::doc_markdown,
clippy::needless_raw_string_hashes,
clippy::too_many_lines
)]
mod tests {
use crate::tools::check_metrics;
use super::*;
#[test]
fn python_tokens_exact_count() {
check_metrics::<PythonParser>("def foo(x): return x", "foo.py", |metric| {
assert_eq!(metric.tokens.tokens_sum(), 8.0);
assert!(metric.tokens.tokens_max() >= 7.0);
});
}
#[test]
fn python_tokens_comments_excluded() {
check_metrics::<PythonParser>(
"def foo(x): return x # explanation\n# header\n",
"foo.py",
|metric| {
assert_eq!(metric.tokens.tokens_sum(), 8.0);
},
);
}
#[test]
fn python_tokens_whitespace_excluded() {
check_metrics::<PythonParser>(
"\n\n def foo(x):\n return x\n\n",
"foo.py",
|metric| {
assert_eq!(metric.tokens.tokens_sum(), 8.0);
},
);
}
#[test]
fn python_tokens_distinct_from_halstead() {
check_metrics::<PythonParser>("def foo(x): return (x + 1)", "foo.py", |metric| {
let halstead_total = metric.halstead.operators() + metric.halstead.operands();
assert!(
metric.tokens.tokens_sum() > halstead_total,
"expected tokens ({}) > halstead N1+N2 ({}); punctuation \
like `(`, `)`, `:` should contribute to tokens but not Halstead",
metric.tokens.tokens_sum(),
halstead_total,
);
});
}
#[test]
fn python_tokens_nested_attribution() {
check_metrics::<PythonParser>(
"def outer():\n def inner():\n return 1\n",
"foo.py",
|metric| {
assert_eq!(metric.tokens.tokens_sum(), 12.0);
assert_eq!(metric.tokens.tokens_max(), 7.0);
assert_eq!(metric.tokens.tokens_min(), 0.0);
},
);
}
#[test]
fn cpp_tokens_block_comments_excluded() {
check_metrics::<CppParser>(
"int foo(int x) { /* multi\n line */ return x; }",
"foo.cpp",
|m| {
assert_eq!(m.tokens.tokens_sum(), 11.0);
},
);
check_metrics::<CppParser>("int foo(int x) { return x; }", "foo.cpp", |m| {
assert_eq!(m.tokens.tokens_sum(), 11.0);
});
}
#[test]
fn cpp_tokens_line_comments_excluded() {
check_metrics::<CppParser>("int x = 1; // a one-line comment\n", "foo.cpp", |m| {
assert_eq!(m.tokens.tokens_sum(), 5.0);
});
check_metrics::<CppParser>("int x = 1;\n", "foo.cpp", |m| {
assert_eq!(m.tokens.tokens_sum(), 5.0);
});
}
#[test]
fn cpp_tokens_whitespace_excluded() {
check_metrics::<CppParser>("\n\nint foo(int x) {\n return x;\n}\n", "foo.cpp", |m| {
assert_eq!(m.tokens.tokens_sum(), 11.0);
});
}
#[test]
fn cpp_tokens_distinct_from_halstead() {
check_metrics::<CppParser>("int foo(int x) { return (x + 1); }", "foo.cpp", |m| {
let halstead_total = m.halstead.operators() + m.halstead.operands();
assert!(
m.tokens.tokens_sum() > halstead_total,
"expected tokens ({}) > halstead N1+N2 ({}); punctuation like \
`(`, `)`, `{{`, `}}` and `;` should contribute to tokens but not Halstead",
m.tokens.tokens_sum(),
halstead_total,
);
});
}
#[test]
fn cpp_tokens_nested_attribution() {
check_metrics::<CppParser>(
"int outer() {\n auto inner = []() { return 1; };\n return inner();\n}\n",
"foo.cpp",
|m| {
assert!(m.tokens.tokens_sum() > 0.0, "expected non-zero tokens_sum");
assert!(
m.tokens.tokens_max() >= 7.0,
"expected tokens_max >= 7 (outer scope dominates), got {}",
m.tokens.tokens_max(),
);
assert!(
m.tokens.tokens_max() <= m.tokens.tokens_sum(),
"tokens_max ({}) cannot exceed tokens_sum ({})",
m.tokens.tokens_max(),
m.tokens.tokens_sum(),
);
},
);
}
#[test]
fn java_tokens_line_comments_excluded() {
check_metrics::<JavaParser>(
"class A { void foo() { // hi\n return; } }",
"A.java",
|m| {
assert_eq!(m.tokens.tokens_sum(), 12.0);
},
);
check_metrics::<JavaParser>("class A { void foo() { return; } }", "A.java", |m| {
assert_eq!(m.tokens.tokens_sum(), 12.0);
});
}
#[test]
fn groovy_tokens_line_comments_excluded() {
check_metrics::<GroovyParser>(
"class A { void foo() { // hi\n return\n } }",
"A.groovy",
|m| {
assert_eq!(m.tokens.tokens_sum(), 11.0);
},
);
}
#[test]
fn rust_tokens_doc_comments_excluded() {
check_metrics::<RustParser>(
"/// outer doc\n/// more doc\nfn f() { let x = 1; }",
"foo.rs",
|m| {
assert_eq!(m.tokens.tokens_sum(), 11.0);
},
);
check_metrics::<RustParser>("fn f() { let x = 1; }", "foo.rs", |m| {
assert_eq!(m.tokens.tokens_sum(), 11.0);
});
}
#[test]
fn smoke_python() {
check_metrics::<PythonParser>("x = 1\n", "foo.py", |m| {
assert!(m.tokens.tokens_sum() > 0.0);
});
}
#[test]
fn smoke_rust() {
check_metrics::<RustParser>("fn f() { let x = 1; }", "foo.rs", |m| {
assert!(m.tokens.tokens_sum() > 0.0);
});
}
#[test]
fn smoke_cpp() {
check_metrics::<CppParser>("int x = 1;", "foo.cpp", |m| {
assert!(m.tokens.tokens_sum() > 0.0);
});
}
#[test]
fn smoke_java() {
check_metrics::<JavaParser>("class A { int x = 1; }", "A.java", |m| {
assert!(m.tokens.tokens_sum() > 0.0);
});
}
#[test]
fn smoke_csharp() {
check_metrics::<CsharpParser>("class A { int X = 1; }", "A.cs", |m| {
assert!(m.tokens.tokens_sum() > 0.0);
});
}
#[test]
fn smoke_javascript() {
check_metrics::<JavascriptParser>("let x = 1;", "foo.js", |m| {
assert!(m.tokens.tokens_sum() > 0.0);
});
}
#[test]
fn smoke_mozjs() {
check_metrics::<MozjsParser>("let x = 1;", "foo.js", |m| {
assert!(m.tokens.tokens_sum() > 0.0);
});
}
#[test]
fn smoke_typescript() {
check_metrics::<TypescriptParser>("const x: number = 1;", "foo.ts", |m| {
assert!(m.tokens.tokens_sum() > 0.0);
});
}
#[test]
fn smoke_tsx() {
check_metrics::<TsxParser>("const x: number = 1;", "foo.tsx", |m| {
assert!(m.tokens.tokens_sum() > 0.0);
});
}
#[test]
fn smoke_go() {
check_metrics::<GoParser>("package main\nfunc f() {}", "foo.go", |m| {
assert!(m.tokens.tokens_sum() > 0.0);
});
}
#[test]
fn smoke_kotlin() {
check_metrics::<KotlinParser>("fun f(): Int = 1", "foo.kt", |m| {
assert!(m.tokens.tokens_sum() > 0.0);
});
}
#[test]
fn smoke_lua() {
check_metrics::<LuaParser>("local x = 1", "foo.lua", |m| {
assert!(m.tokens.tokens_sum() > 0.0);
});
}
#[test]
fn smoke_bash() {
check_metrics::<BashParser>("x=1", "foo.sh", |m| {
assert!(m.tokens.tokens_sum() > 0.0);
});
}
#[test]
fn smoke_tcl() {
check_metrics::<TclParser>("set x 1", "foo.tcl", |m| {
assert!(m.tokens.tokens_sum() > 0.0);
});
}
#[test]
fn smoke_perl() {
check_metrics::<PerlParser>("my $x = 1;", "foo.pl", |m| {
assert!(m.tokens.tokens_sum() > 0.0);
});
}
#[test]
fn smoke_php() {
check_metrics::<PhpParser>("<?php $x = 1;", "foo.php", |m| {
assert!(m.tokens.tokens_sum() > 0.0);
});
}
#[test]
fn smoke_preproc() {
check_metrics::<PreprocParser>("#define FOO 1\n", "foo.h", |m| {
assert!(m.tokens.tokens_sum() > 0.0);
});
}
#[test]
fn smoke_ccomment() {
check_metrics::<CcommentParser>("int x = 1;", "foo.c", |m| {
assert!(m.tokens.tokens_sum() > 0.0);
});
}
}