Skip to main content

opengrep/search/
mod.rs

1//! Core search functionality
2
3use anyhow::Result;
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6use tracing::debug;
7
8pub mod engine;
9pub mod matcher;
10pub mod walker;
11
12pub use engine::SearchEngine;
13pub use matcher::{Matcher, MatchType};
14
15/// A search match
16#[derive(Debug, Clone, serde::Serialize)]
17pub struct Match {
18    /// Line number (1-indexed)
19    pub line_number: usize,
20    /// Column range of the match
21    #[serde(skip_serializing)]
22    pub column_range: std::ops::Range<usize>,
23    /// The matched line
24    pub line_text: String,
25    /// Lines before the match (context)
26    pub before_context: Vec<String>,
27    /// Lines after the match (context)
28    pub after_context: Vec<String>,
29    /// AST context
30    pub ast_context: Option<crate::ast::AstContext>,
31    /// Match score (for ranking)
32    pub score: f64,
33}
34
35/// Search result for a file
36#[derive(Debug, Clone, serde::Serialize)]
37pub struct SearchResult {
38    /// File path
39    pub path: PathBuf,
40    /// All matches in the file
41    pub matches: Vec<Match>,
42    /// File metadata
43    pub metadata: FileMetadata,
44    /// AI insights if enabled
45    #[cfg(feature = "ai")]
46    pub ai_insights: Option<AiInsights>,
47}
48
49/// File metadata
50#[derive(Debug, Clone, serde::Serialize)]
51pub struct FileMetadata {
52    /// File size in bytes
53    pub size: u64,
54    /// Detected language
55    pub language: Option<String>,
56    /// File encoding
57    pub encoding: String,
58    /// Last modified time
59    #[serde(skip_serializing)]
60    pub modified: std::time::SystemTime,
61}
62
63/// AI-generated insights
64#[cfg(feature = "ai")]
65#[derive(Debug, Clone, serde::Serialize)]
66pub struct AiInsights {
67    /// Summary of matches
68    pub summary: String,
69    /// Code explanation
70    pub explanation: Option<String>,
71    /// Suggested refactorings
72    pub suggestions: Vec<String>,
73    /// Related code locations
74    pub related_locations: Vec<PathBuf>,
75}
76
77/// Search statistics
78#[derive(Debug, Default)]
79pub struct SearchStats {
80    /// Files searched
81    pub files_searched: usize,
82    /// Files with matches
83    pub files_matched: usize,
84    /// Total matches
85    pub total_matches: usize,
86    /// Files skipped
87    pub files_skipped: usize,
88    /// Search duration
89    pub duration: std::time::Duration,
90}
91
92/// Search options
93pub struct SearchOptions {
94    /// Pattern to search for
95    pub pattern: String,
96    /// Matcher to use
97    pub matcher: Arc<dyn Matcher>,
98    /// Search configuration
99    pub config: Arc<crate::Config>,
100}
101
102impl Clone for SearchOptions {
103    fn clone(&self) -> Self {
104        Self {
105            pattern: self.pattern.clone(),
106            matcher: Arc::clone(&self.matcher),
107            config: Arc::clone(&self.config),
108        }
109    }
110}
111
112impl SearchOptions {
113    /// Create new search options
114    pub fn new(pattern: &str, config: Arc<crate::Config>) -> Result<Self> {
115        let matcher: Arc<dyn Matcher> = if config.search.regex {
116            Arc::new(matcher::RegexMatcher::new(pattern, config.search.ignore_case)?)
117        } else {
118            Arc::new(matcher::LiteralMatcher::new(pattern, config.search.ignore_case))
119        };
120        
121        Ok(Self {
122            pattern: pattern.to_string(),
123            matcher,
124            config,
125        })
126    }
127}
128
129/// Perform a search in a single file
130pub async fn search_file(
131    path: &Path,
132    options: &SearchOptions,
133) -> Result<Option<SearchResult>> {
134    use tokio::fs;
135    use tokio::io::AsyncReadExt;
136    
137    // Read file
138    let metadata = fs::metadata(path).await?;
139    
140    // Check file size limit
141    if let Some(max_size) = options.config.search.max_file_size {
142        if metadata.len() > max_size {
143            debug!("Skipping large file: {}", path.display());
144            return Ok(None);
145        }
146    }
147    
148    let mut file = fs::File::open(path).await?;
149    let mut contents = String::new();
150    file.read_to_string(&mut contents).await?;
151    
152    // Detect language
153    let language = crate::parsers::detect_language(path);
154    
155    // Search for matches
156    let matches = find_matches(&contents, options, path, language.as_deref()).await?;
157    
158    if matches.is_empty() {
159        return Ok(None);
160    }
161    
162    // Create result
163    let result = SearchResult {
164        path: path.to_path_buf(),
165        matches,
166        metadata: FileMetadata {
167            size: metadata.len(),
168            language,
169            encoding: "UTF-8".to_string(),
170            modified: metadata.modified()?,
171        },
172        #[cfg(feature = "ai")]
173        ai_insights: None,
174    };
175    
176    Ok(Some(result))
177}
178
179/// Find matches in file content
180async fn find_matches(
181    content: &str,
182    options: &SearchOptions,
183    path: &Path,
184    language: Option<&str>,
185) -> Result<Vec<Match>> {
186    let lines: Vec<&str> = content.lines().collect();
187    let mut matches = Vec::new();
188    
189    // Parse AST if language is detected and AST context is enabled
190    let parsed_ast = if options.config.output.show_ast_context && language.is_some() {
191        match crate::parsers::parse_ast(content, language.unwrap()) {
192            Ok(ast) => Some(ast),
193            Err(e) => {
194                debug!("Failed to parse AST for {}: {}", path.display(), e);
195                None
196            }
197        }
198    } else {
199        None
200    };
201    
202    // Search each line
203    for (line_idx, line) in lines.iter().enumerate() {
204        if let Some(column_range) = options.matcher.find_match(line) {
205            let line_number = line_idx + 1;
206            
207            // Get context lines
208            let before_start = line_idx.saturating_sub(options.config.output.before_context);
209            let after_end = (line_idx + options.config.output.after_context + 1).min(lines.len());
210            
211            let before_context = lines[before_start..line_idx]
212                .iter()
213                .map(|s| s.to_string())
214                .collect();
215            
216            let after_context = lines[(line_idx + 1)..after_end]
217                .iter()
218                .map(|s| s.to_string())
219                .collect();
220            
221            // Get AST context
222            let ast_context = parsed_ast
223                .as_ref()
224                .map(|ast| ast.get_context_for_line(line_idx));
225            
226            matches.push(Match {
227                line_number,
228                column_range,
229                line_text: line.to_string(),
230                before_context,
231                after_context,
232                ast_context: ast_context.flatten(),
233                score: 1.0,
234            });
235        }
236    }
237    
238    Ok(matches)
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244    
245    #[test]
246    fn test_match_creation() {
247        let m = Match {
248            line_number: 10,
249            column_range: 5..10,
250            line_text: "test line".to_string(),
251            before_context: vec![],
252            after_context: vec![],
253            ast_context: None,
254            score: 1.0,
255        };
256        
257        assert_eq!(m.line_number, 10);
258        assert_eq!(m.column_range, 5..10);
259    }
260}