use crate::utils::error::ContextCreatorError;
use crate::utils::file_ext::{is_binary_extension, FileType};
use anyhow::Result;
use glob::Pattern;
use ignore::{Walk, WalkBuilder};
use rayon::prelude::*;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tracing::warn;
#[derive(Debug, Clone)]
pub struct CompiledPriority {
pub matcher: Pattern,
pub weight: f32,
pub original_pattern: String,
}
impl CompiledPriority {
pub fn new(pattern: &str, weight: f32) -> Result<Self, glob::PatternError> {
let matcher = Pattern::new(pattern)?;
Ok(Self {
matcher,
weight,
original_pattern: pattern.to_string(),
})
}
pub fn try_from_config_priority(
priority: &crate::config::Priority,
) -> Result<Self, glob::PatternError> {
Self::new(&priority.pattern, priority.weight)
}
}
#[derive(Debug, Clone)]
pub struct WalkOptions {
pub max_file_size: Option<usize>,
pub follow_links: bool,
pub include_hidden: bool,
pub parallel: bool,
pub ignore_file: String,
pub ignore_patterns: Vec<String>,
pub include_patterns: Vec<String>,
pub custom_priorities: Vec<CompiledPriority>,
pub filter_binary_files: bool,
}
impl WalkOptions {
pub fn from_config(config: &crate::cli::Config) -> Result<Self> {
let mut custom_priorities = Vec::new();
for priority in &config.custom_priorities {
match CompiledPriority::try_from_config_priority(priority) {
Ok(compiled) => custom_priorities.push(compiled),
Err(e) => {
return Err(ContextCreatorError::ConfigError(format!(
"Invalid glob pattern '{}' in custom priorities: {e}",
priority.pattern
))
.into());
}
}
}
let include_patterns = config
.get_include_patterns()
.into_iter()
.filter(|pattern| !pattern.trim().is_empty())
.collect();
let ignore_patterns = config
.get_ignore_patterns()
.into_iter()
.filter(|pattern| !pattern.trim().is_empty())
.collect();
Ok(WalkOptions {
max_file_size: Some(10 * 1024 * 1024), follow_links: false,
include_hidden: false,
parallel: true,
ignore_file: ".context-creator-ignore".to_string(),
ignore_patterns,
include_patterns,
custom_priorities,
filter_binary_files: config.get_prompt().is_some(),
})
}
}
impl Default for WalkOptions {
fn default() -> Self {
WalkOptions {
max_file_size: Some(10 * 1024 * 1024), follow_links: false,
include_hidden: false,
parallel: true,
ignore_file: ".context-creator-ignore".to_string(),
ignore_patterns: vec![],
include_patterns: vec![],
custom_priorities: vec![],
filter_binary_files: false,
}
}
}
#[derive(Debug, Clone)]
pub struct FileInfo {
pub path: PathBuf,
pub relative_path: PathBuf,
pub size: u64,
pub file_type: FileType,
pub priority: f32,
pub imports: Vec<PathBuf>,
pub imported_by: Vec<PathBuf>,
pub function_calls: Vec<crate::core::semantic::analyzer::FunctionCall>,
pub type_references: Vec<crate::core::semantic::analyzer::TypeReference>,
pub exported_functions: Vec<crate::core::semantic::analyzer::FunctionDefinition>,
}
impl FileInfo {
pub fn file_type_display(&self) -> &'static str {
use crate::utils::file_ext::FileType;
match self.file_type {
FileType::Rust => "Rust",
FileType::Python => "Python",
FileType::JavaScript => "JavaScript",
FileType::TypeScript => "TypeScript",
FileType::Go => "Go",
FileType::Java => "Java",
FileType::Cpp => "C++",
FileType::C => "C",
FileType::CSharp => "C#",
FileType::Ruby => "Ruby",
FileType::Php => "PHP",
FileType::Swift => "Swift",
FileType::Kotlin => "Kotlin",
FileType::Scala => "Scala",
FileType::Haskell => "Haskell",
FileType::Dart => "Dart",
FileType::Lua => "Lua",
FileType::R => "R",
FileType::Julia => "Julia",
FileType::Elixir => "Elixir",
FileType::Elm => "Elm",
FileType::Markdown => "Markdown",
FileType::Json => "JSON",
FileType::Yaml => "YAML",
FileType::Toml => "TOML",
FileType::Xml => "XML",
FileType::Html => "HTML",
FileType::Css => "CSS",
FileType::Text => "Text",
FileType::Other => "Other",
}
}
}
pub fn walk_directory(root: &Path, options: WalkOptions) -> Result<Vec<FileInfo>> {
if !root.exists() {
return Err(ContextCreatorError::InvalidPath(format!(
"Path does not exist: {}",
root.display()
))
.into());
}
if root.is_file() {
let metadata = root.metadata()?;
let file_type = FileType::from_path(root);
let relative_path = PathBuf::from(
root.file_name()
.ok_or_else(|| anyhow::anyhow!("Invalid file name"))?,
);
let priority = calculate_priority(&file_type, &relative_path, &options.custom_priorities);
let file_info = FileInfo {
path: root.to_path_buf(),
relative_path,
size: metadata.len(),
file_type,
priority,
imports: Vec::new(),
imported_by: Vec::new(),
function_calls: Vec::new(),
type_references: Vec::new(),
exported_functions: Vec::new(),
};
return Ok(vec![file_info]);
}
if !root.is_dir() {
return Err(ContextCreatorError::InvalidPath(format!(
"Path is neither a file nor a directory: {}",
root.display()
))
.into());
}
let root = root.canonicalize()?;
let walker = build_walker(&root, &options)?;
if options.parallel {
walk_parallel(walker, &root, &options)
} else {
walk_sequential(walker, &root, &options)
}
}
pub fn sanitize_pattern(pattern: &str) -> Result<String> {
if pattern.len() > 1000 {
return Err(ContextCreatorError::InvalidConfiguration(
"Pattern too long (max 1000 characters)".to_string(),
)
.into());
}
if pattern.contains('\0')
|| pattern.chars().any(|c| {
c.is_control() ||
c == '\u{2028}' || c == '\u{2029}' || c == '\u{FEFF}' })
{
return Err(ContextCreatorError::InvalidConfiguration(
"Pattern contains invalid characters (null bytes or control characters)".to_string(),
)
.into());
}
if pattern.starts_with('/') || pattern.starts_with('\\') {
return Err(ContextCreatorError::InvalidConfiguration(
"Absolute paths not allowed in patterns".to_string(),
)
.into());
}
if pattern.contains("..") {
return Err(ContextCreatorError::InvalidConfiguration(
"Directory traversal (..) not allowed in patterns".to_string(),
)
.into());
}
Ok(pattern.to_string())
}
fn build_walker(root: &Path, options: &WalkOptions) -> Result<Walk> {
let mut builder = WalkBuilder::new(root);
builder
.follow_links(options.follow_links)
.hidden(!options.include_hidden)
.git_ignore(true)
.git_global(true)
.git_exclude(true)
.ignore(true)
.parents(true)
.add_custom_ignore_filename(&options.ignore_file);
if !options.include_patterns.is_empty() || !options.ignore_patterns.is_empty() {
let mut override_builder = ignore::overrides::OverrideBuilder::new(root);
if options.include_patterns.is_empty() && !options.ignore_patterns.is_empty() {
override_builder.add("**/*").map_err(|e| {
ContextCreatorError::InvalidConfiguration(format!(
"Failed to add include-all pattern: {e}"
))
})?;
}
for pattern in &options.include_patterns {
if !pattern.trim().is_empty() {
let sanitized_pattern = sanitize_pattern(pattern)?;
override_builder.add(&sanitized_pattern).map_err(|e| {
ContextCreatorError::InvalidConfiguration(format!(
"Invalid include pattern '{pattern}': {e}"
))
})?;
}
}
for pattern in &options.ignore_patterns {
if !pattern.trim().is_empty() {
let sanitized_pattern = sanitize_pattern(pattern)?;
let ignore_pattern = format!("!{sanitized_pattern}");
override_builder.add(&ignore_pattern).map_err(|e| {
ContextCreatorError::InvalidConfiguration(format!(
"Invalid ignore pattern '{pattern}': {e}"
))
})?;
}
}
let overrides = override_builder.build().map_err(|e| {
ContextCreatorError::InvalidConfiguration(format!(
"Failed to build pattern overrides: {e}"
))
})?;
builder.overrides(overrides);
}
Ok(builder.build())
}
fn walk_sequential(walker: Walk, root: &Path, options: &WalkOptions) -> Result<Vec<FileInfo>> {
let mut files = Vec::new();
for entry in walker {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
continue;
}
if let Some(file_info) = process_file(path, root, options)? {
files.push(file_info);
}
}
Ok(files)
}
fn walk_parallel(walker: Walk, root: &Path, options: &WalkOptions) -> Result<Vec<FileInfo>> {
use itertools::Itertools;
let root = Arc::new(root.to_path_buf());
let options = Arc::new(options.clone());
let entries: Vec<_> = walker
.filter_map(|e| e.ok())
.filter(|e| !e.path().is_dir())
.collect();
let results: Vec<Result<Option<FileInfo>, ContextCreatorError>> = entries
.into_par_iter()
.map(|entry| {
let path = entry.path();
match process_file(path, &root, &options) {
Ok(file_info) => Ok(file_info),
Err(e) => Err(ContextCreatorError::FileProcessingError {
path: path.display().to_string(),
error: e.to_string(),
}),
}
})
.collect();
let (successes, errors): (Vec<_>, Vec<_>) = results.into_iter().partition_result();
if !errors.is_empty() {
let critical_errors: Vec<_> = errors
.iter()
.filter(|e| {
e.to_string().contains("Permission denied") || e.to_string().contains("Invalid")
})
.collect();
if !critical_errors.is_empty() {
let error_summary: Vec<String> =
critical_errors.iter().map(|e| e.to_string()).collect();
return Err(anyhow::anyhow!(
"Critical file processing errors encountered: {}",
error_summary.join(", ")
));
}
warn!("Warning: {} files could not be processed:", errors.len());
for error in &errors {
warn!(" {}", error);
}
}
let files: Vec<FileInfo> = successes.into_iter().flatten().collect();
Ok(files)
}
fn process_file(path: &Path, root: &Path, options: &WalkOptions) -> Result<Option<FileInfo>> {
let metadata = match std::fs::metadata(path) {
Ok(meta) => meta,
Err(_) => return Ok(None), };
let size = metadata.len();
if let Some(max_size) = options.max_file_size {
if size > max_size as u64 {
return Ok(None);
}
}
if options.filter_binary_files && is_binary_extension(path) {
return Ok(None);
}
let relative_path = path.strip_prefix(root).unwrap_or(path).to_path_buf();
let file_type = FileType::from_path(path);
if options.filter_binary_files && file_type == FileType::Other {
return Ok(None);
}
let priority = calculate_priority(&file_type, &relative_path, &options.custom_priorities);
Ok(Some(FileInfo {
path: path.to_path_buf(),
relative_path,
size,
file_type,
priority,
imports: Vec::new(), imported_by: Vec::new(), function_calls: Vec::new(), type_references: Vec::new(), exported_functions: Vec::new(), }))
}
fn calculate_priority(
file_type: &FileType,
relative_path: &Path,
custom_priorities: &[CompiledPriority],
) -> f32 {
let base_score = calculate_base_priority(file_type, relative_path);
for priority in custom_priorities {
if priority.matcher.matches_path(relative_path) {
return base_score + priority.weight;
}
}
base_score
}
fn calculate_base_priority(file_type: &FileType, relative_path: &Path) -> f32 {
let mut score: f32 = match file_type {
FileType::Rust => 1.0,
FileType::Python => 0.9,
FileType::JavaScript => 0.9,
FileType::TypeScript => 0.95,
FileType::Go => 0.9,
FileType::Java => 0.85,
FileType::Cpp => 0.85,
FileType::C => 0.8,
FileType::CSharp => 0.85,
FileType::Ruby => 0.8,
FileType::Php => 0.75,
FileType::Swift => 0.85,
FileType::Kotlin => 0.85,
FileType::Scala => 0.8,
FileType::Haskell => 0.75,
FileType::Dart => 0.85,
FileType::Lua => 0.7,
FileType::R => 0.75,
FileType::Julia => 0.8,
FileType::Elixir => 0.8,
FileType::Elm => 0.75,
FileType::Markdown => 0.6,
FileType::Json => 0.5,
FileType::Yaml => 0.5,
FileType::Toml => 0.5,
FileType::Xml => 0.4,
FileType::Html => 0.4,
FileType::Css => 0.4,
FileType::Text => 0.3,
FileType::Other => 0.2,
};
let path_str = relative_path.to_string_lossy().to_lowercase();
if path_str.contains("main") || path_str.contains("index") {
score *= 1.5;
}
if path_str.contains("lib") || path_str.contains("src") {
score *= 1.2;
}
if path_str.contains("test") || path_str.contains("spec") {
score *= 0.8;
}
if path_str.contains("example") || path_str.contains("sample") {
score *= 0.7;
}
if relative_path.parent().is_none() || relative_path.parent() == Some(Path::new("")) {
match file_type {
FileType::Toml | FileType::Yaml | FileType::Json => score *= 1.3,
_ => {}
}
}
score.min(2.0) }
pub fn perform_semantic_analysis(
files: &mut [FileInfo],
config: &crate::cli::Config,
cache: &crate::core::cache::FileCache,
) -> Result<()> {
crate::core::semantic_graph::perform_semantic_analysis_graph(files, config, cache)
}
#[allow(dead_code)]
fn capitalize_first(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::{self, File};
use tempfile::TempDir;
#[test]
fn test_walk_directory_basic() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
File::create(root.join("main.rs")).unwrap();
File::create(root.join("lib.rs")).unwrap();
fs::create_dir(root.join("src")).unwrap();
File::create(root.join("src/utils.rs")).unwrap();
let options = WalkOptions::default();
let files = walk_directory(root, options).unwrap();
assert_eq!(files.len(), 3);
assert!(files
.iter()
.any(|f| f.relative_path == PathBuf::from("main.rs")));
assert!(files
.iter()
.any(|f| f.relative_path == PathBuf::from("lib.rs")));
assert!(files
.iter()
.any(|f| f.relative_path == PathBuf::from("src/utils.rs")));
}
#[test]
fn test_walk_with_contextignore() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
File::create(root.join("main.rs")).unwrap();
File::create(root.join("ignored.rs")).unwrap();
fs::write(root.join(".context-creator-ignore"), "ignored.rs").unwrap();
let options = WalkOptions::default();
let files = walk_directory(root, options).unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].relative_path, PathBuf::from("main.rs"));
}
#[test]
fn test_priority_calculation() {
let rust_priority = calculate_priority(&FileType::Rust, Path::new("src/main.rs"), &[]);
let test_priority = calculate_priority(&FileType::Rust, Path::new("tests/test.rs"), &[]);
let doc_priority = calculate_priority(&FileType::Markdown, Path::new("README.md"), &[]);
assert!(rust_priority > doc_priority);
assert!(rust_priority > test_priority);
}
#[test]
fn test_file_size_limit() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let large_file = root.join("large.txt");
let data = vec![0u8; 1024 * 1024]; fs::write(&large_file, &data).unwrap();
File::create(root.join("small.txt")).unwrap();
let options = WalkOptions {
max_file_size: Some(512 * 1024), ..Default::default()
};
let files = walk_directory(root, options).unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].relative_path, PathBuf::from("small.txt"));
}
#[test]
fn test_walk_empty_directory() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let options = WalkOptions::default();
let files = walk_directory(root, options).unwrap();
assert_eq!(files.len(), 0);
}
#[test]
fn test_walk_options_from_config() {
use crate::cli::Config;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let config = Config {
paths: Some(vec![temp_dir.path().to_path_buf()]),
..Config::default()
};
let options = WalkOptions::from_config(&config).unwrap();
assert_eq!(options.max_file_size, Some(10 * 1024 * 1024));
assert!(!options.follow_links);
assert!(!options.include_hidden);
assert!(options.parallel);
assert_eq!(options.ignore_file, ".context-creator-ignore");
}
#[test]
fn test_walk_with_custom_options() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
File::create(root.join("main.rs")).unwrap();
File::create(root.join("test.rs")).unwrap();
File::create(root.join("readme.md")).unwrap();
let options = WalkOptions {
ignore_patterns: vec!["*.md".to_string()],
..Default::default()
};
let files = walk_directory(root, options).unwrap();
assert!(files.len() >= 2);
assert!(files
.iter()
.any(|f| f.relative_path == PathBuf::from("main.rs")));
assert!(files
.iter()
.any(|f| f.relative_path == PathBuf::from("test.rs")));
}
#[test]
fn test_walk_with_include_patterns() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
File::create(root.join("main.rs")).unwrap();
File::create(root.join("lib.rs")).unwrap();
File::create(root.join("README.md")).unwrap();
let options = WalkOptions {
include_patterns: vec!["*.rs".to_string()],
..Default::default()
};
let files = walk_directory(root, options).unwrap();
assert!(files.len() >= 2);
assert!(files
.iter()
.any(|f| f.relative_path == PathBuf::from("main.rs")));
assert!(files
.iter()
.any(|f| f.relative_path == PathBuf::from("lib.rs")));
}
#[test]
fn test_walk_subdirectories() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
fs::create_dir(root.join("src")).unwrap();
fs::create_dir(root.join("src").join("utils")).unwrap();
File::create(root.join("main.rs")).unwrap();
File::create(root.join("src").join("lib.rs")).unwrap();
File::create(root.join("src").join("utils").join("helpers.rs")).unwrap();
let options = WalkOptions::default();
let files = walk_directory(root, options).unwrap();
assert_eq!(files.len(), 3);
assert!(files
.iter()
.any(|f| f.relative_path == PathBuf::from("main.rs")));
assert!(files
.iter()
.any(|f| f.relative_path == PathBuf::from("src/lib.rs")));
assert!(files
.iter()
.any(|f| f.relative_path == PathBuf::from("src/utils/helpers.rs")));
}
#[test]
fn test_priority_edge_cases() {
let main_priority = calculate_priority(&FileType::Rust, Path::new("main.rs"), &[]);
let lib_priority = calculate_priority(&FileType::Rust, Path::new("lib.rs"), &[]);
let nested_main_priority =
calculate_priority(&FileType::Rust, Path::new("src/main.rs"), &[]);
assert!(main_priority > lib_priority);
assert!(nested_main_priority > lib_priority);
let toml_priority = calculate_priority(&FileType::Toml, Path::new("Cargo.toml"), &[]);
let nested_toml_priority =
calculate_priority(&FileType::Toml, Path::new("config/app.toml"), &[]);
assert!(toml_priority > nested_toml_priority);
}
#[test]
fn test_custom_priority_no_match_returns_base_priority() {
let custom_priorities = [CompiledPriority::new("docs/*.md", 5.0).unwrap()];
let priority = calculate_priority(
&FileType::Rust,
Path::new("src/main.rs"),
&custom_priorities,
);
let expected_base = calculate_priority(&FileType::Rust, Path::new("src/main.rs"), &[]);
assert_eq!(priority, expected_base);
}
#[test]
fn test_custom_priority_single_match_adds_weight() {
let custom_priorities = [CompiledPriority::new("src/core/mod.rs", 10.0).unwrap()];
let priority = calculate_priority(
&FileType::Rust,
Path::new("src/core/mod.rs"),
&custom_priorities,
);
let base_priority = calculate_priority(&FileType::Rust, Path::new("src/core/mod.rs"), &[]);
let expected = base_priority + 10.0;
assert_eq!(priority, expected);
}
#[test]
fn test_custom_priority_glob_pattern_match() {
let custom_priorities = [CompiledPriority::new("src/**/*.rs", 2.5).unwrap()];
let priority = calculate_priority(
&FileType::Rust,
Path::new("src/api/handlers.rs"),
&custom_priorities,
);
let base_priority =
calculate_priority(&FileType::Rust, Path::new("src/api/handlers.rs"), &[]);
let expected = base_priority + 2.5;
assert_eq!(priority, expected);
}
#[test]
fn test_custom_priority_negative_weight() {
let custom_priorities = [CompiledPriority::new("tests/*", -0.5).unwrap()];
let priority = calculate_priority(
&FileType::Rust,
Path::new("tests/test_utils.rs"),
&custom_priorities,
);
let base_priority =
calculate_priority(&FileType::Rust, Path::new("tests/test_utils.rs"), &[]);
let expected = base_priority - 0.5;
assert_eq!(priority, expected);
}
#[test]
fn test_custom_priority_first_match_wins() {
let custom_priorities = [
CompiledPriority::new("src/**/*.rs", 5.0).unwrap(),
CompiledPriority::new("src/main.rs", 100.0).unwrap(),
];
let priority = calculate_priority(
&FileType::Rust,
Path::new("src/main.rs"),
&custom_priorities,
);
let base_priority = calculate_priority(&FileType::Rust, Path::new("src/main.rs"), &[]);
let expected = base_priority + 5.0;
assert_eq!(priority, expected);
}
#[test]
fn test_custom_priority_zero_weight() {
let custom_priorities = [CompiledPriority::new("*.rs", 0.0).unwrap()];
let priority = calculate_priority(
&FileType::Rust,
Path::new("src/main.rs"),
&custom_priorities,
);
let base_priority = calculate_priority(&FileType::Rust, Path::new("src/main.rs"), &[]);
assert_eq!(priority, base_priority);
}
#[test]
fn test_custom_priority_empty_list() {
let custom_priorities: &[CompiledPriority] = &[];
let priority =
calculate_priority(&FileType::Rust, Path::new("src/main.rs"), custom_priorities);
let expected_base = calculate_priority(&FileType::Rust, Path::new("src/main.rs"), &[]);
assert_eq!(priority, expected_base);
}
#[test]
fn test_config_to_walker_data_flow() {
use crate::config::{ConfigFile, Priority};
use std::fs::{self, File};
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
File::create(root.join("high_priority.rs")).unwrap();
File::create(root.join("normal.txt")).unwrap();
fs::create_dir(root.join("logs")).unwrap();
File::create(root.join("logs/app.log")).unwrap();
let config_file = ConfigFile {
priorities: vec![
Priority {
pattern: "*.rs".to_string(),
weight: 10.0,
},
Priority {
pattern: "logs/*.log".to_string(),
weight: -5.0,
},
],
..Default::default()
};
let mut config = crate::cli::Config {
paths: Some(vec![root.to_path_buf()]),
semantic_depth: 3,
..Default::default()
};
config_file.apply_to_cli_config(&mut config);
let walk_options = WalkOptions::from_config(&config).unwrap();
let files = walk_directory(root, walk_options).unwrap();
let rs_file = files
.iter()
.find(|f| {
f.relative_path
.to_string_lossy()
.contains("high_priority.rs")
})
.unwrap();
let log_file = files
.iter()
.find(|f| f.relative_path.to_string_lossy().contains("app.log"))
.unwrap();
let txt_file = files
.iter()
.find(|f| f.relative_path.to_string_lossy().contains("normal.txt"))
.unwrap();
let base_rs = calculate_base_priority(&rs_file.file_type, &rs_file.relative_path);
let base_txt = calculate_base_priority(&txt_file.file_type, &txt_file.relative_path);
let base_log = calculate_base_priority(&log_file.file_type, &log_file.relative_path);
assert_eq!(rs_file.priority, base_rs + 10.0);
assert_eq!(log_file.priority, base_log - 5.0);
assert_eq!(txt_file.priority, base_txt);
}
#[test]
fn test_invalid_glob_pattern_in_config() {
use crate::config::{ConfigFile, Priority};
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let config_file = ConfigFile {
priorities: vec![Priority {
pattern: "[invalid_glob".to_string(),
weight: 5.0,
}],
..Default::default()
};
let mut config = crate::cli::Config {
paths: Some(vec![temp_dir.path().to_path_buf()]),
semantic_depth: 3,
..Default::default()
};
config_file.apply_to_cli_config(&mut config);
let result = WalkOptions::from_config(&config);
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("invalid_glob") || error_msg.contains("Invalid"));
}
#[test]
fn test_empty_custom_priorities_config() {
use crate::config::ConfigFile;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let config_file = ConfigFile {
priorities: vec![], ..Default::default()
};
let mut config = crate::cli::Config {
paths: Some(vec![temp_dir.path().to_path_buf()]),
semantic_depth: 3,
..Default::default()
};
config_file.apply_to_cli_config(&mut config);
let walk_options = WalkOptions::from_config(&config).unwrap();
assert!(walk_directory(temp_dir.path(), walk_options).is_ok());
}
#[test]
fn test_empty_pattern_in_config() {
use crate::config::{ConfigFile, Priority};
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let config_file = ConfigFile {
priorities: vec![Priority {
pattern: "".to_string(),
weight: 5.0,
}],
..Default::default()
};
let mut config = crate::cli::Config {
paths: Some(vec![temp_dir.path().to_path_buf()]),
semantic_depth: 3,
..Default::default()
};
config_file.apply_to_cli_config(&mut config);
let result = WalkOptions::from_config(&config);
assert!(result.is_ok());
let walk_options = result.unwrap();
assert_eq!(walk_options.custom_priorities.len(), 1);
}
#[test]
fn test_extreme_weights_in_config() {
use crate::config::{ConfigFile, Priority};
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let config_file = ConfigFile {
priorities: vec![
Priority {
pattern: "*.rs".to_string(),
weight: f32::MAX,
},
Priority {
pattern: "*.txt".to_string(),
weight: f32::MIN,
},
Priority {
pattern: "*.md".to_string(),
weight: f32::INFINITY,
},
Priority {
pattern: "*.log".to_string(),
weight: f32::NEG_INFINITY,
},
],
..Default::default()
};
let mut config = crate::cli::Config {
paths: Some(vec![temp_dir.path().to_path_buf()]),
semantic_depth: 3,
..Default::default()
};
config_file.apply_to_cli_config(&mut config);
let result = WalkOptions::from_config(&config);
assert!(result.is_ok());
let walk_options = result.unwrap();
assert_eq!(walk_options.custom_priorities.len(), 4);
}
#[test]
fn test_file_info_file_type_display() {
let file_info = FileInfo {
path: PathBuf::from("test.rs"),
relative_path: PathBuf::from("test.rs"),
size: 1000,
file_type: FileType::Rust,
priority: 1.0,
imports: Vec::new(),
imported_by: Vec::new(),
function_calls: Vec::new(),
type_references: Vec::new(),
exported_functions: Vec::new(),
};
assert_eq!(file_info.file_type_display(), "Rust");
let file_info_md = FileInfo {
path: PathBuf::from("README.md"),
relative_path: PathBuf::from("README.md"),
size: 500,
file_type: FileType::Markdown,
priority: 0.6,
imports: Vec::new(),
imported_by: Vec::new(),
function_calls: Vec::new(),
type_references: Vec::new(),
exported_functions: Vec::new(),
};
assert_eq!(file_info_md.file_type_display(), "Markdown");
}
#[test]
fn test_walk_options_from_config_with_include_patterns() {
let config = crate::cli::Config {
include: Some(vec!["**/*.rs".to_string(), "**/test[0-9].py".to_string()]),
semantic_depth: 3,
..Default::default()
};
let options = WalkOptions::from_config(&config).unwrap();
assert_eq!(options.include_patterns, vec!["**/*.rs", "**/test[0-9].py"]);
}
#[test]
fn test_walk_options_from_config_empty_include_patterns() {
let config = crate::cli::Config {
semantic_depth: 3,
..Default::default()
};
let options = WalkOptions::from_config(&config).unwrap();
assert_eq!(options.include_patterns, Vec::<String>::new());
}
#[test]
fn test_walk_options_filters_empty_patterns() {
let config = crate::cli::Config {
include: Some(vec![
"**/*.rs".to_string(),
"".to_string(),
" ".to_string(),
"*.py".to_string(),
]),
semantic_depth: 3,
..Default::default()
};
let options = WalkOptions::from_config(&config).unwrap();
assert_eq!(options.include_patterns, vec!["**/*.rs", "*.py"]);
}
#[test]
fn test_sanitize_pattern_valid_patterns() {
let valid_patterns = vec![
"*.py",
"**/*.rs",
"src/**/*.{js,ts}",
"test[0-9].py",
"**/*{model,service}*.py",
"**/db/**",
"some-file.txt",
"dir/subdir/*.md",
];
for pattern in valid_patterns {
let result = sanitize_pattern(pattern);
assert!(result.is_ok(), "Pattern '{pattern}' should be valid");
assert_eq!(result.unwrap(), pattern);
}
}
#[test]
fn test_sanitize_pattern_length_limit() {
let short_pattern = "a".repeat(999);
let exact_limit = "a".repeat(1000);
let too_long = "a".repeat(1001);
assert!(sanitize_pattern(&short_pattern).is_ok());
assert!(sanitize_pattern(&exact_limit).is_ok());
let result = sanitize_pattern(&too_long);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Pattern too long"));
}
#[test]
fn test_sanitize_pattern_null_bytes() {
let patterns_with_nulls = vec!["test\0.py", "\0*.rs", "**/*.js\0", "dir/\0file.txt"];
for pattern in patterns_with_nulls {
let result = sanitize_pattern(pattern);
assert!(
result.is_err(),
"Pattern with null byte should be rejected: {pattern:?}"
);
assert!(result
.unwrap_err()
.to_string()
.contains("invalid characters"));
}
}
#[test]
fn test_sanitize_pattern_control_characters() {
let control_chars = vec![
"test\x01.py", "file\x08.txt", "dir\x0c/*.rs", "test\x1f.md", "*.py\x7f", ];
for pattern in control_chars {
let result = sanitize_pattern(pattern);
assert!(
result.is_err(),
"Pattern with control char should be rejected: {pattern:?}"
);
assert!(result
.unwrap_err()
.to_string()
.contains("invalid characters"));
}
}
#[test]
fn test_sanitize_pattern_absolute_paths() {
let absolute_paths = vec![
"/etc/passwd",
"/usr/bin/*.sh",
"/home/user/file.txt",
"\\Windows\\System32\\*.dll", "\\Program Files\\*",
];
for pattern in absolute_paths {
let result = sanitize_pattern(pattern);
assert!(
result.is_err(),
"Absolute path should be rejected: {pattern}"
);
assert!(result
.unwrap_err()
.to_string()
.contains("Absolute paths not allowed"));
}
}
#[test]
fn test_sanitize_pattern_directory_traversal() {
let traversal_patterns = vec![
"../../../etc/passwd",
"dir/../../../file.txt",
"**/../secret/*",
"test/../../*.py",
"../config.toml",
"subdir/../../other.rs",
];
for pattern in traversal_patterns {
let result = sanitize_pattern(pattern);
assert!(
result.is_err(),
"Directory traversal should be rejected: {pattern}"
);
assert!(result
.unwrap_err()
.to_string()
.contains("Directory traversal"));
}
}
#[test]
fn test_sanitize_pattern_edge_cases() {
let result = sanitize_pattern("");
assert!(result.is_ok(), "Empty string should be allowed");
let result = sanitize_pattern(" ");
assert!(result.is_ok(), "Whitespace-only should be allowed");
let result = sanitize_pattern("файл*.txt");
assert!(result.is_ok(), "Unicode should be allowed");
let result = sanitize_pattern("file[!abc]*.{py,rs}");
assert!(result.is_ok(), "Complex glob patterns should be allowed");
let result = sanitize_pattern("file\nname.txt");
assert!(result.is_err(), "Newlines should be rejected");
let result = sanitize_pattern("file\tname.txt");
assert!(result.is_err(), "Tabs should be rejected");
}
#[test]
fn test_sanitize_pattern_boundary_conditions() {
let result = sanitize_pattern("file..name.txt");
assert!(result.is_err(), "Any '..' should be rejected for safety");
let result = sanitize_pattern("**/*.py");
assert!(result.is_ok(), "Recursive glob should be allowed");
let result = sanitize_pattern("valid/*.py/../invalid");
assert!(result.is_err(), "Mixed pattern should be rejected");
}
#[test]
fn test_sanitize_pattern_security_bypass_attempts() {
let result = sanitize_pattern("file%00.txt");
assert!(result.is_ok(), "URL encoding should not be decoded");
let result = sanitize_pattern("file%2e%2e/secret");
assert!(result.is_ok(), "Double encoding should not be decoded");
let result = sanitize_pattern("file\u{002e}\u{002e}/secret");
assert!(result.is_err(), "Unicode dots should be treated as '..'");
let result = sanitize_pattern("legitimate-pattern\0");
assert!(result.is_err(), "Trailing null should be caught");
}
#[test]
fn test_error_handling_classification() {
use crate::utils::error::ContextCreatorError;
let critical_errors = [
ContextCreatorError::FileProcessingError {
path: "test.txt".to_string(),
error: "Permission denied".to_string(),
},
ContextCreatorError::InvalidConfiguration("Invalid pattern".to_string()),
];
let error_string = critical_errors[0].to_string();
assert!(error_string.contains("Permission denied"));
let error_string = critical_errors[1].to_string();
assert!(error_string.contains("Invalid"));
}
#[test]
fn test_pattern_sanitization_integration() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let options = WalkOptions {
max_file_size: Some(1024),
follow_links: false,
include_hidden: false,
parallel: false,
ignore_file: ".context-creator-ignore".to_string(),
ignore_patterns: vec![],
include_patterns: vec!["../../../etc/passwd".to_string()], custom_priorities: vec![],
filter_binary_files: false,
};
let result = build_walker(root, &options);
assert!(
result.is_err(),
"Directory traversal pattern should be rejected by sanitization"
);
if let Err(e) = result {
let error_msg = e.to_string();
assert!(error_msg.contains("Directory traversal") || error_msg.contains("Invalid"));
}
}
#[test]
fn test_walk_options_filters_binary_files_with_prompt() {
use crate::cli::Config;
let config = Config {
prompt: Some("test prompt".to_string()),
paths: Some(vec![PathBuf::from(".")]),
llm_tool: crate::cli::LlmTool::Gemini,
semantic_depth: 3,
..Default::default()
};
let options = WalkOptions::from_config(&config).unwrap();
assert!(options.filter_binary_files);
}
#[test]
fn test_walk_options_no_binary_filter_without_prompt() {
use crate::cli::Config;
let config = Config {
paths: Some(vec![PathBuf::from(".")]),
llm_tool: crate::cli::LlmTool::Gemini,
semantic_depth: 3,
..Default::default()
};
let options = WalkOptions::from_config(&config).unwrap();
assert!(!options.filter_binary_files);
}
#[test]
fn test_filter_binary_files_when_enabled() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
File::create(root.join("image.jpg")).unwrap();
File::create(root.join("video.mp4")).unwrap();
File::create(root.join("main.rs")).unwrap();
File::create(root.join("config.json")).unwrap();
let options = WalkOptions {
filter_binary_files: true,
..Default::default()
};
let files = walk_directory(root, options).unwrap();
assert_eq!(files.len(), 2);
assert!(files
.iter()
.any(|f| f.relative_path == PathBuf::from("main.rs")));
assert!(files
.iter()
.any(|f| f.relative_path == PathBuf::from("config.json")));
assert!(!files
.iter()
.any(|f| f.relative_path == PathBuf::from("image.jpg")));
assert!(!files
.iter()
.any(|f| f.relative_path == PathBuf::from("video.mp4")));
}
#[test]
fn test_no_filtering_when_disabled() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
File::create(root.join("image.jpg")).unwrap();
File::create(root.join("video.mp4")).unwrap();
File::create(root.join("main.rs")).unwrap();
File::create(root.join("config.json")).unwrap();
let options = WalkOptions {
filter_binary_files: false,
..Default::default()
};
let files = walk_directory(root, options).unwrap();
assert_eq!(files.len(), 4);
assert!(files
.iter()
.any(|f| f.relative_path == PathBuf::from("main.rs")));
assert!(files
.iter()
.any(|f| f.relative_path == PathBuf::from("config.json")));
assert!(files
.iter()
.any(|f| f.relative_path == PathBuf::from("image.jpg")));
assert!(files
.iter()
.any(|f| f.relative_path == PathBuf::from("video.mp4")));
}
#[test]
fn test_edge_case_files_without_extensions() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
File::create(root.join("README")).unwrap();
File::create(root.join("LICENSE")).unwrap();
File::create(root.join("Makefile")).unwrap();
File::create(root.join("Dockerfile")).unwrap();
File::create(root.join("binary.exe")).unwrap();
let options = WalkOptions {
filter_binary_files: true,
..Default::default()
};
let files = walk_directory(root, options).unwrap();
assert_eq!(files.len(), 4);
assert!(files
.iter()
.any(|f| f.relative_path == PathBuf::from("README")));
assert!(files
.iter()
.any(|f| f.relative_path == PathBuf::from("LICENSE")));
assert!(files
.iter()
.any(|f| f.relative_path == PathBuf::from("Makefile")));
assert!(files
.iter()
.any(|f| f.relative_path == PathBuf::from("Dockerfile")));
assert!(!files
.iter()
.any(|f| f.relative_path == PathBuf::from("binary.exe")));
}
}