opengrep 1.1.0

Advanced AST-aware code search tool with tree-sitter parsing and AI integration capabilities
Documentation
//! Core search functionality

use anyhow::Result;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tracing::debug;

pub mod engine;
pub mod matcher;
pub mod walker;

pub use engine::SearchEngine;
pub use matcher::{Matcher, MatchType};

/// A search match
#[derive(Debug, Clone, serde::Serialize)]
pub struct Match {
    /// Line number (1-indexed)
    pub line_number: usize,
    /// Column range of the match
    #[serde(skip_serializing)]
    pub column_range: std::ops::Range<usize>,
    /// The matched line
    pub line_text: String,
    /// Lines before the match (context)
    pub before_context: Vec<String>,
    /// Lines after the match (context)
    pub after_context: Vec<String>,
    /// AST context
    pub ast_context: Option<crate::ast::AstContext>,
    /// Match score (for ranking)
    pub score: f64,
}

/// Search result for a file
#[derive(Debug, Clone, serde::Serialize)]
pub struct SearchResult {
    /// File path
    pub path: PathBuf,
    /// All matches in the file
    pub matches: Vec<Match>,
    /// File metadata
    pub metadata: FileMetadata,
    /// AI insights if enabled
    #[cfg(feature = "ai")]
    pub ai_insights: Option<AiInsights>,
}

/// File metadata
#[derive(Debug, Clone, serde::Serialize)]
pub struct FileMetadata {
    /// File size in bytes
    pub size: u64,
    /// Detected language
    pub language: Option<String>,
    /// File encoding
    pub encoding: String,
    /// Last modified time
    #[serde(skip_serializing)]
    pub modified: std::time::SystemTime,
}

/// AI-generated insights
#[cfg(feature = "ai")]
#[derive(Debug, Clone, serde::Serialize)]
pub struct AiInsights {
    /// Summary of matches
    pub summary: String,
    /// Code explanation
    pub explanation: Option<String>,
    /// Suggested refactorings
    pub suggestions: Vec<String>,
    /// Related code locations
    pub related_locations: Vec<PathBuf>,
}

/// Search statistics
#[derive(Debug, Default)]
pub struct SearchStats {
    /// Files searched
    pub files_searched: usize,
    /// Files with matches
    pub files_matched: usize,
    /// Total matches
    pub total_matches: usize,
    /// Files skipped
    pub files_skipped: usize,
    /// Search duration
    pub duration: std::time::Duration,
}

/// Search options
pub struct SearchOptions {
    /// Pattern to search for
    pub pattern: String,
    /// Matcher to use
    pub matcher: Arc<dyn Matcher>,
    /// Search configuration
    pub config: Arc<crate::Config>,
}

impl Clone for SearchOptions {
    fn clone(&self) -> Self {
        Self {
            pattern: self.pattern.clone(),
            matcher: Arc::clone(&self.matcher),
            config: Arc::clone(&self.config),
        }
    }
}

impl SearchOptions {
    /// Create new search options
    pub fn new(pattern: &str, config: Arc<crate::Config>) -> Result<Self> {
        let matcher: Arc<dyn Matcher> = if config.search.regex {
            Arc::new(matcher::RegexMatcher::new(pattern, config.search.ignore_case)?)
        } else {
            Arc::new(matcher::LiteralMatcher::new(pattern, config.search.ignore_case))
        };
        
        Ok(Self {
            pattern: pattern.to_string(),
            matcher,
            config,
        })
    }
}

/// Perform a search in a single file
pub async fn search_file(
    path: &Path,
    options: &SearchOptions,
) -> Result<Option<SearchResult>> {
    use tokio::fs;
    use tokio::io::AsyncReadExt;
    
    // Read file
    let metadata = fs::metadata(path).await?;
    
    // Check file size limit
    if let Some(max_size) = options.config.search.max_file_size {
        if metadata.len() > max_size {
            debug!("Skipping large file: {}", path.display());
            return Ok(None);
        }
    }
    
    let mut file = fs::File::open(path).await?;
    let mut contents = String::new();
    file.read_to_string(&mut contents).await?;
    
    // Detect language
    let language = crate::parsers::detect_language(path);
    
    // Search for matches
    let matches = find_matches(&contents, options, path, language.as_deref()).await?;
    
    if matches.is_empty() {
        return Ok(None);
    }
    
    // Create result
    let result = SearchResult {
        path: path.to_path_buf(),
        matches,
        metadata: FileMetadata {
            size: metadata.len(),
            language,
            encoding: "UTF-8".to_string(),
            modified: metadata.modified()?,
        },
        #[cfg(feature = "ai")]
        ai_insights: None,
    };
    
    Ok(Some(result))
}

/// Find matches in file content
async fn find_matches(
    content: &str,
    options: &SearchOptions,
    path: &Path,
    language: Option<&str>,
) -> Result<Vec<Match>> {
    let lines: Vec<&str> = content.lines().collect();
    let mut matches = Vec::new();
    
    // Parse AST if language is detected and AST context is enabled
    let parsed_ast = if options.config.output.show_ast_context && language.is_some() {
        match crate::parsers::parse_ast(content, language.unwrap()) {
            Ok(ast) => Some(ast),
            Err(e) => {
                debug!("Failed to parse AST for {}: {}", path.display(), e);
                None
            }
        }
    } else {
        None
    };
    
    // Search each line
    for (line_idx, line) in lines.iter().enumerate() {
        if let Some(column_range) = options.matcher.find_match(line) {
            let line_number = line_idx + 1;
            
            // Get context lines
            let before_start = line_idx.saturating_sub(options.config.output.before_context);
            let after_end = (line_idx + options.config.output.after_context + 1).min(lines.len());
            
            let before_context = lines[before_start..line_idx]
                .iter()
                .map(|s| s.to_string())
                .collect();
            
            let after_context = lines[(line_idx + 1)..after_end]
                .iter()
                .map(|s| s.to_string())
                .collect();
            
            // Get AST context
            let ast_context = parsed_ast
                .as_ref()
                .map(|ast| ast.get_context_for_line(line_idx));
            
            matches.push(Match {
                line_number,
                column_range,
                line_text: line.to_string(),
                before_context,
                after_context,
                ast_context: ast_context.flatten(),
                score: 1.0,
            });
        }
    }
    
    Ok(matches)
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_match_creation() {
        let m = Match {
            line_number: 10,
            column_range: 5..10,
            line_text: "test line".to_string(),
            before_context: vec![],
            after_context: vec![],
            ast_context: None,
            score: 1.0,
        };
        
        assert_eq!(m.line_number, 10);
        assert_eq!(m.column_range, 5..10);
    }
}