opengrep 1.1.0

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

use clap::{Parser, ValueEnum};
use std::path::PathBuf;

/// OpenGrep - Advanced AST-aware code search
#[derive(Parser, Debug)]
#[command(
    name = "opengrep",
    version,
    about = "Advanced AST-aware code search with AI enhancement",
    long_about = "OpenGrep is a next-generation code search tool that understands your code structure \
                  and provides intelligent insights. It combines the power of tree-sitter for AST parsing \
                  with optional AI integration for code analysis and suggestions."
)]
pub struct Cli {
    /// Search pattern
    #[arg(value_name = "PATTERN", help = "The pattern to search for")]
    pub pattern: String,
    
    /// Paths to search (defaults to current directory)
    #[arg(value_name = "PATH", help = "Files or directories to search")]
    pub paths: Vec<PathBuf>,
    
    /// Case-insensitive search
    #[arg(short, long, help = "Perform case-insensitive matching")]
    pub ignore_case: bool,
    
    /// Use regular expressions
    #[arg(short = 'e', long = "regex", help = "Treat pattern as a regular expression")]
    pub regex: bool,
    
    /// Show line numbers
    #[arg(short = 'n', long, help = "Show line numbers with matches")]
    pub line_numbers: bool,
    
    /// Lines of context before each match
    #[arg(
        short = 'B', 
        long, 
        default_value = "2",
        help = "Number of lines to show before each match"
    )]
    pub before_context: usize,
    
    /// Lines of context after each match
    #[arg(
        short = 'A', 
        long, 
        default_value = "2",
        help = "Number of lines to show after each match"
    )]
    pub after_context: usize,
    
    /// Show AST context
    #[arg(
        short = 'a', 
        long = "ast-context", 
        help = "Show AST context (functions, classes, etc.) around matches"
    )]
    pub ast_context: bool,
    
    /// Maximum AST depth to show
    #[arg(
        long, 
        default_value = "3",
        help = "Maximum depth of AST context to display"
    )]
    pub max_ast_depth: usize,
    
    /// Output format
    #[arg(
        short = 'o', 
        long, 
        value_enum, 
        default_value = "text",
        help = "Output format"
    )]
    pub output_format: OutputFormat,
    
    /// Force color output
    #[arg(
        long, 
        conflicts_with = "no_color",
        help = "Force colored output even when not to a terminal"
    )]
    pub color: Option<bool>,
    
    /// Disable color output
    #[arg(long, help = "Never use colored output")]
    pub no_color: bool,
    
    /// Number of threads to use
    #[arg(
        short = 'j', 
        long, 
        help = "Number of search threads (default: number of CPU cores)"
    )]
    pub threads: Option<usize>,
    
    /// Maximum search depth
    #[arg(long, help = "Maximum directory traversal depth")]
    pub max_depth: Option<usize>,
    
    /// Maximum file size to search (in bytes)
    #[arg(long, help = "Skip files larger than this size")]
    pub max_file_size: Option<u64>,
    
    /// Search hidden files and directories
    #[arg(short = 'H', long, help = "Search hidden files and directories")]
    pub hidden: bool,
    
    /// Don't respect .gitignore
    #[arg(long, help = "Don't respect .gitignore files")]
    pub no_ignore: bool,
    
    /// Follow symbolic links
    #[arg(short = 'L', long, help = "Follow symbolic links")]
    pub follow_symlinks: bool,
    
    /// Filter by language
    #[arg(
        short = 'l', 
        long = "lang", 
        help = "Only search files of specified language(s)"
    )]
    pub languages: Vec<String>,
    
    /// Exclude patterns
    #[arg(
        short = 'x', 
        long = "exclude", 
        help = "Exclude files/directories matching pattern"
    )]
    pub exclude: Vec<String>,
    
    /// Include patterns
    #[arg(long = "include", help = "Only search files matching pattern")]
    pub include: Vec<String>,
    
    /// Interactive mode
    #[arg(short = 'i', long, help = "Start in interactive mode")]
    pub interactive: bool,
    
    /// Count only (don't show matches)
    #[arg(short = 'c', long, help = "Only show count of matching lines")]
    pub count: bool,
    
    /// Files with matches (don't show matches)
    #[arg(short = 'l', long = "files-with-matches", help = "Only show files with matches")]
    pub files_with_matches: bool,
    
    /// Enable AI-powered insights
    #[arg(long, help = "Enable AI-powered insights and analysis")]
    pub ai_insights: bool,
    
    /// Enable AI code explanation
    #[arg(long, help = "Enable AI code explanation for matches")]
    pub ai_explain: bool,
    
    /// AI model to use
    #[arg(
        long, 
        default_value = "gpt-4o-mini",
        help = "AI model to use for insights"
    )]
    pub ai_model: String,
    
    /// List supported languages
    #[arg(long, help = "List all supported programming languages")]
    pub list_languages: bool,
    
    /// Show statistics
    #[arg(long, help = "Show search statistics")]
    pub stats: bool,
    
    /// Dry run (don't actually search)
    #[arg(long, help = "Show what would be searched without searching")]
    pub dry_run: bool,
    
    /// Config file path
    #[arg(long, help = "Path to configuration file")]
    pub config: Option<PathBuf>,
    
    /// Verbose output
    #[arg(
        short = 'v', 
        long, 
        action = clap::ArgAction::Count,
        help = "Verbose output (-v, -vv, -vvv for increasing verbosity)"
    )]
    pub verbose: u8,
    
    /// Print version
    #[arg(long, help = "Print version information")]
    pub version: bool,
}

