use super::*;
use std::fs;
use tempfile::TempDir;
fn make_symbol_ref(symbol: &str, kind: SymbolKind, file: &str, line: usize) -> SymbolReference {
SymbolReference {
symbol: symbol.to_string(),
kind,
file: PathBuf::from(file),
line,
context: format!("{symbol} context"),
}
}
fn make_dependency(from: &str, to: &str, kind: DependencyKind) -> FileDependency {
FileDependency { from: PathBuf::from(from), to: PathBuf::from(to), kind }
}
fn make_dead_code(symbol: &str, kind: SymbolKind, file: &str, line: usize) -> DeadCode {
DeadCode {
symbol: symbol.to_string(),
kind,
file: PathBuf::from(file),
line,
reason: "No references found".to_string(),
}
}
fn assert_json_roundtrip<T: Serialize + serde::de::DeserializeOwned>(value: &T) -> T {
let json = serde_json::to_string(value).unwrap();
serde_json::from_str(&json).unwrap()
}
#[test]
fn test_analyzer_creation() {
let analyzer = ParfAnalyzer::new();
assert_eq!(analyzer.file_cache.len(), 0);
}
#[test]
fn test_function_name_extraction() {
assert_eq!(ParfAnalyzer::extract_function_name("fn main() {"), Some("main".to_string()));
assert_eq!(
ParfAnalyzer::extract_function_name("pub fn test_function() -> Result<()> {"),
Some("test_function".to_string())
);
}
#[test]
fn test_type_name_extraction() {
assert_eq!(ParfAnalyzer::extract_type_name("struct MyStruct {"), Some("MyStruct".to_string()));
assert_eq!(ParfAnalyzer::extract_type_name("pub enum Status {"), Some("Status".to_string()));
}
#[test]
fn test_python_function_name_extraction() {
assert_eq!(
ParfAnalyzer::extract_python_function_name("def my_function():"),
Some("my_function".to_string())
);
assert_eq!(
ParfAnalyzer::extract_python_function_name(" def helper(arg1, arg2):"),
Some("helper".to_string())
);
}
#[test]
fn test_python_class_name_extraction() {
assert_eq!(
ParfAnalyzer::extract_python_class_name("class MyClass:"),
Some("MyClass".to_string())
);
assert_eq!(
ParfAnalyzer::extract_python_class_name("class MyClass(BaseClass):"),
Some("MyClass".to_string())
);
}
#[test]
fn test_index_codebase() -> Result<()> {
let temp_dir = TempDir::new()?;
let test_file = temp_dir.path().join("test.rs");
fs::write(&test_file, "fn hello() {}\nfn world() {}")?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
assert_eq!(analyzer.file_cache.len(), 1);
assert!(analyzer.symbol_definitions.contains_key("hello"));
assert!(analyzer.symbol_definitions.contains_key("world"));
Ok(())
}
#[test]
fn test_pattern_detection() -> Result<()> {
let temp_dir = TempDir::new()?;
let test_file = temp_dir.path().join("test.rs");
fs::write(&test_file, "// TODO: fix this\nlet x = y.unwrap();")?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
let patterns = analyzer.detect_patterns();
assert!(patterns.iter().any(|p| matches!(p, CodePattern::TechDebt { .. })));
assert!(patterns.iter().any(|p| matches!(p, CodePattern::ErrorHandling { .. })));
Ok(())
}
#[test]
fn test_symbol_kind_all_variants() {
let variants = [
SymbolKind::Function,
SymbolKind::Class,
SymbolKind::Variable,
SymbolKind::Constant,
SymbolKind::Module,
SymbolKind::Import,
];
assert_eq!(variants.len(), 6);
}
#[test]
fn test_symbol_kind_equality() {
assert_eq!(SymbolKind::Function, SymbolKind::Function);
assert_ne!(SymbolKind::Function, SymbolKind::Class);
}
#[test]
fn test_symbol_kind_serialization() {
let kind = SymbolKind::Function;
let deserialized = assert_json_roundtrip(&kind);
assert_eq!(kind, deserialized);
}
#[test]
fn test_symbol_reference_construction() {
let sym_ref = make_symbol_ref("test_func", SymbolKind::Function, "test.rs", 42);
assert_eq!(sym_ref.symbol, "test_func");
assert_eq!(sym_ref.kind, SymbolKind::Function);
assert_eq!(sym_ref.line, 42);
}
#[test]
fn test_symbol_reference_serialization() {
let sym_ref = make_symbol_ref("my_var", SymbolKind::Variable, "module.rs", 10);
let deserialized = assert_json_roundtrip(&sym_ref);
assert_eq!(sym_ref.symbol, deserialized.symbol);
assert_eq!(sym_ref.kind, deserialized.kind);
assert_eq!(sym_ref.line, deserialized.line);
}
#[test]
fn test_code_pattern_tech_debt() {
let pattern = CodePattern::TechDebt {
message: "// TODO: refactor".to_string(),
file: PathBuf::from("main.rs"),
line: 100,
};
match pattern {
CodePattern::TechDebt { message, .. } => assert!(message.contains("TODO")),
_ => panic!("Wrong variant"),
}
}
#[test]
fn test_code_pattern_deprecated_api() {
let pattern = CodePattern::DeprecatedApi {
api: "old_function".to_string(),
file: PathBuf::from("api.rs"),
line: 50,
};
match pattern {
CodePattern::DeprecatedApi { api, .. } => assert_eq!(api, "old_function"),
_ => panic!("Wrong variant"),
}
}
#[test]
fn test_code_pattern_error_handling() {
let pattern = CodePattern::ErrorHandling {
pattern: "unwrap()".to_string(),
file: PathBuf::from("lib.rs"),
line: 25,
};
match pattern {
CodePattern::ErrorHandling { pattern, .. } => assert!(pattern.contains("unwrap")),
_ => panic!("Wrong variant"),
}
}
#[test]
fn test_code_pattern_resource_management() {
let pattern = CodePattern::ResourceManagement {
resource_type: "file".to_string(),
file: PathBuf::from("io.rs"),
line: 15,
};
match pattern {
CodePattern::ResourceManagement { resource_type, .. } => {
assert_eq!(resource_type, "file");
}
_ => panic!("Wrong variant"),
}
}
#[test]
fn test_code_pattern_duplicate_code() {
let pattern = CodePattern::DuplicateCode {
pattern: "for i in 0..10".to_string(),
occurrences: vec![(PathBuf::from("a.rs"), 10), (PathBuf::from("b.rs"), 20)],
};
match pattern {
CodePattern::DuplicateCode { occurrences, .. } => assert_eq!(occurrences.len(), 2),
_ => panic!("Wrong variant"),
}
}
#[test]
fn test_code_pattern_serialization() {
let pattern = CodePattern::TechDebt {
message: "FIXME".to_string(),
file: PathBuf::from("test.rs"),
line: 1,
};
let deserialized = assert_json_roundtrip(&pattern);
match deserialized {
CodePattern::TechDebt { message, .. } => assert_eq!(message, "FIXME"),
_ => panic!("Wrong variant"),
}
}
#[test]
fn test_file_dependency_construction() {
let dep = make_dependency("main.rs", "module.rs", DependencyKind::Import);
assert_eq!(dep.from, PathBuf::from("main.rs"));
assert_eq!(dep.to, PathBuf::from("module.rs"));
assert_eq!(dep.kind, DependencyKind::Import);
}
#[test]
fn test_dependency_kind_all_variants() {
let kinds = [
DependencyKind::Import,
DependencyKind::Include,
DependencyKind::Require,
DependencyKind::ModuleUse,
];
assert_eq!(kinds.len(), 4);
}
#[test]
fn test_dependency_kind_equality() {
assert_eq!(DependencyKind::Import, DependencyKind::Import);
assert_ne!(DependencyKind::Import, DependencyKind::Include);
}
#[test]
fn test_file_dependency_serialization() {
let dep = make_dependency("a.rs", "b.rs", DependencyKind::ModuleUse);
let deserialized = assert_json_roundtrip(&dep);
assert_eq!(dep.from, deserialized.from);
assert_eq!(dep.kind, deserialized.kind);
}
#[test]
fn test_dead_code_construction() {
let dead = make_dead_code("unused_func", SymbolKind::Function, "old.rs", 99);
assert_eq!(dead.symbol, "unused_func");
assert_eq!(dead.kind, SymbolKind::Function);
assert_eq!(dead.line, 99);
}
#[test]
fn test_dead_code_serialization() {
let dead = make_dead_code("dead", SymbolKind::Class, "lib.rs", 1);
let deserialized = assert_json_roundtrip(&dead);
assert_eq!(dead.symbol, deserialized.symbol);
assert_eq!(dead.kind, deserialized.kind);
}
#[test]
fn test_default_analyzer() {
let analyzer = ParfAnalyzer::default();
assert_eq!(analyzer.file_cache.len(), 0);
assert_eq!(analyzer.symbol_definitions.len(), 0);
}
#[test]
fn test_find_references() -> Result<()> {
let temp_dir = TempDir::new()?;
let test_file = temp_dir.path().join("test.rs");
fs::write(&test_file, "fn main() {\n println!(\"hello\");\n hello();\n}\nfn hello() {}")?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
let refs = analyzer.find_references("hello", SymbolKind::Function);
assert!(!refs.is_empty());
assert!(refs.iter().any(|r| r.context.contains("hello")));
Ok(())
}
#[test]
fn test_find_references_not_found() -> Result<()> {
let temp_dir = TempDir::new()?;
let test_file = temp_dir.path().join("test.rs");
fs::write(&test_file, "fn main() {}")?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
let refs = analyzer.find_references("nonexistent", SymbolKind::Function);
assert!(refs.is_empty());
Ok(())
}
#[test]
fn test_detect_patterns_fixme() -> Result<()> {
let temp_dir = TempDir::new()?;
let test_file = temp_dir.path().join("code.rs");
fs::write(&test_file, "// FIXME: broken code\nfn test() {}")?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
let patterns = analyzer.detect_patterns();
assert!(patterns.iter().any(|p| matches!(p, CodePattern::TechDebt { .. })));
Ok(())
}
#[test]
fn test_detect_patterns_deprecated() -> Result<()> {
let temp_dir = TempDir::new()?;
let test_file = temp_dir.path().join("api.rs");
fs::write(&test_file, "#[deprecated]\nfn old_api() {}")?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
let patterns = analyzer.detect_patterns();
assert!(patterns.iter().any(|p| matches!(p, CodePattern::DeprecatedApi { .. })));
Ok(())
}
#[test]
fn test_detect_patterns_file_handling() -> Result<()> {
let temp_dir = TempDir::new()?;
let test_file = temp_dir.path().join("io.rs");
fs::write(&test_file, "let f = File::open(\"test.txt\");")?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
let patterns = analyzer.detect_patterns();
assert!(patterns.iter().any(|p| matches!(p, CodePattern::ResourceManagement { .. })));
Ok(())
}
#[test]
fn test_analyze_dependencies_rust() -> Result<()> {
let temp_dir = TempDir::new()?;
let test_file = temp_dir.path().join("lib.rs");
fs::write(&test_file, "use std::fs;\nuse serde::Serialize;")?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
let deps = analyzer.analyze_dependencies();
assert!(!deps.is_empty());
assert!(deps.iter().any(|d| d.kind == DependencyKind::ModuleUse));
Ok(())
}
#[test]
fn test_analyze_dependencies_python() -> Result<()> {
let temp_dir = TempDir::new()?;
let test_file = temp_dir.path().join("main.py");
fs::write(&test_file, "import numpy as np\nfrom sklearn import linear_model")?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
let deps = analyzer.analyze_dependencies();
assert!(!deps.is_empty());
assert!(deps.iter().any(|d| d.kind == DependencyKind::Import));
Ok(())
}
#[test]
fn test_find_dead_code_with_unused_function() -> Result<()> {
let temp_dir = TempDir::new()?;
let test_file = temp_dir.path().join("dead.rs");
fs::write(&test_file, "fn unused_func() {}\nfn main() {\n // nothing\n}")?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
let dead = analyzer.find_dead_code();
assert!(dead.is_empty() || dead.iter().any(|d| d.symbol == "unused_func"));
Ok(())
}
#[test]
fn test_find_dead_code_skips_tests() -> Result<()> {
let temp_dir = TempDir::new()?;
let test_file = temp_dir.path().join("tests.rs");
fs::write(&test_file, "#[test]\nfn test_something() {}")?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
let dead = analyzer.find_dead_code();
assert!(!dead.iter().any(|d| d.symbol == "test_something"));
Ok(())
}
#[test]
fn test_find_dead_code_skips_main() -> Result<()> {
let temp_dir = TempDir::new()?;
let test_file = temp_dir.path().join("main.rs");
fs::write(&test_file, "fn main() {}")?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
let dead = analyzer.find_dead_code();
assert!(!dead.iter().any(|d| d.symbol == "main"));
Ok(())
}
#[test]
fn test_generate_report() -> Result<()> {
let temp_dir = TempDir::new()?;
let test_file = temp_dir.path().join("test.rs");
fs::write(&test_file, "fn test() {}\n// TODO: fix")?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
let report = analyzer.generate_report();
assert!(report.contains("PARF Analysis Report"));
assert!(report.contains("Files analyzed:"));
assert!(report.contains("Symbols defined:"));
assert!(report.contains("Patterns detected:"));
Ok(())
}
#[test]
fn test_generate_report_with_dead_code() -> Result<()> {
let temp_dir = TempDir::new()?;
let test_file = temp_dir.path().join("code.rs");
fs::write(&test_file, "fn unused() {}\nfn main() {}")?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
let report = analyzer.generate_report();
assert!(report.contains("Potentially dead code:"));
Ok(())
}
#[test]
fn test_index_python_file() -> Result<()> {
let temp_dir = TempDir::new()?;
let test_file = temp_dir.path().join("test.py");
fs::write(&test_file, "def my_function():\n pass\n\nclass MyClass:\n pass")?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
assert!(analyzer.symbol_definitions.contains_key("my_function"));
assert!(analyzer.symbol_definitions.contains_key("MyClass"));
Ok(())
}
#[test]
fn test_index_multiple_files() -> Result<()> {
let temp_dir = TempDir::new()?;
fs::write(temp_dir.path().join("a.rs"), "fn func_a() {}")?;
fs::write(temp_dir.path().join("b.rs"), "fn func_b() {}")?;
fs::write(temp_dir.path().join("c.py"), "def func_c(): pass")?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
assert_eq!(analyzer.file_cache.len(), 3);
assert!(analyzer.symbol_definitions.contains_key("func_a"));
assert!(analyzer.symbol_definitions.contains_key("func_b"));
assert!(analyzer.symbol_definitions.contains_key("func_c"));
Ok(())
}
#[test]
fn test_extract_function_name_edge_cases() {
assert_eq!(
ParfAnalyzer::extract_function_name("pub async fn test() {"),
Some("test".to_string())
);
assert_eq!(ParfAnalyzer::extract_function_name("no function here"), None);
assert_eq!(ParfAnalyzer::extract_function_name("fn ()"), Some("".to_string()));
}
#[test]
fn test_extract_type_name_with_generics() {
assert_eq!(ParfAnalyzer::extract_type_name("struct Vec<T> {"), Some("Vec".to_string()));
assert_eq!(ParfAnalyzer::extract_type_name("enum Option<T>"), Some("Option".to_string()));
}
#[test]
fn test_extract_python_function_name_with_args() {
assert_eq!(
ParfAnalyzer::extract_python_function_name("def process(data, config):"),
Some("process".to_string())
);
}
#[test]
fn test_extract_python_class_name_with_inheritance() {
assert_eq!(
ParfAnalyzer::extract_python_class_name("class Child(Parent, Mixin):"),
Some("Child".to_string())
);
}
#[test]
fn test_find_references_across_multiple_files() -> Result<()> {
let temp_dir = TempDir::new()?;
fs::write(
temp_dir.path().join("def.rs"),
"fn shared_func() {\n println!(\"definition\");\n}",
)?;
fs::write(temp_dir.path().join("use.rs"), "fn caller() {\n shared_func();\n}")?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
let refs = analyzer.find_references("shared_func", SymbolKind::Function);
assert!(refs.len() >= 2, "Expected refs in both files, got {}", refs.len());
Ok(())
}
#[test]
fn test_find_references_with_variable_kind() -> Result<()> {
let temp_dir = TempDir::new()?;
fs::write(
temp_dir.path().join("vars.rs"),
"fn main() {\n let my_var = 42;\n println!(\"{}\", my_var);\n}",
)?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
let refs = analyzer.find_references("my_var", SymbolKind::Variable);
assert!(!refs.is_empty());
Ok(())
}
#[test]
fn test_detect_patterns_hack() -> Result<()> {
let temp_dir = TempDir::new()?;
fs::write(
temp_dir.path().join("code.rs"),
"// HACK: temporary workaround\nfn workaround() {}",
)?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
let patterns = analyzer.detect_patterns();
assert!(
patterns.iter().any(|p| matches!(p, CodePattern::TechDebt { .. })),
"HACK should be detected as tech debt"
);
Ok(())
}
#[test]
fn test_detect_patterns_unwrap_in_comment_skipped() -> Result<()> {
let temp_dir = TempDir::new()?;
fs::write(temp_dir.path().join("code.rs"), "// This uses unwrap() in a comment\nfn safe() {}")?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
let patterns = analyzer.detect_patterns();
assert!(
!patterns.iter().any(|p| matches!(
p,
CodePattern::ErrorHandling { pattern, .. }
if pattern.contains("unwrap()")
)),
"unwrap() in comments should be skipped"
);
Ok(())
}
#[test]
fn test_detect_patterns_expect() -> Result<()> {
let temp_dir = TempDir::new()?;
fs::write(
temp_dir.path().join("code.rs"),
"fn main() {\n let x = result.expect(\"should work\");\n}",
)?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
let patterns = analyzer.detect_patterns();
assert!(
patterns.iter().any(|p| matches!(p, CodePattern::ErrorHandling { .. })),
"expect() should be detected as error handling"
);
Ok(())
}
#[test]
fn test_detect_patterns_at_deprecated() -> Result<()> {
let temp_dir = TempDir::new()?;
fs::write(temp_dir.path().join("api.py"), "@deprecated\ndef old_api(): pass")?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
let patterns = analyzer.detect_patterns();
assert!(
patterns.iter().any(|p| matches!(p, CodePattern::DeprecatedApi { .. })),
"@deprecated should be detected"
);
Ok(())
}
#[test]
fn test_detect_patterns_fs_read() -> Result<()> {
let temp_dir = TempDir::new()?;
fs::write(temp_dir.path().join("io.rs"), "let content = fs::read(\"file.txt\");")?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
let patterns = analyzer.detect_patterns();
assert!(
patterns.iter().any(|p| matches!(p, CodePattern::ResourceManagement { .. })),
"fs::read should be detected as resource management"
);
Ok(())
}
#[test]
fn test_dead_code_unreferenced_function() -> Result<()> {
let temp_dir = TempDir::new()?;
fs::write(
temp_dir.path().join("isolated.rs"),
"fn xyzzy_unique_unreferenced_func() {\n println!(\"never called\");\n}",
)?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
let dead = analyzer.find_dead_code();
assert!(dead.is_empty() || !dead.is_empty());
Ok(())
}
#[test]
fn test_dead_code_test_function_skipped() -> Result<()> {
let temp_dir = TempDir::new()?;
fs::write(
temp_dir.path().join("tests.rs"),
"#[test]\nfn test_something_unique_unreferenced() {}",
)?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
let dead = analyzer.find_dead_code();
assert!(
!dead.iter().any(|d| d.symbol == "test_something_unique_unreferenced"),
"Test functions should be skipped in dead code detection"
);
Ok(())
}
#[test]
fn test_generate_report_contains_all_sections() -> Result<()> {
let temp_dir = TempDir::new()?;
fs::write(
temp_dir.path().join("code.rs"),
"use std::fs;\n// TODO: fix\nfn main() {}\nfn helper() { main(); }",
)?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
let report = analyzer.generate_report();
assert!(report.contains("PARF Analysis Report"));
assert!(report.contains("===================="));
assert!(report.contains("Files analyzed: 1"));
assert!(report.contains("Symbols defined:"));
assert!(report.contains("Patterns detected:"));
assert!(report.contains("Dependencies:"));
assert!(report.contains("Potentially dead code:"));
Ok(())
}
#[test]
fn test_generate_report_many_dead_code_truncation() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut code = String::new();
for i in 0..15 {
code.push_str(&format!("struct Unreferenced{} {{}}\n", i));
}
fs::write(temp_dir.path().join("many.rs"), &code)?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
let report = analyzer.generate_report();
let dead = analyzer.find_dead_code();
if dead.len() > 10 {
assert!(
report.contains("... and"),
"Report should truncate dead code list with '... and N more'"
);
}
Ok(())
}
#[test]
fn test_generate_report_no_dead_code() -> Result<()> {
let temp_dir = TempDir::new()?;
fs::write(temp_dir.path().join("code.rs"), "fn main() { helper(); }\nfn helper() { main(); }")?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
let report = analyzer.generate_report();
assert!(report.contains("Potentially dead code: 0"));
Ok(())
}
#[test]
fn test_analyze_dependencies_empty_codebase() {
let analyzer = ParfAnalyzer::new();
let deps = analyzer.analyze_dependencies();
assert!(deps.is_empty());
}
#[test]
fn test_detect_patterns_empty_codebase() {
let analyzer = ParfAnalyzer::new();
let patterns = analyzer.detect_patterns();
assert!(patterns.is_empty());
}
#[test]
fn test_find_dead_code_empty_codebase() {
let analyzer = ParfAnalyzer::new();
let dead = analyzer.find_dead_code();
assert!(dead.is_empty());
}
#[test]
fn test_extract_type_name_no_match() {
assert_eq!(ParfAnalyzer::extract_type_name("let x = 5;"), None);
}
#[test]
fn test_extract_function_name_no_paren() {
assert_eq!(ParfAnalyzer::extract_function_name("fn missing_paren"), None);
}
#[test]
fn test_extract_python_function_name_no_paren() {
assert_eq!(ParfAnalyzer::extract_python_function_name("def no_paren"), None);
}
#[test]
fn test_extract_python_class_name_no_colon_or_paren() {
assert_eq!(ParfAnalyzer::extract_python_class_name("class NoEnd"), None);
}
#[test]
fn test_index_skips_non_source_files() -> Result<()> {
let temp_dir = TempDir::new()?;
fs::write(temp_dir.path().join("data.txt"), "fn not_indexed() {}")?;
fs::write(temp_dir.path().join("config.toml"), "[package]\nname = \"test\"")?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
assert_eq!(analyzer.file_cache.len(), 0, "Non-source files should not be indexed");
Ok(())
}
#[test]
fn test_index_c_files() -> Result<()> {
let temp_dir = TempDir::new()?;
fs::write(temp_dir.path().join("main.c"), "#include <stdio.h>\nint main() { return 0; }")?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
assert_eq!(analyzer.file_cache.len(), 1, "C files should be indexed");
Ok(())
}
#[test]
fn test_index_js_files() -> Result<()> {
let temp_dir = TempDir::new()?;
fs::write(temp_dir.path().join("app.js"), "function hello() { return 'world'; }")?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
assert_eq!(analyzer.file_cache.len(), 1, "JS files should be indexed");
Ok(())
}
#[test]
fn test_symbol_kind_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(SymbolKind::Function);
set.insert(SymbolKind::Class);
set.insert(SymbolKind::Function); assert_eq!(set.len(), 2);
}
#[test]
fn test_dependency_kind_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(DependencyKind::Import);
set.insert(DependencyKind::Include);
set.insert(DependencyKind::Import); assert_eq!(set.len(), 2);
}
#[test]
fn test_dead_code_reason_field() {
let dead = make_dead_code("sym", SymbolKind::Variable, "test.rs", 1);
assert_eq!(dead.reason, "No references found");
}
#[test]
fn test_symbol_reference_clone() {
let sym_ref = make_symbol_ref("func", SymbolKind::Function, "a.rs", 10);
let cloned = sym_ref.clone();
assert_eq!(sym_ref.symbol, cloned.symbol);
assert_eq!(sym_ref.line, cloned.line);
}
#[test]
fn test_add_definition_multiple_defs_same_symbol() {
let mut analyzer = ParfAnalyzer::new();
analyzer.add_definition(
"shared_name".to_string(),
SymbolKind::Function,
&PathBuf::from("a.rs"),
1,
"fn shared_name()",
);
analyzer.add_definition(
"shared_name".to_string(),
SymbolKind::Function,
&PathBuf::from("b.rs"),
10,
"fn shared_name()",
);
let defs = analyzer.symbol_definitions.get("shared_name").unwrap();
assert_eq!(defs.len(), 2);
}
#[test]
fn test_index_struct_with_brace_on_same_line() -> Result<()> {
let temp_dir = TempDir::new()?;
fs::write(
temp_dir.path().join("types.rs"),
"struct Inline { field: u32 }\nenum Color { Red, Green, Blue }",
)?;
let mut analyzer = ParfAnalyzer::new();
analyzer.index_codebase(temp_dir.path())?;
assert!(analyzer.symbol_definitions.contains_key("Inline"));
assert!(analyzer.symbol_definitions.contains_key("Color"));
Ok(())
}
#[test]
fn test_find_dead_code_truly_unreferenced_symbol() {
let mut analyzer = ParfAnalyzer::new();
analyzer
.file_cache
.insert(PathBuf::from("main.rs"), vec!["fn main() { println!(\"hello\"); }".to_string()]);
analyzer.add_definition(
"zzz_never_used".to_string(),
SymbolKind::Function,
&PathBuf::from("orphan.rs"),
1,
"fn zzz_never_used()",
);
let dead = analyzer.find_dead_code();
assert!(
dead.iter().any(|d| d.symbol == "zzz_never_used"),
"Truly unreferenced symbol should appear in dead code"
);
assert_eq!(dead[0].reason, "No references found");
}
#[test]
fn test_find_dead_code_skips_test_function_context() {
let mut analyzer = ParfAnalyzer::new();
analyzer.file_cache.insert(PathBuf::from("main.rs"), vec!["fn main() {}".to_string()]);
analyzer.add_definition(
"test_check_result".to_string(),
SymbolKind::Function,
&PathBuf::from("tests.rs"),
1,
"#[test] fn test_check_result()",
);
let dead = analyzer.find_dead_code();
assert!(
!dead.iter().any(|d| d.symbol == "test_check_result"),
"Test functions (context contains #[test]) should be skipped"
);
}
#[test]
fn test_find_dead_code_skips_test_underscore_context() {
let mut analyzer = ParfAnalyzer::new();
analyzer.file_cache.insert(PathBuf::from("main.rs"), vec!["fn main() {}".to_string()]);
analyzer.add_definition(
"zz_unique_sym".to_string(),
SymbolKind::Function,
&PathBuf::from("tests.rs"),
1,
"fn test_zz_unique_sym()",
);
let dead = analyzer.find_dead_code();
assert!(
!dead.iter().any(|d| d.symbol == "zz_unique_sym"),
"Functions with test_ in context should be skipped"
);
}
#[test]
fn test_find_dead_code_skips_main_function() {
let mut analyzer = ParfAnalyzer::new();
analyzer.file_cache.insert(PathBuf::from("empty.rs"), vec!["let x = 42;".to_string()]);
analyzer.add_definition(
"main".to_string(),
SymbolKind::Function,
&PathBuf::from("bin.rs"),
1,
"fn main()",
);
let dead = analyzer.find_dead_code();
assert!(!dead.iter().any(|d| d.symbol == "main"), "main function should always be skipped");
}
#[test]
fn test_find_dead_code_multiple_definitions_of_unreferenced() {
let mut analyzer = ParfAnalyzer::new();
analyzer.file_cache.insert(PathBuf::from("other.rs"), vec!["let x = 42;".to_string()]);
analyzer.add_definition(
"orphan_fn".to_string(),
SymbolKind::Function,
&PathBuf::from("a.rs"),
10,
"fn orphan_fn()",
);
analyzer.add_definition(
"orphan_fn".to_string(),
SymbolKind::Function,
&PathBuf::from("b.rs"),
20,
"fn orphan_fn()",
);
let dead = analyzer.find_dead_code();
let orphan_entries: Vec<_> = dead.iter().filter(|d| d.symbol == "orphan_fn").collect();
assert_eq!(orphan_entries.len(), 2, "Both definitions of unreferenced symbol should appear");
}
#[test]
fn test_generate_report_dead_code_listing() {
let mut analyzer = ParfAnalyzer::new();
analyzer.file_cache.insert(PathBuf::from("code.rs"), vec!["let x = 42;".to_string()]);
for i in 0..3 {
analyzer.add_definition(
format!("orphan_func_{}", i),
SymbolKind::Function,
&PathBuf::from("orphan.rs"),
i + 1,
&format!("fn orphan_func_{}()", i),
);
}
let report = analyzer.generate_report();
assert!(
report.contains("Dead Code Candidates:"),
"Report should contain dead code section header"
);
assert!(report.contains("---------------------"), "Report should contain separator");
assert!(report.contains("orphan_func_0"), "Report should list first dead code item");
}
#[test]
fn test_generate_report_dead_code_truncation_over_10() {
let mut analyzer = ParfAnalyzer::new();
analyzer.file_cache.insert(PathBuf::from("code.rs"), vec!["let x = 42;".to_string()]);
for i in 0..15 {
analyzer.add_definition(
format!("dead_sym_{:02}", i),
SymbolKind::Function,
&PathBuf::from(format!("file_{}.rs", i)),
i + 1,
&format!("fn dead_sym_{:02}()", i),
);
}
let report = analyzer.generate_report();
let dead = analyzer.find_dead_code();
assert!(dead.len() >= 11, "Should have more than 10 dead code items, got {}", dead.len());
assert!(report.contains("... and"), "Report should show truncation with '... and N more'");
assert!(
report.contains("Dead Code Candidates:"),
"Report should contain dead code listing section"
);
}
#[test]
fn test_generate_report_exactly_10_dead_code_no_truncation() {
let mut analyzer = ParfAnalyzer::new();
analyzer.file_cache.insert(PathBuf::from("code.rs"), vec!["let x = 42;".to_string()]);
for i in 0..10 {
analyzer.add_definition(
format!("dead_exact_{:02}", i),
SymbolKind::Function,
&PathBuf::from(format!("exact_{}.rs", i)),
i + 1,
&format!("fn dead_exact_{:02}()", i),
);
}
let report = analyzer.generate_report();
let dead = analyzer.find_dead_code();
assert_eq!(dead.len(), 10);
assert!(!report.contains("... and"), "Exactly 10 dead items should not trigger truncation");
}
#[test]
fn test_generate_report_with_dependencies_and_patterns() {
let mut analyzer = ParfAnalyzer::new();
analyzer.file_cache.insert(
PathBuf::from("complex.rs"),
vec![
"use std::io;".to_string(),
"use serde::Serialize;".to_string(),
"// TODO: refactor this".to_string(),
"fn process() {}".to_string(),
],
);
analyzer.add_definition(
"orphan_complex".to_string(),
SymbolKind::Function,
&PathBuf::from("orphan.rs"),
1,
"fn orphan_complex()",
);
let report = analyzer.generate_report();
assert!(report.contains("Files analyzed: 1"));
assert!(report.contains("Dependencies:"));
assert!(report.contains("Patterns detected:"));
assert!(report.contains("Potentially dead code:"));
}