use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use tracing::debug;
use tree_sitter::{Node, Tree};
use crate::ast::AstExtractor;
use crate::callgraph::scanner::{ProjectScanner, ScanConfig};
use crate::error::{Result, BrrrError};
use crate::lang::LanguageRegistry;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HalsteadMetrics {
pub distinct_operators: u32,
pub distinct_operands: u32,
pub total_operators: u32,
pub total_operands: u32,
pub vocabulary: u32,
pub length: u32,
pub calculated_length: f64,
pub volume: f64,
pub difficulty: f64,
pub effort: f64,
pub time_seconds: f64,
pub bugs: f64,
}
impl Default for HalsteadMetrics {
fn default() -> Self {
Self {
distinct_operators: 0,
distinct_operands: 0,
total_operators: 0,
total_operands: 0,
vocabulary: 0,
length: 0,
calculated_length: 0.0,
volume: 0.0,
difficulty: 0.0,
effort: 0.0,
time_seconds: 0.0,
bugs: 0.0,
}
}
}
impl HalsteadMetrics {
#[must_use]
pub fn from_counts(n1: u32, n2: u32, total_n1: u32, total_n2: u32) -> Self {
let vocabulary = n1 + n2;
let length = total_n1 + total_n2;
let calculated_length = if n1 > 0 && n2 > 0 {
f64::from(n1) * f64::from(n1).log2() + f64::from(n2) * f64::from(n2).log2()
} else {
0.0
};
let volume = if vocabulary > 0 {
f64::from(length) * f64::from(vocabulary).log2()
} else {
0.0
};
let difficulty = if n2 > 0 {
(f64::from(n1) / 2.0) * (f64::from(total_n2) / f64::from(n2))
} else {
0.0
};
let effort = difficulty * volume;
let time_seconds = effort / 18.0;
let bugs = volume / 3000.0;
Self {
distinct_operators: n1,
distinct_operands: n2,
total_operators: total_n1,
total_operands: total_n2,
vocabulary,
length,
calculated_length,
volume,
difficulty,
effort,
time_seconds,
bugs,
}
}
#[must_use]
pub fn quality_assessment(&self) -> HalsteadQuality {
let volume_level = if self.volume < 100.0 {
QualityLevel::Low
} else if self.volume < 1000.0 {
QualityLevel::Medium
} else if self.volume < 8000.0 {
QualityLevel::High
} else {
QualityLevel::VeryHigh
};
let difficulty_level = if self.difficulty < 5.0 {
QualityLevel::Low
} else if self.difficulty < 15.0 {
QualityLevel::Medium
} else if self.difficulty < 30.0 {
QualityLevel::High
} else {
QualityLevel::VeryHigh
};
HalsteadQuality {
volume_level,
difficulty_level,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum QualityLevel {
Low,
Medium,
High,
VeryHigh,
}
impl std::fmt::Display for QualityLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Low => write!(f, "low"),
Self::Medium => write!(f, "medium"),
Self::High => write!(f, "high"),
Self::VeryHigh => write!(f, "very_high"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HalsteadQuality {
pub volume_level: QualityLevel,
pub difficulty_level: QualityLevel,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FunctionHalstead {
pub function_name: String,
pub file: PathBuf,
pub line: usize,
pub end_line: usize,
pub metrics: HalsteadMetrics,
pub quality: HalsteadQuality,
#[serde(skip_serializing_if = "Option::is_none")]
pub operators: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub operands: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HalsteadStats {
pub total_functions: usize,
pub avg_volume: f64,
pub max_volume: f64,
pub avg_difficulty: f64,
pub max_difficulty: f64,
pub total_bugs: f64,
pub total_time_seconds: f64,
}
impl HalsteadStats {
fn from_functions(functions: &[FunctionHalstead]) -> Self {
if functions.is_empty() {
return Self {
total_functions: 0,
avg_volume: 0.0,
max_volume: 0.0,
avg_difficulty: 0.0,
max_difficulty: 0.0,
total_bugs: 0.0,
total_time_seconds: 0.0,
};
}
let total = functions.len();
let volumes: Vec<f64> = functions.iter().map(|f| f.metrics.volume).collect();
let difficulties: Vec<f64> = functions.iter().map(|f| f.metrics.difficulty).collect();
let bugs: f64 = functions.iter().map(|f| f.metrics.bugs).sum();
let time: f64 = functions.iter().map(|f| f.metrics.time_seconds).sum();
Self {
total_functions: total,
avg_volume: volumes.iter().sum::<f64>() / total as f64,
max_volume: volumes.iter().cloned().fold(0.0, f64::max),
avg_difficulty: difficulties.iter().sum::<f64>() / total as f64,
max_difficulty: difficulties.iter().cloned().fold(0.0, f64::max),
total_bugs: bugs,
total_time_seconds: time,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HalsteadAnalysis {
pub path: PathBuf,
pub language: Option<String>,
pub functions: Vec<FunctionHalstead>,
pub stats: HalsteadStats,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub errors: Vec<HalsteadError>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HalsteadError {
pub file: PathBuf,
pub message: String,
}
const COMMON_OPERATORS: &[&str] = &[
"+", "-", "*", "/", "%",
"==", "!=", "<", ">", "<=", ">=",
"=", "+=", "-=", "*=", "/=", "%=",
"&", "|", "^", "~", "<<", ">>",
"&=", "|=", "^=", "<<=", ">>=",
"&&", "||", "!",
".", ",", ";",
"(", ")", "[", "]", "{", "}",
"?", ":",
];
const PYTHON_OPERATORS: &[&str] = &[
"**", "//", "@", "->",
"**=", "//=", "@=",
"and", "or", "not", "in", "is",
"if", "else", "elif", "while", "for",
"try", "except", "finally", "raise",
"with", "as", "from", "import",
"def", "class", "return", "yield",
"break", "continue", "pass",
"lambda", "assert", "del",
"global", "nonlocal", "async", "await",
];
const PYTHON_VALUE_KEYWORDS: &[&str] = &["True", "False", "None"];
const TYPESCRIPT_OPERATORS: &[&str] = &[
"=>", "?.", "??", "??=",
"as", "typeof", "instanceof", "keyof", "readonly",
"++", "--",
"if", "else", "while", "for", "do",
"switch", "case", "default",
"try", "catch", "finally", "throw",
"return", "break", "continue",
"function", "class", "new", "delete",
"void", "in", "of",
"import", "export", "from",
"let", "const", "var",
"async", "await", "yield",
"type", "interface", "enum", "namespace",
"extends", "implements",
"...",
];
const TYPESCRIPT_VALUE_KEYWORDS: &[&str] = &["true", "false", "null", "undefined", "this", "super"];
const RUST_OPERATORS: &[&str] = &[
"::", "->", "=>", "..", "..=",
"?",
"&mut",
"if", "else", "while", "for", "loop",
"match", "return", "break", "continue",
"fn", "struct", "enum", "impl", "trait",
"pub", "mod", "use", "crate", "super",
"let", "mut", "ref", "const", "static",
"unsafe", "async", "await", "move",
"where", "type", "as", "in", "dyn",
"box", "yield",
];
const RUST_VALUE_KEYWORDS: &[&str] = &["true", "false", "self", "Self"];
const GO_OPERATORS: &[&str] = &[
":=", "<-", "...",
"if", "else", "for", "range",
"switch", "case", "default", "select",
"func", "return", "break", "continue",
"go", "defer", "chan",
"type", "struct", "interface",
"package", "import", "const", "var",
"fallthrough", "goto", "map",
];
const GO_VALUE_KEYWORDS: &[&str] = &["true", "false", "nil", "iota"];
const JAVA_OPERATORS: &[&str] = &[
"->", "::",
"++", "--",
"instanceof",
"if", "else", "while", "for", "do",
"switch", "case", "default",
"try", "catch", "finally", "throw", "throws",
"return", "break", "continue",
"class", "interface", "enum", "extends", "implements",
"new", "void",
"public", "private", "protected", "static", "final",
"abstract", "synchronized", "volatile", "transient",
"import", "package",
"assert", "native", "strictfp",
];
const JAVA_VALUE_KEYWORDS: &[&str] = &["true", "false", "null", "this", "super"];
const C_CPP_OPERATORS: &[&str] = &[
"->", "::", ".*", "->*",
"++", "--",
"sizeof", "alignof",
"if", "else", "while", "for", "do",
"switch", "case", "default",
"return", "break", "continue", "goto",
"struct", "union", "enum", "class", "typedef",
"const", "volatile", "static", "extern", "register",
"inline", "virtual", "explicit", "friend",
"public", "private", "protected",
"new", "delete", "throw", "try", "catch",
"namespace", "using", "template", "typename",
"auto", "decltype", "constexpr", "noexcept",
];
const C_CPP_VALUE_KEYWORDS: &[&str] = &["true", "false", "nullptr", "NULL", "this"];
struct TokenCollector {
operators: HashMap<String, u32>,
operands: HashMap<String, u32>,
operator_set: HashSet<&'static str>,
value_keywords: HashSet<&'static str>,
#[allow(dead_code)]
language: String,
}
impl TokenCollector {
fn new(language: &str) -> Self {
let mut operator_set: HashSet<&'static str> = COMMON_OPERATORS.iter().copied().collect();
let mut value_keywords: HashSet<&'static str> = HashSet::new();
match language.to_lowercase().as_str() {
"python" => {
operator_set.extend(PYTHON_OPERATORS.iter().copied());
value_keywords.extend(PYTHON_VALUE_KEYWORDS.iter().copied());
}
"typescript" | "javascript" | "tsx" | "jsx" => {
operator_set.extend(TYPESCRIPT_OPERATORS.iter().copied());
value_keywords.extend(TYPESCRIPT_VALUE_KEYWORDS.iter().copied());
}
"rust" => {
operator_set.extend(RUST_OPERATORS.iter().copied());
value_keywords.extend(RUST_VALUE_KEYWORDS.iter().copied());
}
"go" => {
operator_set.extend(GO_OPERATORS.iter().copied());
value_keywords.extend(GO_VALUE_KEYWORDS.iter().copied());
}
"java" => {
operator_set.extend(JAVA_OPERATORS.iter().copied());
value_keywords.extend(JAVA_VALUE_KEYWORDS.iter().copied());
}
"c" | "cpp" | "c++" => {
operator_set.extend(C_CPP_OPERATORS.iter().copied());
value_keywords.extend(C_CPP_VALUE_KEYWORDS.iter().copied());
}
_ => {
operator_set.extend(PYTHON_OPERATORS.iter().copied());
value_keywords.extend(PYTHON_VALUE_KEYWORDS.iter().copied());
}
}
Self {
operators: HashMap::new(),
operands: HashMap::new(),
operator_set,
value_keywords,
language: language.to_lowercase(),
}
}
fn collect_from_node(&mut self, node: Node, source: &[u8]) {
self.visit_node(node, source);
}
fn visit_node(&mut self, node: Node, source: &[u8]) {
let kind = node.kind();
let text = node.utf8_text(source).unwrap_or("");
self.classify_node(kind, text);
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
self.visit_node(child, source);
}
}
fn classify_node(&mut self, kind: &str, text: &str) {
if text.trim().is_empty() {
return;
}
if self.is_operator_kind(kind) || self.operator_set.contains(text) {
self.add_operator(text);
return;
}
if self.is_operand_kind(kind) {
self.add_operand(text);
return;
}
if self.value_keywords.contains(text) {
self.add_operand(text);
return;
}
if self.operator_set.contains(text) {
self.add_operator(text);
}
}
fn is_operator_kind(&self, kind: &str) -> bool {
matches!(
kind,
"binary_operator"
| "unary_operator"
| "comparison_operator"
| "boolean_operator"
| "augmented_assignment"
| "assignment"
| "assignment_expression"
| "+"
| "-"
| "*"
| "/"
| "%"
| "**"
| "//"
| "=="
| "!="
| "<"
| ">"
| "<="
| ">="
| "&&"
| "||"
| "&"
| "|"
| "^"
| "~"
| "<<"
| ">>"
| "="
| "+="
| "-="
| "*="
| "/="
| "!"
| "?"
| "("
| ")"
| "["
| "]"
| "{"
| "}"
| "."
| ","
| ";"
| ":"
| "->"
| "=>"
| "::"
| "?."
| "??"
| "spread_element"
| "rest_pattern"
| "arrow_function"
| "ternary_expression"
| "conditional_expression"
)
}
fn is_operand_kind(&self, kind: &str) -> bool {
matches!(
kind,
"identifier"
| "field_identifier"
| "property_identifier"
| "type_identifier"
| "shorthand_property_identifier"
| "shorthand_field_identifier"
| "number"
| "integer"
| "float"
| "string"
| "string_literal"
| "raw_string_literal"
| "char_literal"
| "boolean"
| "true"
| "false"
| "none"
| "null"
| "nil"
| "template_string"
| "regex"
| "regex_literal"
| "attribute"
| "self"
| "crate"
| "metavariable"
| "this"
| "super"
| "undefined"
)
}
fn add_operator(&mut self, text: &str) {
let normalized = self.normalize_token(text);
if !normalized.is_empty() {
*self.operators.entry(normalized).or_insert(0) += 1;
}
}
fn add_operand(&mut self, text: &str) {
let normalized = self.normalize_token(text);
if !normalized.is_empty() {
*self.operands.entry(normalized).or_insert(0) += 1;
}
}
fn normalize_token(&self, text: &str) -> String {
let trimmed = text.trim();
if (trimmed.starts_with('"') && trimmed.ends_with('"'))
|| (trimmed.starts_with('\'') && trimmed.ends_with('\''))
|| (trimmed.starts_with('`') && trimmed.ends_with('`'))
{
return trimmed.to_string();
}
trimmed.to_string()
}
fn compute_metrics(&self) -> HalsteadMetrics {
let n1 = self.operators.len() as u32;
let n2 = self.operands.len() as u32;
let total_n1: u32 = self.operators.values().sum();
let total_n2: u32 = self.operands.values().sum();
HalsteadMetrics::from_counts(n1, n2, total_n1, total_n2)
}
fn distinct_operators(&self) -> Vec<String> {
let mut ops: Vec<String> = self.operators.keys().cloned().collect();
ops.sort();
ops
}
fn distinct_operands(&self) -> Vec<String> {
let mut ops: Vec<String> = self.operands.keys().cloned().collect();
ops.sort();
ops
}
}
pub fn analyze_halstead(
path: impl AsRef<Path>,
language: Option<&str>,
include_tokens: bool,
) -> Result<HalsteadAnalysis> {
let path = path.as_ref();
if !path.exists() {
return Err(BrrrError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Path not found: {}", path.display()),
)));
}
if path.is_file() {
return analyze_file_halstead(path, include_tokens);
}
let path_str = path.to_str().ok_or_else(|| {
BrrrError::InvalidArgument("Invalid path encoding".to_string())
})?;
let scanner = ProjectScanner::new(path_str)?;
let config = if let Some(lang) = language {
ScanConfig::for_language(lang)
} else {
ScanConfig::default()
};
let scan_result = scanner.scan_with_config(&config)?;
if scan_result.files.is_empty() {
return Err(BrrrError::InvalidArgument(format!(
"No source files found in {} (filter: {:?})",
path.display(),
language
)));
}
debug!("Analyzing {} files for Halstead metrics", scan_result.files.len());
let results: Vec<(Vec<FunctionHalstead>, Vec<HalsteadError>)> = scan_result
.files
.par_iter()
.map(|file| analyze_file_functions_halstead(file, include_tokens))
.collect();
let mut all_functions = Vec::new();
let mut all_errors = Vec::new();
for (functions, errors) in results {
all_functions.extend(functions);
all_errors.extend(errors);
}
let stats = HalsteadStats::from_functions(&all_functions);
Ok(HalsteadAnalysis {
path: path.to_path_buf(),
language: language.map(String::from),
functions: all_functions,
stats,
errors: all_errors,
})
}
pub fn analyze_file_halstead(
file: impl AsRef<Path>,
include_tokens: bool,
) -> Result<HalsteadAnalysis> {
let file = file.as_ref();
if !file.exists() {
return Err(BrrrError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("File not found: {}", file.display()),
)));
}
if !file.is_file() {
return Err(BrrrError::InvalidArgument(format!(
"Expected a file, got directory: {}",
file.display()
)));
}
let (functions, errors) = analyze_file_functions_halstead(file, include_tokens);
let stats = HalsteadStats::from_functions(&functions);
let registry = LanguageRegistry::global();
let language = registry
.detect_language(file)
.map(|l| l.name().to_string());
Ok(HalsteadAnalysis {
path: file.to_path_buf(),
language,
functions,
stats,
errors,
})
}
fn analyze_function_halstead(
file: &Path,
function_name: &str,
start_line: usize,
end_line: usize,
include_tokens: bool,
) -> Result<FunctionHalstead> {
let source = std::fs::read_to_string(file)?;
let registry = LanguageRegistry::global();
let lang = registry
.detect_language(file)
.ok_or_else(|| BrrrError::UnsupportedLanguage(
file.extension()
.and_then(|e| e.to_str())
.unwrap_or("unknown")
.to_string()
))?;
let mut parser = lang.parser()?;
let tree = parser.parse(&source, None).ok_or_else(|| {
BrrrError::Parse {
file: file.display().to_string(),
message: "Failed to parse file".to_string(),
}
})?;
let function_node = find_function_node(&tree, start_line, end_line);
let node_to_analyze = function_node.unwrap_or_else(|| tree.root_node());
let mut collector = TokenCollector::new(lang.name());
collector.collect_from_node(node_to_analyze, source.as_bytes());
let metrics = collector.compute_metrics();
let quality = metrics.quality_assessment();
Ok(FunctionHalstead {
function_name: function_name.to_string(),
file: file.to_path_buf(),
line: start_line,
end_line,
metrics,
quality,
operators: if include_tokens {
Some(collector.distinct_operators())
} else {
None
},
operands: if include_tokens {
Some(collector.distinct_operands())
} else {
None
},
})
}
fn find_function_node(tree: &Tree, start_line: usize, end_line: usize) -> Option<Node<'_>> {
let root = tree.root_node();
find_function_node_recursive(root, start_line, end_line)
}
fn find_function_node_recursive(node: Node<'_>, start_line: usize, end_line: usize) -> Option<Node<'_>> {
let node_start = node.start_position().row + 1; let node_end = node.end_position().row + 1;
if is_function_node(&node) && node_start == start_line && node_end >= end_line {
return Some(node);
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if let Some(found) = find_function_node_recursive(child, start_line, end_line) {
return Some(found);
}
}
None
}
fn is_function_node(node: &Node) -> bool {
matches!(
node.kind(),
"function_definition"
| "function_declaration"
| "method_definition"
| "function_item"
| "function"
| "method"
| "arrow_function"
| "function_expression"
| "method_declaration"
)
}
fn analyze_file_functions_halstead(
file: &Path,
include_tokens: bool,
) -> (Vec<FunctionHalstead>, Vec<HalsteadError>) {
let mut results = Vec::new();
let mut errors = Vec::new();
let module = match AstExtractor::extract_file(file) {
Ok(m) => m,
Err(e) => {
errors.push(HalsteadError {
file: file.to_path_buf(),
message: format!("Failed to parse file: {}", e),
});
return (results, errors);
}
};
for func in &module.functions {
let start_line = func.line_number;
let end_line = func.end_line_number.unwrap_or(start_line);
match analyze_function_halstead(file, &func.name, start_line, end_line, include_tokens) {
Ok(halstead) => results.push(halstead),
Err(e) => {
debug!("Failed to analyze function {}: {}", func.name, e);
errors.push(HalsteadError {
file: file.to_path_buf(),
message: format!("Failed to analyze {}: {}", func.name, e),
});
}
}
}
for class in &module.classes {
for method in &class.methods {
let qualified_name = format!("{}.{}", class.name, method.name);
let start_line = method.line_number;
let end_line = method.end_line_number.unwrap_or(start_line);
match analyze_function_halstead(file, &qualified_name, start_line, end_line, include_tokens) {
Ok(halstead) => results.push(halstead),
Err(e) => {
debug!("Failed to analyze method {}: {}", qualified_name, e);
errors.push(HalsteadError {
file: file.to_path_buf(),
message: format!("Failed to analyze {}: {}", qualified_name, e),
});
}
}
}
}
(results, errors)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
fn create_temp_file(content: &str, extension: &str) -> NamedTempFile {
let mut file = tempfile::Builder::new()
.suffix(extension)
.tempfile()
.expect("Failed to create temp file");
file.write_all(content.as_bytes())
.expect("Failed to write to temp file");
file
}
#[test]
fn test_halstead_from_counts() {
let metrics = HalsteadMetrics::from_counts(5, 4, 10, 8);
assert_eq!(metrics.distinct_operators, 5);
assert_eq!(metrics.distinct_operands, 4);
assert_eq!(metrics.total_operators, 10);
assert_eq!(metrics.total_operands, 8);
assert_eq!(metrics.vocabulary, 9);
assert_eq!(metrics.length, 18);
assert!(metrics.volume > 50.0 && metrics.volume < 60.0);
assert!((metrics.difficulty - 5.0).abs() < 0.1);
}
#[test]
fn test_halstead_zero_counts() {
let metrics = HalsteadMetrics::from_counts(0, 0, 0, 0);
assert_eq!(metrics.vocabulary, 0);
assert_eq!(metrics.length, 0);
assert_eq!(metrics.volume, 0.0);
assert_eq!(metrics.difficulty, 0.0);
assert_eq!(metrics.effort, 0.0);
}
#[test]
fn test_quality_assessment() {
let low = HalsteadMetrics::from_counts(3, 2, 5, 4);
let quality = low.quality_assessment();
assert_eq!(quality.volume_level, QualityLevel::Low);
let high = HalsteadMetrics::from_counts(50, 100, 500, 1000);
let quality = high.quality_assessment();
assert!(matches!(quality.volume_level, QualityLevel::High | QualityLevel::VeryHigh));
}
#[test]
fn test_token_collector_python() {
let collector = TokenCollector::new("python");
assert!(collector.operator_set.contains("and"));
assert!(collector.operator_set.contains("or"));
assert!(collector.operator_set.contains("**"));
assert!(collector.operator_set.contains("//"));
assert!(collector.value_keywords.contains("True"));
assert!(collector.value_keywords.contains("False"));
assert!(collector.value_keywords.contains("None"));
}
#[test]
fn test_token_collector_rust() {
let collector = TokenCollector::new("rust");
assert!(collector.operator_set.contains("::"));
assert!(collector.operator_set.contains("=>"));
assert!(collector.operator_set.contains("?"));
assert!(collector.operator_set.contains("mut"));
assert!(collector.value_keywords.contains("true"));
assert!(collector.value_keywords.contains("false"));
assert!(collector.value_keywords.contains("self"));
}
#[test]
fn test_token_collector_typescript() {
let collector = TokenCollector::new("typescript");
assert!(collector.operator_set.contains("=>"));
assert!(collector.operator_set.contains("?."));
assert!(collector.operator_set.contains("??"));
assert!(collector.operator_set.contains("as"));
assert!(collector.value_keywords.contains("null"));
assert!(collector.value_keywords.contains("undefined"));
assert!(collector.value_keywords.contains("this"));
}
#[test]
fn test_simple_python_function() {
let source = r#"
def add(a, b):
return a + b
"#;
let file = create_temp_file(source, ".py");
let result = analyze_file_halstead(file.path(), true);
assert!(result.is_ok());
let analysis = result.unwrap();
assert_eq!(analysis.functions.len(), 1);
let func = &analysis.functions[0];
assert_eq!(func.function_name, "add");
assert!(func.metrics.distinct_operators > 0);
assert!(func.metrics.distinct_operands > 0);
assert!(func.metrics.volume > 0.0);
}
#[test]
fn test_complex_python_function() {
let source = r#"
def process(items, threshold):
result = []
for item in items:
if item > threshold:
result.append(item * 2)
elif item == threshold:
result.append(item)
return result
"#;
let file = create_temp_file(source, ".py");
let result = analyze_file_halstead(file.path(), true);
assert!(result.is_ok());
let analysis = result.unwrap();
assert_eq!(analysis.functions.len(), 1);
let func = &analysis.functions[0];
assert!(func.metrics.volume > 50.0);
assert!(func.metrics.difficulty > 1.0);
}
#[test]
fn test_typescript_function() {
let source = r#"
function greet(name: string): string {
if (name === "") {
return "Hello, stranger!";
}
return `Hello, ${name}!`;
}
"#;
let file = create_temp_file(source, ".ts");
let result = analyze_file_halstead(file.path(), true);
assert!(result.is_ok());
let analysis = result.unwrap();
assert!(!analysis.functions.is_empty());
}
#[test]
fn test_rust_function() {
let source = r#"
fn factorial(n: u64) -> u64 {
if n <= 1 {
1
} else {
n * factorial(n - 1)
}
}
"#;
let file = create_temp_file(source, ".rs");
let result = analyze_file_halstead(file.path(), true);
assert!(result.is_ok());
let analysis = result.unwrap();
assert_eq!(analysis.functions.len(), 1);
let func = &analysis.functions[0];
assert_eq!(func.function_name, "factorial");
}
#[test]
fn test_class_methods() {
let source = r#"
class Calculator:
def add(self, a, b):
return a + b
def multiply(self, a, b):
return a * b
"#;
let file = create_temp_file(source, ".py");
let result = analyze_file_halstead(file.path(), false);
assert!(result.is_ok());
let analysis = result.unwrap();
assert_eq!(analysis.functions.len(), 2);
let add = analysis.functions.iter().find(|f| f.function_name == "Calculator.add");
let mul = analysis.functions.iter().find(|f| f.function_name == "Calculator.multiply");
assert!(add.is_some());
assert!(mul.is_some());
}
#[test]
fn test_aggregate_statistics() {
let source = r#"
def simple():
return 1
def complex(a, b, c):
result = a + b
if result > c:
return result * 2
else:
return result - c
"#;
let file = create_temp_file(source, ".py");
let result = analyze_file_halstead(file.path(), false);
assert!(result.is_ok());
let analysis = result.unwrap();
assert_eq!(analysis.stats.total_functions, 2);
assert!(analysis.stats.avg_volume > 0.0);
assert!(analysis.stats.max_volume >= analysis.stats.avg_volume);
}
#[test]
fn test_empty_file() {
let source = "# Just a comment\n";
let file = create_temp_file(source, ".py");
let result = analyze_file_halstead(file.path(), false);
assert!(result.is_ok());
let analysis = result.unwrap();
assert_eq!(analysis.functions.len(), 0);
assert_eq!(analysis.stats.total_functions, 0);
}
#[test]
fn test_nonexistent_file() {
let result = analyze_file_halstead("/nonexistent/path/file.py", false);
assert!(result.is_err());
}
#[test]
fn test_go_operators() {
let collector = TokenCollector::new("go");
assert!(collector.operator_set.contains(":="));
assert!(collector.operator_set.contains("<-"));
assert!(collector.operator_set.contains("..."));
assert!(collector.operator_set.contains("range"));
assert!(collector.value_keywords.contains("nil"));
assert!(collector.value_keywords.contains("iota"));
}
#[test]
fn test_java_operators() {
let collector = TokenCollector::new("java");
assert!(collector.operator_set.contains("instanceof"));
assert!(collector.operator_set.contains("->"));
assert!(collector.operator_set.contains("synchronized"));
assert!(collector.value_keywords.contains("null"));
assert!(collector.value_keywords.contains("this"));
assert!(collector.value_keywords.contains("super"));
}
#[test]
fn test_calculated_length_accuracy() {
let metrics = HalsteadMetrics::from_counts(10, 20, 50, 100);
assert!(metrics.calculated_length > 100.0 && metrics.calculated_length < 150.0);
}
#[test]
fn test_time_and_bugs_estimates() {
let metrics = HalsteadMetrics::from_counts(20, 30, 100, 150);
assert!(metrics.time_seconds > 0.0);
assert!(metrics.bugs > 0.0);
assert!(metrics.bugs < metrics.volume / 1000.0);
}
}