use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtractedFileData {
pub path: PathBuf,
pub functions: Vec<ExtractedFunctionData>,
pub structs: Vec<ExtractedStructData>,
pub impls: Vec<ExtractedImplData>,
pub imports: Vec<ImportInfo>,
pub total_lines: usize,
pub detected_patterns: Vec<DetectedPattern>,
#[serde(default)]
pub test_lines: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum DetectedPattern {
GodObject {
name: String,
field_count: usize,
},
LongFunction {
name: String,
lines: usize,
},
ManyParameters {
name: String,
param_count: usize,
},
DeepNesting {
function_name: String,
depth: u32,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtractedFunctionData {
pub name: String,
pub qualified_name: String,
pub line: usize,
pub end_line: usize,
pub length: usize,
pub cyclomatic: u32,
pub cognitive: u32,
pub nesting: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub entropy_score: Option<crate::complexity::entropy_core::EntropyScore>,
pub purity_analysis: PurityAnalysisData,
pub io_operations: Vec<IoOperation>,
pub parameter_names: Vec<String>,
pub transformation_patterns: Vec<TransformationPattern>,
pub calls: Vec<CallSite>,
pub is_test: bool,
pub is_async: bool,
pub visibility: Option<String>,
pub is_trait_method: bool,
pub in_test_module: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PurityAnalysisData {
pub is_pure: bool,
pub has_mutations: bool,
pub has_io_operations: bool,
pub has_unsafe: bool,
pub local_mutations: Vec<String>,
pub upvalue_mutations: Vec<String>,
pub total_mutations: usize,
pub var_names: HashMap<usize, String>,
pub confidence: f32,
pub purity_level: PurityLevel,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum PurityLevel {
StrictlyPure,
LocallyPure,
ReadOnly,
#[default]
Impure,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtractedStructData {
pub name: String,
pub line: usize,
pub fields: Vec<FieldInfo>,
pub is_public: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldInfo {
pub name: String,
pub type_str: String,
pub is_public: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtractedImplData {
pub type_name: String,
pub trait_name: Option<String>,
pub methods: Vec<MethodInfo>,
pub line: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MethodInfo {
pub name: String,
pub line: usize,
pub is_public: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallSite {
pub callee_name: String,
pub call_type: CallType,
pub line: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CallType {
Direct,
Method,
StaticMethod,
TraitMethod,
Closure,
FunctionPointer,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImportInfo {
pub path: String,
pub alias: Option<String>,
pub is_glob: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IoOperation {
pub io_type: IoType,
pub description: String,
pub line: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum IoType {
File,
Console,
Network,
Database,
AsyncIO,
Environment,
System,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransformationPattern {
pub pattern_type: PatternType,
pub line: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PatternType {
Map,
Filter,
Fold,
FlatMap,
Collect,
ForEach,
Find,
Any,
All,
Reduce,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct LocMetrics {
pub total_loc: usize,
pub production_loc: usize,
pub test_loc: usize,
pub test_percentage: u8,
}
impl LocMetrics {
pub fn new(total_loc: usize, test_loc: usize) -> Self {
let production_loc = total_loc.saturating_sub(test_loc);
let test_percentage = if total_loc > 0 {
((test_loc as f64 / total_loc as f64) * 100.0).min(100.0) as u8
} else {
0
};
Self {
total_loc,
production_loc,
test_loc,
test_percentage,
}
}
pub fn has_significant_tests(&self) -> bool {
self.test_percentage >= 25
}
pub fn display_loc(&self) -> String {
if self.test_loc > 0 && self.has_significant_tests() {
format!("{} ({} with tests)", self.production_loc, self.total_loc)
} else {
format!("{}", self.total_loc)
}
}
}
impl Default for LocMetrics {
fn default() -> Self {
Self::new(0, 0)
}
}
impl ExtractedFileData {
pub fn empty(path: PathBuf) -> Self {
Self {
path,
functions: Vec::new(),
structs: Vec::new(),
impls: Vec::new(),
imports: Vec::new(),
total_lines: 0,
detected_patterns: Vec::new(),
test_lines: 0,
}
}
pub fn production_lines(&self) -> usize {
self.total_lines.saturating_sub(self.test_lines)
}
pub fn loc_metrics(&self) -> LocMetrics {
LocMetrics::new(self.total_lines, self.test_lines)
}
pub fn has_content(&self) -> bool {
!self.functions.is_empty() || !self.structs.is_empty() || !self.impls.is_empty()
}
pub fn function_count(&self) -> usize {
self.functions.len()
}
}
impl ExtractedFunctionData {
pub fn function_id(
&self,
file_path: &std::path::Path,
) -> crate::priority::call_graph::FunctionId {
crate::priority::call_graph::FunctionId::new(
file_path.to_path_buf(),
self.name.clone(),
self.line,
)
}
#[cfg(test)]
pub fn minimal(name: &str, line: usize) -> Self {
Self {
name: name.to_string(),
qualified_name: name.to_string(),
line,
end_line: line + 1,
length: 1,
cyclomatic: 1,
cognitive: 0,
nesting: 0,
entropy_score: None,
purity_analysis: PurityAnalysisData::default(),
io_operations: Vec::new(),
parameter_names: Vec::new(),
transformation_patterns: Vec::new(),
calls: Vec::new(),
is_test: false,
is_async: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
}
}
}
impl Default for ExtractedFunctionData {
fn default() -> Self {
Self {
name: String::new(),
qualified_name: String::new(),
line: 0,
end_line: 0,
length: 0,
cyclomatic: 1,
cognitive: 0,
nesting: 0,
entropy_score: None,
purity_analysis: PurityAnalysisData::default(),
io_operations: Vec::new(),
parameter_names: Vec::new(),
transformation_patterns: Vec::new(),
calls: Vec::new(),
is_test: false,
is_async: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
}
}
}
impl PurityAnalysisData {
pub fn pure() -> Self {
Self {
is_pure: true,
has_mutations: false,
has_io_operations: false,
has_unsafe: false,
local_mutations: Vec::new(),
upvalue_mutations: Vec::new(),
total_mutations: 0,
var_names: HashMap::new(),
confidence: 1.0,
purity_level: PurityLevel::StrictlyPure,
}
}
pub fn impure(reason: &str) -> Self {
Self {
is_pure: false,
has_mutations: true,
has_io_operations: false,
has_unsafe: false,
local_mutations: vec![reason.to_string()],
upvalue_mutations: Vec::new(),
total_mutations: 1,
var_names: HashMap::new(),
confidence: 1.0,
purity_level: PurityLevel::Impure,
}
}
}
fn _assert_send_sync<T: Send + Sync>() {}
#[allow(dead_code)]
const _: () = {
let _ = _assert_send_sync::<ExtractedFileData>;
let _ = _assert_send_sync::<ExtractedFunctionData>;
let _ = _assert_send_sync::<PurityAnalysisData>;
let _ = _assert_send_sync::<ExtractedStructData>;
let _ = _assert_send_sync::<ExtractedImplData>;
let _ = _assert_send_sync::<CallSite>;
let _ = _assert_send_sync::<IoOperation>;
let _ = _assert_send_sync::<TransformationPattern>;
let _ = _assert_send_sync::<DetectedPattern>;
let _ = _assert_send_sync::<LocMetrics>;
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extracted_file_data_empty() {
let data = ExtractedFileData::empty(PathBuf::from("test.rs"));
assert_eq!(data.path, PathBuf::from("test.rs"));
assert!(data.functions.is_empty());
assert!(data.structs.is_empty());
assert!(data.impls.is_empty());
assert!(data.imports.is_empty());
assert_eq!(data.total_lines, 0);
assert!(!data.has_content());
}
#[test]
fn test_extracted_file_data_has_content() {
let mut data = ExtractedFileData::empty(PathBuf::from("test.rs"));
assert!(!data.has_content());
data.functions
.push(ExtractedFunctionData::minimal("foo", 1));
assert!(data.has_content());
}
#[test]
fn test_extracted_function_data_minimal() {
let func = ExtractedFunctionData::minimal("test_fn", 42);
assert_eq!(func.name, "test_fn");
assert_eq!(func.line, 42);
assert_eq!(func.cyclomatic, 1);
assert!(!func.is_test);
}
#[test]
fn test_purity_analysis_data_pure() {
let purity = PurityAnalysisData::pure();
assert!(purity.is_pure);
assert!(!purity.has_mutations);
assert!(!purity.has_io_operations);
assert_eq!(purity.purity_level, PurityLevel::StrictlyPure);
assert_eq!(purity.confidence, 1.0);
}
#[test]
fn test_purity_analysis_data_impure() {
let purity = PurityAnalysisData::impure("mutates global");
assert!(!purity.is_pure);
assert!(purity.has_mutations);
assert_eq!(purity.purity_level, PurityLevel::Impure);
assert_eq!(purity.local_mutations.len(), 1);
}
#[test]
fn test_function_id_generation() {
let file = PathBuf::from("src/main.rs");
let func = ExtractedFunctionData::minimal("process", 100);
let func_id = func.function_id(&file);
assert_eq!(func_id.file, file);
assert_eq!(func_id.name, "process");
assert_eq!(func_id.line, 100);
}
#[test]
fn test_cloning_works() {
let original = ExtractedFileData {
path: PathBuf::from("test.rs"),
functions: vec![ExtractedFunctionData::minimal("foo", 1)],
structs: vec![ExtractedStructData {
name: "MyStruct".to_string(),
line: 10,
fields: vec![FieldInfo {
name: "field".to_string(),
type_str: "i32".to_string(),
is_public: false,
}],
is_public: true,
}],
impls: vec![ExtractedImplData {
type_name: "MyStruct".to_string(),
trait_name: None,
methods: vec![MethodInfo {
name: "new".to_string(),
line: 15,
is_public: true,
}],
line: 12,
}],
imports: vec![ImportInfo {
path: "std::collections::HashMap".to_string(),
alias: None,
is_glob: false,
}],
total_lines: 100,
detected_patterns: vec![],
test_lines: 20, };
let cloned = original.clone();
assert_eq!(cloned.path, original.path);
assert_eq!(cloned.functions.len(), original.functions.len());
assert_eq!(cloned.structs.len(), original.structs.len());
assert_eq!(cloned.impls.len(), original.impls.len());
}
#[test]
fn test_serialization_roundtrip() {
let data = ExtractedFileData {
path: PathBuf::from("test.rs"),
functions: vec![ExtractedFunctionData {
name: "test".to_string(),
qualified_name: "MyStruct::test".to_string(),
line: 1,
end_line: 10,
length: 9,
cyclomatic: 5,
cognitive: 3,
nesting: 2,
entropy_score: None,
purity_analysis: PurityAnalysisData::pure(),
io_operations: vec![IoOperation {
io_type: IoType::File,
description: "read file".to_string(),
line: 5,
}],
parameter_names: vec!["self".to_string(), "path".to_string()],
transformation_patterns: vec![TransformationPattern {
pattern_type: PatternType::Map,
line: 7,
}],
calls: vec![CallSite {
callee_name: "read_to_string".to_string(),
call_type: CallType::Method,
line: 5,
}],
is_test: false,
is_async: true,
visibility: Some("pub".to_string()),
is_trait_method: false,
in_test_module: false,
}],
structs: Vec::new(),
impls: Vec::new(),
imports: Vec::new(),
total_lines: 10,
detected_patterns: vec![],
test_lines: 0, };
let json = serde_json::to_string(&data).expect("serialization failed");
let restored: ExtractedFileData =
serde_json::from_str(&json).expect("deserialization failed");
assert_eq!(restored.path, data.path);
assert_eq!(restored.functions.len(), 1);
assert_eq!(restored.functions[0].name, "test");
assert_eq!(restored.functions[0].io_operations.len(), 1);
assert_eq!(restored.functions[0].io_operations[0].io_type, IoType::File);
}
#[test]
fn test_io_type_variants() {
let types = [
IoType::File,
IoType::Console,
IoType::Network,
IoType::Database,
IoType::AsyncIO,
IoType::Environment,
IoType::System,
];
for io_type in types {
let op = IoOperation {
io_type,
description: "test".to_string(),
line: 1,
};
let _ = op.clone();
let json = serde_json::to_string(&op).unwrap();
let _: IoOperation = serde_json::from_str(&json).unwrap();
}
}
#[test]
fn test_call_type_variants() {
let types = [
CallType::Direct,
CallType::Method,
CallType::StaticMethod,
CallType::TraitMethod,
CallType::Closure,
CallType::FunctionPointer,
];
for call_type in types {
let call = CallSite {
callee_name: "foo".to_string(),
call_type,
line: 1,
};
let _ = call.clone();
}
}
#[test]
fn test_pattern_type_variants() {
let types = [
PatternType::Map,
PatternType::Filter,
PatternType::Fold,
PatternType::FlatMap,
PatternType::Collect,
PatternType::ForEach,
PatternType::Find,
PatternType::Any,
PatternType::All,
PatternType::Reduce,
];
for pattern_type in types {
let pattern = TransformationPattern {
pattern_type,
line: 1,
};
let _ = pattern.clone();
}
}
#[test]
fn test_purity_level_default() {
let level: PurityLevel = Default::default();
assert_eq!(level, PurityLevel::Impure);
}
#[test]
fn test_memory_size_estimate() {
let data = ExtractedFileData {
path: PathBuf::from("src/some_module/file.rs"),
functions: (0..10)
.map(|i| {
let mut func = ExtractedFunctionData::minimal(&format!("func_{}", i), i * 10);
func.parameter_names = vec!["self".to_string(), "arg".to_string()];
func.calls = vec![CallSite {
callee_name: "other".to_string(),
call_type: CallType::Method,
line: i * 10 + 5,
}];
func
})
.collect(),
structs: vec![ExtractedStructData {
name: "MyStruct".to_string(),
line: 1,
fields: (0..5)
.map(|i| FieldInfo {
name: format!("field_{}", i),
type_str: "String".to_string(),
is_public: false,
})
.collect(),
is_public: true,
}],
impls: vec![ExtractedImplData {
type_name: "MyStruct".to_string(),
trait_name: Some("Display".to_string()),
methods: vec![MethodInfo {
name: "fmt".to_string(),
line: 50,
is_public: true,
}],
line: 45,
}],
imports: (0..5)
.map(|i| ImportInfo {
path: format!("std::module_{}", i),
alias: None,
is_glob: false,
})
.collect(),
total_lines: 200,
detected_patterns: vec![],
test_lines: 40, };
let json = serde_json::to_string(&data).expect("serialization failed");
assert!(
json.len() < 16000,
"Serialized size {} bytes exceeds 16KB limit",
json.len()
);
}
#[test]
fn test_loc_metrics_new() {
let metrics = LocMetrics::new(867, 247);
assert_eq!(metrics.total_loc, 867);
assert_eq!(metrics.production_loc, 620);
assert_eq!(metrics.test_loc, 247);
assert_eq!(metrics.test_percentage, 28); }
#[test]
fn test_loc_metrics_no_tests() {
let metrics = LocMetrics::new(500, 0);
assert_eq!(metrics.total_loc, 500);
assert_eq!(metrics.production_loc, 500);
assert_eq!(metrics.test_loc, 0);
assert_eq!(metrics.test_percentage, 0);
assert!(!metrics.has_significant_tests());
}
#[test]
fn test_loc_metrics_significant_tests() {
let metrics = LocMetrics::new(100, 25);
assert!(metrics.has_significant_tests());
let metrics_below = LocMetrics::new(100, 24);
assert!(!metrics_below.has_significant_tests());
}
#[test]
fn test_loc_metrics_display() {
let metrics = LocMetrics::new(867, 247);
assert_eq!(metrics.display_loc(), "620 (867 with tests)");
let metrics_few_tests = LocMetrics::new(500, 10);
assert_eq!(metrics_few_tests.display_loc(), "500");
let metrics_no_tests = LocMetrics::new(500, 0);
assert_eq!(metrics_no_tests.display_loc(), "500");
}
#[test]
fn test_loc_metrics_zero_lines() {
let metrics = LocMetrics::new(0, 0);
assert_eq!(metrics.total_loc, 0);
assert_eq!(metrics.production_loc, 0);
assert_eq!(metrics.test_loc, 0);
assert_eq!(metrics.test_percentage, 0);
}
#[test]
fn test_extracted_file_data_production_lines() {
let mut data = ExtractedFileData::empty(PathBuf::from("test.rs"));
data.total_lines = 867;
data.test_lines = 247;
assert_eq!(data.production_lines(), 620);
}
#[test]
fn test_extracted_file_data_loc_metrics() {
let mut data = ExtractedFileData::empty(PathBuf::from("test.rs"));
data.total_lines = 867;
data.test_lines = 247;
let metrics = data.loc_metrics();
assert_eq!(metrics.total_loc, 867);
assert_eq!(metrics.production_loc, 620);
assert_eq!(metrics.test_loc, 247);
}
#[test]
fn test_loc_metrics_serialization() {
let metrics = LocMetrics::new(867, 247);
let json = serde_json::to_string(&metrics).expect("serialization failed");
let restored: LocMetrics = serde_json::from_str(&json).expect("deserialization failed");
assert_eq!(restored.total_loc, metrics.total_loc);
assert_eq!(restored.production_loc, metrics.production_loc);
assert_eq!(restored.test_loc, metrics.test_loc);
assert_eq!(restored.test_percentage, metrics.test_percentage);
}
}