/// Output format options
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum OutputFormat {
    /// Plain text output with colors
    Text,
    /// JSON output for machine processing
    Json,
    /// HTML output for web display
    Html,
    /// XML output
    Xml,
    /// CSV output for spreadsheets
    Csv,
}

impl Cli {
    /// Validate CLI arguments
    pub fn validate(&self) -> anyhow::Result<()> {
        if self.pattern.is_empty() && !self.interactive && !self.list_languages && !self.version {
            anyhow::bail!("No search pattern provided (use -i for interactive mode)");
        }
        
        if self.before_context > 1000 || self.after_context > 1000 {
            anyhow::bail!("Context lines must be <= 1000");
        }
        
        if self.max_ast_depth > 20 {
            anyhow::bail!("AST depth must be <= 20");
        }
        
        if let Some(threads) = self.threads {
            if threads == 0 || threads > 256 {
                anyhow::bail!("Thread count must be between 1 and 256");
            }
        }
        
        if let Some(max_file_size) = self.max_file_size {
            if max_file_size > 1024 * 1024 * 1024 * 10 {  // 10GB
                anyhow::bail!("Maximum file size cannot exceed 10GB");
            }
        }
        
        if self.ai_insights || self.ai_explain {
            if std::env::var("OPENAI_API_KEY").is_err() {
                anyhow::bail!("OPENAI_API_KEY environment variable required for AI features");
            }
        }
        
        Ok(())
    }
    
    /// Get computed color setting
    pub fn should_use_color(&self) -> bool {
        if self.no_color {
            false
        } else if let Some(color) = self.color {
            color
        } else {
            atty::is(atty::Stream::Stdout)
        }
    }
    
    /// Check if any AI features are enabled
    pub fn has_ai_features(&self) -> bool {
        self.ai_insights || self.ai_explain
    }
    
    /// Get effective thread count
    pub fn effective_threads(&self) -> usize {
        self.threads.unwrap_or_else(num_cpus::get)
    }
}

/// Generate shell completions
pub fn generate_completions(shell: clap_complete::Shell) -> String {
    use clap::CommandFactory;
    use clap_complete::generate;
    
    let mut cmd = Cli::command();
    let mut buf = Vec::new();
    generate(shell, &mut cmd, "opengrep", &mut buf);
    String::from_utf8(buf).expect("Failed to convert completion to string")
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_cli_parsing() {
        let cli = Cli::parse_from(&["opengrep", "test", "src/"]);
        assert_eq!(cli.pattern, "test");
        assert_eq!(cli.paths, vec![PathBuf::from("src/")]);
        assert!(!cli.ignore_case);
        assert!(!cli.regex);
    }
    
    #[test]
    fn test_cli_validation() {
        let mut cli = Cli::parse_from(&["opengrep", "test"]);
        assert!(cli.validate().is_ok());
        
        cli.pattern = String::new();
        assert!(cli.validate().is_err());
        
        cli.pattern = "test".to_string();
        cli.before_context = 2000;
        assert!(cli.validate().is_err());
    }
    
    #[test]
    fn test_color_setting() {
        let cli = Cli::parse_from(&["opengrep", "test"]);
        assert!(cli.should_use_color() || !atty::is(atty::Stream::Stdout));
        
        let cli = Cli::parse_from(&["opengrep", "test", "--no-color"]);
        assert!(!cli.should_use_color());
    }
    
    #[test]
    fn test_ai_features() {
        let cli = Cli::parse_from(&["opengrep", "test"]);
        assert!(!cli.has_ai_features());
        
        let cli = Cli::parse_from(&["opengrep", "test", "--ai-insights"]);
        assert!(cli.has_ai_features());
    }
    
    #[test]
    fn test_effective_threads() {
        let cli = Cli::parse_from(&["opengrep", "test"]);
        assert_eq!(cli.effective_threads(), num_cpus::get());
        
        let cli = Cli::parse_from(&["opengrep", "test", "-j", "4"]);
        assert_eq!(cli.effective_threads(), 4);
    }
}