opengrep 1.1.0

Advanced AST-aware code search tool with tree-sitter parsing and AI integration capabilities
Documentation
//! Configuration structures for OpenGrep

use anyhow::Result;
use serde::{Deserialize, Serialize};

/// Main configuration structure
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    /// Search configuration
    pub search: SearchConfig,
    /// Output configuration
    pub output: OutputConfig,
    /// AI configuration
    #[cfg(feature = "ai")]
    pub ai: Option<AiConfig>,
}

/// Search configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchConfig {
    /// Case-insensitive search
    pub ignore_case: bool,
    /// Use regex patterns
    pub regex: bool,
    /// Follow symlinks
    pub follow_symlinks: bool,
    /// Maximum depth for directory traversal
    pub max_depth: Option<usize>,
    /// File size limit in bytes
    pub max_file_size: Option<u64>,
    /// Number of threads for parallel search
    pub threads: usize,
    /// Include hidden files
    pub hidden: bool,
    /// Respect .gitignore
    pub respect_gitignore: bool,
    /// Include binary files
    pub include_binary: bool,
    /// Maximum number of matches per file
    pub max_matches_per_file: Option<usize>,
}

/// Output configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutputConfig {
    /// Enable color output
    pub color: bool,
    /// Show line numbers
    pub line_numbers: bool,
    /// Context lines before match
    pub before_context: usize,
    /// Context lines after match
    pub after_context: usize,
    /// Show AST context
    pub show_ast_context: bool,
    /// Maximum AST depth to show
    pub max_ast_depth: usize,
    /// Highlight matches
    pub highlight: bool,
    /// Show file headers
    pub show_file_headers: bool,
    /// Show match count
    pub show_match_count: bool,
}

/// AI configuration
#[cfg(feature = "ai")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AiConfig {
    /// OpenAI API key
    pub api_key: String,
    /// Model to use
    pub model: String,
    /// Enable AI-powered insights
    pub enable_insights: bool,
    /// Enable code explanation
    pub enable_explanation: bool,
    /// Maximum tokens for AI responses
    pub max_tokens: u32,
    /// Temperature for AI responses
    pub temperature: f32,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            search: SearchConfig::default(),
            output: OutputConfig::default(),
            #[cfg(feature = "ai")]
            ai: None,
        }
    }
}

impl Default for SearchConfig {
    fn default() -> Self {
        Self {
            ignore_case: false,
            regex: false,
            follow_symlinks: false,
            max_depth: None,
            max_file_size: Some(50 * 1024 * 1024), // 50MB
            threads: num_cpus::get(),
            hidden: false,
            respect_gitignore: true,
            include_binary: false,
            max_matches_per_file: Some(1000),
        }
    }
}

impl Default for OutputConfig {
    fn default() -> Self {
        Self {
            color: atty::is(atty::Stream::Stdout),
            line_numbers: true,
            before_context: 2,
            after_context: 2,
            show_ast_context: false,
            max_ast_depth: 3,
            highlight: true,
            show_file_headers: true,
            show_match_count: false,
        }
    }
}

#[cfg(feature = "ai")]
impl Default for AiConfig {
    fn default() -> Self {
        Self {
            api_key: String::new(),
            model: "gpt-4o-mini".to_string(),
            enable_insights: false,
            enable_explanation: false,
            max_tokens: 1000,
            temperature: 0.3,
        }
    }
}

impl Config {
    /// Create configuration from CLI arguments
    pub fn from_cli(cli: &crate::cli::Cli) -> Result<Self> {
        let mut config = Self::default();
        
        config.search.ignore_case = cli.ignore_case;
        config.search.regex = cli.regex;
        config.search.follow_symlinks = cli.follow_symlinks;
        config.search.max_depth = cli.max_depth;
        config.search.max_file_size = cli.max_file_size;
        config.search.threads = cli.threads.unwrap_or_else(num_cpus::get);
        config.search.hidden = cli.hidden;
        config.search.respect_gitignore = !cli.no_ignore;
        
        config.output.color = cli.color.unwrap_or_else(|| atty::is(atty::Stream::Stdout));
        config.output.line_numbers = cli.line_numbers;
        config.output.before_context = cli.before_context;
        config.output.after_context = cli.after_context;
        config.output.show_ast_context = cli.ast_context;
        config.output.max_ast_depth = cli.max_ast_depth;
        
        #[cfg(feature = "ai")]
        if cli.ai_insights || cli.ai_explain {
            config.ai = Some(AiConfig {
                api_key: std::env::var("OPENAI_API_KEY")
                    .map_err(|_| anyhow::anyhow!("OPENAI_API_KEY environment variable required for AI features"))?,
                model: cli.ai_model.clone(),
                enable_insights: cli.ai_insights,
                enable_explanation: cli.ai_explain,
                max_tokens: 1000,
                temperature: 0.3,
            });
        }
        
        Ok(config)
    }
    
    /// Load configuration from file
    pub fn from_file(path: &std::path::Path) -> Result<Self> {
        let content = std::fs::read_to_string(path)?;
        let config = if path.extension().and_then(|s| s.to_str()) == Some("toml") {
            toml::from_str(&content)?
        } else {
            serde_yaml::from_str(&content)?
        };
        Ok(config)
    }
    
    /// Save configuration to file
    pub fn save_to_file(&self, path: &std::path::Path) -> Result<()> {
        let content = if path.extension().and_then(|s| s.to_str()) == Some("toml") {
            toml::to_string_pretty(self)?
        } else {
            serde_yaml::to_string(self)?
        };
        std::fs::write(path, content)?;
        Ok(())
    }
    
    /// Validate configuration
    pub fn validate(&self) -> Result<()> {
        if self.search.threads == 0 {
            anyhow::bail!("Thread count must be greater than 0");
        }
        
        if self.output.before_context > 1000 || self.output.after_context > 1000 {
            anyhow::bail!("Context lines must be <= 1000");
        }
        
        if self.output.max_ast_depth > 20 {
            anyhow::bail!("AST depth must be <= 20");
        }
        
        #[cfg(feature = "ai")]
        if let Some(ai_config) = &self.ai {
            if ai_config.api_key.is_empty() && (ai_config.enable_insights || ai_config.enable_explanation) {
                anyhow::bail!("OpenAI API key required for AI features");
            }
            
            if ai_config.max_tokens == 0 || ai_config.max_tokens > 4096 {
                anyhow::bail!("AI max tokens must be between 1 and 4096");
            }
            
            if !(0.0..=2.0).contains(&ai_config.temperature) {
                anyhow::bail!("AI temperature must be between 0.0 and 2.0");
            }
        }
        
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_default_config() {
        let config = Config::default();
        assert!(!config.search.ignore_case);
        assert!(!config.search.regex);
        assert!(config.search.respect_gitignore);
        assert!(config.output.line_numbers);
    }
    
    #[test]
    fn test_config_validation() {
        let mut config = Config::default();
        assert!(config.validate().is_ok());
        
        config.search.threads = 0;
        assert!(config.validate().is_err());
        
        config.search.threads = 1;
        config.output.before_context = 2000;
        assert!(config.validate().is_err());
    }
    
    #[test]
    fn test_config_serialization() {
        let config = Config::default();
        let yaml = serde_yaml::to_string(&config).unwrap();
        let deserialized: Config = serde_yaml::from_str(&yaml).unwrap();
        
        assert_eq!(config.search.threads, deserialized.search.threads);
        assert_eq!(config.output.line_numbers, deserialized.output.line_numbers);
    }
}