codesearch 0.1.14

A fast, intelligent CLI tool with multiple search modes (regex, fuzzy, semantic), code analysis, and dead code detection for popular programming languages
Documentation
//! Search Module
//!
//! Core search functionality with parallel processing, fuzzy matching, and semantic search.

pub mod bm25;
pub mod context;
pub mod core;
pub mod declarations;
pub mod engine;
pub mod fuzzy;
pub mod pure;
pub mod semantic;
pub mod utilities;

pub use core::{list_files, search_code};
pub use engine::DefaultSearchEngine;
pub use fuzzy::{calculate_relevance_score, search_in_file_parallel};
pub use semantic::enhance_query_semantically;
pub use utilities::{compare_with_grep, print_results, print_search_stats};

/// Unified matcher for regex or fixed-string (aho-corasick) search.
///
/// This enum abstracts over the two search backends so that the parallel
/// file search code can work with either without branching at every line.
pub enum SearchMatcher {
    Regex(std::sync::Arc<regex::Regex>),
    Fixed(aho_corasick::AhoCorasick),
}

impl SearchMatcher {
    /// Find the first match in a haystack.
    pub fn find<'t>(&self, text: &'t str) -> Option<std::ops::Range<usize>> {
        match self {
            SearchMatcher::Regex(re) => re.find(text).map(|m| m.range()),
            SearchMatcher::Fixed(ac) => {
                ac.find(text).map(|m| m.start()..m.end())
            }
        }
    }

    /// Iterate over all matches in a haystack.
    pub fn find_iter<'a, 't: 'a>(
        &'a self,
        text: &'t str,
    ) -> Box<dyn Iterator<Item = std::ops::Range<usize>> + 'a> {
        match self {
            SearchMatcher::Regex(re) => {
                Box::new(re.find_iter(text).map(|m| m.range()))
            }
            SearchMatcher::Fixed(ac) => {
                Box::new(ac.find_iter(text).map(|m| m.start()..m.end()))
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::SearchOptions;
    use std::path::Path;

    #[test]
    fn test_enhance_query_semantically() {
        let enhanced = enhance_query_semantically("function");
        assert!(
            enhanced.contains("function") || enhanced.contains("def") || enhanced.contains("fn")
        );

        let enhanced = enhance_query_semantically("class");
        assert!(enhanced.contains("class") || enhanced.contains("struct"));
    }

    #[test]
    fn test_calculate_relevance_score() {
        let score = calculate_relevance_score(
            "fn test_function() {",
            "test",
            10,
            Path::new("test.rs"),
            false,
            None,
        );
        assert!(score > 0.0);
        assert!(score <= 100.0);
    }

    #[test]
    fn test_search_code_basic() {
        use std::fs;
        use tempfile::tempdir;

        let dir = tempdir().unwrap();
        let file_path = dir.path().join("test.rs");
        fs::write(&file_path, "fn test() {\n    println!(\"hello\");\n}").unwrap();

        let options = SearchOptions {
            extensions: Some(vec!["rs".to_string()]),
            ignore_case: true,
            fuzzy: false,
            fuzzy_threshold: 0.6,
            max_results: 10,
            exclude: None,
            rank: false,
            cache: false,
            semantic: false,
            benchmark: false,
            vs_grep: false,
            use_bm25: false,
            context_filter: None,
            fixed_string: false,
            declaration_filter: None,
        };
        let results = search_code("test", dir.path(), &options);

        assert!(results.is_ok());
        let results = results.unwrap();
        assert!(!results.is_empty());
    }

    #[test]
    fn test_list_files() {
        use std::fs;
        use tempfile::tempdir;

        let dir = tempdir().unwrap();
        fs::write(dir.path().join("test1.rs"), "test").unwrap();
        fs::write(dir.path().join("test2.py"), "test").unwrap();

        let files = list_files(dir.path(), Some(&["rs".to_string()]), None);
        assert!(files.is_ok());
        let files = files.unwrap();
        assert_eq!(files.len(), 1);
    }

    #[test]
    fn test_list_files_with_extension_filter() {
        use std::fs;
        use tempfile::tempdir;

        let dir = tempdir().unwrap();
        fs::write(dir.path().join("test1.rs"), "test").unwrap();
        fs::write(dir.path().join("test2.py"), "test").unwrap();

        let files = list_files(dir.path(), Some(&["rs".to_string()]), None);
        assert!(files.is_ok());
        let files = files.unwrap();
        assert_eq!(files.len(), 1);
    }

    #[test]
    fn test_list_files_nonexistent_directory() {
        use std::path::Path;

        let result = list_files(Path::new("/nonexistent/path"), None, None);
        assert!(result.is_err());
        // The error should mention the directory was not found
        let error_msg = result.unwrap_err().to_string();
        assert!(error_msg.contains("not found") || error_msg.contains("Directory"));
    }

    #[test]
    fn test_list_files_file_instead_of_directory() {
        use std::fs;
        use tempfile::tempdir;

        let dir = tempdir().unwrap();
        let file_path = dir.path().join("test.rs");
        fs::write(&file_path, "fn test() {}").unwrap();

        // Try to list files on a file path instead of directory
        let result = list_files(&file_path, None, None);
        assert!(result.is_err());
        let error_msg = result.unwrap_err().to_string();
        assert!(error_msg.contains("not a directory") || error_msg.contains("Invalid"));
    }

    #[test]
    fn test_search_code_with_invalid_regex() {
        use std::fs;
        use tempfile::tempdir;

        let dir = tempdir().unwrap();
        fs::write(dir.path().join("test.rs"), "fn test() {}").unwrap();

        // Invalid regex pattern (unclosed bracket)
        let options = SearchOptions {
            extensions: Some(vec!["rs".to_string()]),
            ignore_case: false,
            fuzzy: false,
            fuzzy_threshold: 0.6,
            max_results: 10,
            exclude: None,
            rank: false,
            cache: false,
            semantic: false,
            benchmark: false,
            vs_grep: false,
            use_bm25: false,
            context_filter: None,
            fixed_string: false,
            declaration_filter: None,
        };

        let result = search_code("[invalid", dir.path(), &options);
        assert!(result.is_err());
        let error_msg = result.unwrap_err().to_string();
        assert!(error_msg.contains("Invalid regex") || error_msg.contains("Invalid pattern"));
    }

    #[test]
    fn test_search_code_with_extension_filter() {
        use std::fs;
        use tempfile::tempdir;

        let dir = tempdir().unwrap();
        fs::write(dir.path().join("test.rs"), "fn test() {}").unwrap();
        fs::write(dir.path().join("test.py"), "def test(): pass").unwrap();

        let options = SearchOptions {
            extensions: Some(vec!["rs".to_string()]),
            ignore_case: true,
            fuzzy: false,
            fuzzy_threshold: 0.6,
            max_results: 10,
            exclude: None,
            rank: false,
            cache: false,
            semantic: false,
            benchmark: false,
            vs_grep: false,
            use_bm25: false,
            context_filter: None,
            fixed_string: false,
            declaration_filter: None,
        };
        let results = search_code("test", dir.path(), &options);

        assert!(results.is_ok());
        let results = results.unwrap();
        assert_eq!(results.len(), 1);
    }
}