use super::{
ComprehensiveToolOutput, OperationContext, Change, ChangeKind,
ComprehensiveSemanticImpact, ContextSnapshot, OperationScope,
PerformanceMetrics, OperationMetadata, ScopeType
};
use super::patch::SourceLocation;
use ast_grep_core::{AstGrep, NodeMatch};
use ast_grep_config::{RuleConfig, from_yaml_string};
use ast_grep_language::SupportLang as SgLang;
use dashmap::DashMap;
use tree_sitter_rust;
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, Instant};
use thiserror::Error;
use tracing::{debug, warn, error};
use uuid::Uuid;
#[derive(Debug, Error)]
pub enum GrepError {
#[error("invalid pattern: {pattern} - {reason}")]
InvalidPattern { pattern: String, reason: String },
#[error("unsupported language: {language}")]
UnsupportedLanguage { language: String },
#[error("query compilation failed: {query} - {reason}")]
QueryCompilationFailed { query: String, reason: String },
#[error("YAML rule parsing failed: {rule} - {reason}")]
YamlRuleFailed { rule: String, reason: String },
#[error("search timeout after {duration:?}")]
SearchTimeout { duration: Duration },
#[error("file access error: {path} - {reason}")]
FileAccess { path: PathBuf, reason: String },
#[error("parse error for {path}: {reason}")]
ParseError { path: PathBuf, reason: String },
#[error("pattern cache overflow: {current_size} >= {max_size}")]
CacheOverflow { current_size: usize, max_size: usize },
#[error("performance threshold exceeded: {actual_ms}ms > {threshold_ms}ms")]
PerformanceThreshold { actual_ms: u64, threshold_ms: u64 },
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("AST-grep language error: {0}")]
AstGrepLanguage(String),
}
pub type GrepResult<T> = std::result::Result<T, GrepError>;
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct PatternKey {
pattern: String,
language: SupportedLanguage,
rule_type: RuleType,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum RuleType {
Pattern,
Query,
YamlRule,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum SupportedLanguage {
Rust,
Python,
JavaScript,
TypeScript,
Go,
Java,
C,
Cpp,
CSharp,
Html,
Css,
Json,
Yaml,
Bash,
Php,
Ruby,
Swift,
Kotlin,
}
impl SupportedLanguage {
pub fn as_str(&self) -> &'static str {
match self {
SupportedLanguage::Rust => "rust",
SupportedLanguage::Python => "python",
SupportedLanguage::JavaScript => "javascript",
SupportedLanguage::TypeScript => "typescript",
SupportedLanguage::Go => "go",
SupportedLanguage::Java => "java",
SupportedLanguage::C => "c",
SupportedLanguage::Cpp => "cpp",
SupportedLanguage::CSharp => "c_sharp",
SupportedLanguage::Html => "html",
SupportedLanguage::Css => "css",
SupportedLanguage::Json => "json",
SupportedLanguage::Yaml => "yaml",
SupportedLanguage::Bash => "bash",
SupportedLanguage::Php => "php",
SupportedLanguage::Ruby => "ruby",
SupportedLanguage::Swift => "swift",
SupportedLanguage::Kotlin => "kotlin",
}
}
pub fn to_ast_grep_language_str(&self) -> &'static str {
match self {
SupportedLanguage::Rust => "rust",
SupportedLanguage::Python => "python",
SupportedLanguage::JavaScript => "javascript",
SupportedLanguage::TypeScript => "typescript",
SupportedLanguage::Go => "go",
SupportedLanguage::Java => "java",
SupportedLanguage::C => "c",
SupportedLanguage::Cpp => "cpp",
SupportedLanguage::CSharp => "c_sharp",
SupportedLanguage::Html => "html",
SupportedLanguage::Css => "css",
SupportedLanguage::Json => "javascript",
SupportedLanguage::Yaml => "yaml",
SupportedLanguage::Bash => "bash",
SupportedLanguage::Php => "php",
SupportedLanguage::Ruby => "ruby",
SupportedLanguage::Swift => "swift",
SupportedLanguage::Kotlin => "kotlin",
}
}
pub fn from_path(path: &Path) -> Option<Self> {
let extension = path.extension()?.to_str()?;
match extension.to_lowercase().as_str() {
"rs" => Some(SupportedLanguage::Rust),
"py" | "pyw" | "pyi" => Some(SupportedLanguage::Python),
"js" | "mjs" | "cjs" => Some(SupportedLanguage::JavaScript),
"ts" | "tsx" | "cts" | "mts" => Some(SupportedLanguage::TypeScript),
"go" => Some(SupportedLanguage::Go),
"java" => Some(SupportedLanguage::Java),
"c" | "h" => Some(SupportedLanguage::C),
"cpp" | "cxx" | "cc" | "hpp" | "hxx" | "hh" => Some(SupportedLanguage::Cpp),
"cs" => Some(SupportedLanguage::CSharp),
"html" | "htm" => Some(SupportedLanguage::Html),
"css" => Some(SupportedLanguage::Css),
"json" => Some(SupportedLanguage::Json),
"yaml" | "yml" => Some(SupportedLanguage::Yaml),
"sh" | "bash" | "zsh" => Some(SupportedLanguage::Bash),
"php" => Some(SupportedLanguage::Php),
"rb" => Some(SupportedLanguage::Ruby),
"swift" => Some(SupportedLanguage::Swift),
"kt" | "kts" => Some(SupportedLanguage::Kotlin),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct CompiledPattern {
pub original: String,
pub language: SupportedLanguage,
pub rule_type: RuleType,
pub rule_config: Option<RuleConfig<SgLang>>,
pub complexity: u8,
pub has_metavars: bool,
pub metavar_names: Vec<String>,
pub compiled_at: Instant,
pub performance_hint: PerformanceHint,
}
#[derive(Debug, Clone, PartialEq)]
pub enum PerformanceHint {
Literal,
Structural,
Complex,
YamlRule,
}
#[derive(Debug)]
pub struct LanguageDetector {
extension_cache: Arc<DashMap<String, SupportedLanguage>>,
}
impl Default for LanguageDetector {
fn default() -> Self {
Self::new()
}
}
impl LanguageDetector {
pub fn new() -> Self {
Self {
extension_cache: Arc::new(DashMap::new()),
}
}
pub fn detect(&self, path: &Path) -> Option<SupportedLanguage> {
let extension = path.extension()?.to_str()?.to_lowercase();
if let Some(cached) = self.extension_cache.get(&extension) {
return Some(*cached);
}
let detected = SupportedLanguage::from_path(path);
if let Some(lang) = detected {
self.extension_cache.insert(extension, lang);
}
detected
}
}
#[derive(Debug)]
pub struct QueryOptimizer {
analysis_cache: Arc<DashMap<String, PatternAnalysis>>,
}
#[derive(Debug, Clone)]
pub struct PatternAnalysis {
pub complexity: u8,
pub has_metavars: bool,
pub metavar_names: Vec<String>,
pub performance_hint: PerformanceHint,
pub estimated_selectivity: f32, }
impl Default for QueryOptimizer {
fn default() -> Self {
Self::new()
}
}
impl QueryOptimizer {
pub fn new() -> Self {
Self {
analysis_cache: Arc::new(DashMap::new()),
}
}
pub fn analyze(&self, pattern: &str) -> PatternAnalysis {
if let Some(cached) = self.analysis_cache.get(pattern) {
return cached.clone();
}
let mut complexity = 0u8;
let mut metavar_names = Vec::new();
let mut has_metavars = false;
if pattern.contains('(') && pattern.contains(')') {
complexity += 3; }
if pattern.contains('@') {
complexity += 2; }
if pattern.contains("where") || pattern.contains("has") {
complexity += 4; }
for part in pattern.split_whitespace() {
if part.starts_with('$') {
has_metavars = true;
let metavar = if part.starts_with("$$") {
&part[2..]
} else {
&part[1..]
};
if !metavar.is_empty() {
metavar_names.push(metavar.to_string());
complexity += 1;
}
}
}
let performance_hint = if pattern.chars().all(|c| c.is_alphanumeric() || c == '_') {
PerformanceHint::Literal
} else if complexity <= 3 {
PerformanceHint::Structural
} else {
PerformanceHint::Complex
};
let estimated_selectivity = if performance_hint == PerformanceHint::Literal {
0.1 } else if has_metavars {
0.6 } else {
0.3 };
let analysis = PatternAnalysis {
complexity: complexity.min(10),
has_metavars,
metavar_names,
performance_hint,
estimated_selectivity,
};
self.analysis_cache.insert(pattern.to_string(), analysis.clone());
analysis
}
}
#[derive(Debug)]
pub struct SemanticRanker {
scoring_config: ScoringConfig,
}
#[derive(Debug, Clone)]
pub struct ScoringConfig {
pub exact_match_weight: f32,
pub definition_weight: f32,
pub visibility_weight: f32,
pub path_length_weight: f32,
pub context_weight: f32,
}
impl Default for ScoringConfig {
fn default() -> Self {
Self {
exact_match_weight: 0.4,
definition_weight: 0.25,
visibility_weight: 0.15,
path_length_weight: 0.1,
context_weight: 0.1,
}
}
}
impl SemanticRanker {
pub fn new() -> Self {
Self::with_config(ScoringConfig::default())
}
pub fn with_config(config: ScoringConfig) -> Self {
Self {
scoring_config: config,
}
}
pub fn score_match(&self, grep_match: &GrepMatch, query: &GrepQuery) -> f32 {
let mut score = 0.0;
if grep_match.content.contains(&query.pattern) {
score += self.scoring_config.exact_match_weight;
}
if grep_match.semantic_context.is_definition {
score += self.scoring_config.definition_weight;
}
match &grep_match.semantic_context.access_level {
Some(AccessLevel::Public) => score += self.scoring_config.visibility_weight,
Some(AccessLevel::Protected) => score += self.scoring_config.visibility_weight * 0.5,
_ => {}
}
let path_components = grep_match.file.components().count();
let path_score = (10.0 - path_components.min(10) as f32) / 10.0;
score += self.scoring_config.path_length_weight * path_score;
if !grep_match.surrounding_context.containing_function.is_none() {
score += self.scoring_config.context_weight;
}
score.min(1.0)
}
}
impl Default for SemanticRanker {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct AstGrepEngine {
pattern_cache: Arc<DashMap<PatternKey, CompiledPattern>>,
language_detector: LanguageDetector,
query_optimizer: QueryOptimizer,
result_ranker: SemanticRanker,
config: GrepConfig,
}
#[derive(Debug, Clone)]
pub struct GrepConfig {
pub max_pattern_cache_size: usize,
pub pattern_cache_ttl: Duration,
pub search_timeout: Duration,
pub parallel_processing: bool,
pub max_results: usize,
pub context_lines: usize,
pub performance_threshold_ms: u64,
}
impl Default for GrepConfig {
fn default() -> Self {
Self {
max_pattern_cache_size: 1000,
pattern_cache_ttl: Duration::from_secs(3600), search_timeout: Duration::from_secs(30),
parallel_processing: true,
max_results: 1000,
context_lines: 3,
performance_threshold_ms: 10, }
}
}
#[derive(Debug, Clone)]
pub struct GrepMatch {
pub file: PathBuf,
pub line: usize,
pub column: usize,
pub byte_offset: usize,
pub content: String,
pub confidence: f32,
pub metavar_bindings: HashMap<String, String>,
pub semantic_context: SemanticContext,
pub surrounding_context: SurroundingContext,
pub suggested_fixes: Vec<SuggestedFix>,
}
#[derive(Debug, Clone)]
pub struct SemanticContext {
pub role: SemanticRole,
pub containing_scope: Option<String>,
pub symbol_name: Option<String>,
pub is_definition: bool,
pub access_level: Option<AccessLevel>,
pub node_type: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum SemanticRole {
Declaration,
Call,
Reference,
TypeAnnotation,
Import,
Export,
ControlFlow,
Comment,
Other,
}
#[derive(Debug, Clone, PartialEq)]
pub enum AccessLevel {
Public,
Private,
Protected,
Internal,
Package,
}
#[derive(Debug, Clone)]
pub struct SurroundingContext {
pub before_lines: Vec<ContextLine>,
pub after_lines: Vec<ContextLine>,
pub containing_function: Option<String>,
pub containing_class: Option<String>,
pub containing_module: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ContextLine {
pub number: usize,
pub content: String,
pub is_match: bool,
pub indent_level: usize,
}
#[derive(Debug, Clone)]
pub struct SuggestedFix {
pub description: String,
pub replacement: String,
pub confidence: f32,
pub auto_applicable: bool,
}
#[derive(Debug, Clone)]
pub struct GrepQuery {
pub pattern: String,
pub paths: Vec<PathBuf>,
pub language: Option<SupportedLanguage>,
pub rule_type: RuleType,
pub with_context: bool,
pub limit: Option<usize>,
pub case_sensitive: bool,
}
impl Default for GrepQuery {
fn default() -> Self {
Self {
pattern: String::new(),
paths: Vec::new(),
language: None,
rule_type: RuleType::Pattern,
with_context: true,
limit: Some(100),
case_sensitive: true,
}
}
}
#[derive(Debug)]
pub struct GrepTool {
engine: AstGrepEngine,
}
impl AstGrepEngine {
fn to_sg_lang(&self, language: SupportedLanguage) -> SgLang {
match language {
SupportedLanguage::Rust => SgLang::Rust,
SupportedLanguage::Python => SgLang::Python,
SupportedLanguage::JavaScript => SgLang::JavaScript,
SupportedLanguage::TypeScript => SgLang::TypeScript,
SupportedLanguage::Go => SgLang::Go,
SupportedLanguage::Java => SgLang::Java,
SupportedLanguage::C => SgLang::C,
SupportedLanguage::Cpp => SgLang::Cpp,
SupportedLanguage::CSharp => SgLang::CSharp,
SupportedLanguage::Html => SgLang::Html,
SupportedLanguage::Css => SgLang::Css,
SupportedLanguage::Json => SgLang::Json,
SupportedLanguage::Yaml => SgLang::Yaml,
SupportedLanguage::Bash => SgLang::Bash,
SupportedLanguage::Ruby => SgLang::Ruby,
SupportedLanguage::Php => SgLang::Php,
SupportedLanguage::Swift => SgLang::Swift,
SupportedLanguage::Kotlin => SgLang::Kotlin,
SupportedLanguage::Toml => SgLang::Json, SupportedLanguage::Haskell => SgLang::Haskell,
SupportedLanguage::Elixir => SgLang::Elixir,
SupportedLanguage::Sql => SgLang::Ruby, SupportedLanguage::Dockerfile => SgLang::Bash, SupportedLanguage::Markdown => SgLang::Html, }
}
pub fn new(config: GrepConfig) -> Self {
Self {
pattern_cache: Arc::new(DashMap::new()),
language_detector: LanguageDetector::new(),
query_optimizer: QueryOptimizer::new(),
result_ranker: SemanticRanker::new(),
config,
}
}
pub fn compile_pattern(&self, pattern: &str, language: SupportedLanguage, rule_type: RuleType) -> GrepResult<CompiledPattern> {
let key = PatternKey {
pattern: pattern.to_string(),
language,
rule_type: rule_type.clone(),
};
if let Some(cached) = self.pattern_cache.get(&key) {
if cached.compiled_at.elapsed() < self.config.pattern_cache_ttl {
return Ok(cached.clone());
}
self.pattern_cache.remove(&key);
}
if self.pattern_cache.len() >= self.config.max_pattern_cache_size {
return Err(GrepError::CacheOverflow {
current_size: self.pattern_cache.len(),
max_size: self.config.max_pattern_cache_size,
});
}
let start = Instant::now();
let analysis = self.query_optimizer.analyze(pattern);
let rule_config = if rule_type == RuleType::YamlRule {
Some(self.compile_yaml_rule(pattern, language)?)
} else {
None
};
let compiled = CompiledPattern {
original: pattern.to_string(),
language,
rule_type,
rule_config,
complexity: analysis.complexity,
has_metavars: analysis.has_metavars,
metavar_names: analysis.metavar_names,
compiled_at: start,
performance_hint: analysis.performance_hint,
};
self.pattern_cache.insert(key, compiled.clone());
debug!("Compiled pattern '{}' for {:?} in {:?}",
pattern, language, start.elapsed());
Ok(compiled)
}
fn compile_yaml_rule(&self, yaml_content: &str, language: SupportedLanguage) -> GrepResult<String> {
Ok(yaml_content.to_string())
}
pub fn search_with_pattern(&self, pattern: &CompiledPattern, query: &GrepQuery) -> GrepResult<Vec<GrepMatch>> {
let start = Instant::now();
if pattern.complexity > 7 {
let threshold = Duration::from_millis(self.config.performance_threshold_ms);
if start.elapsed() > threshold {
return Err(GrepError::PerformanceThreshold {
actual_ms: start.elapsed().as_millis() as u64,
threshold_ms: self.config.performance_threshold_ms,
});
}
}
let use_parallel = self.config.parallel_processing && query.paths.len() > 1;
let matches = if use_parallel {
self.search_parallel(pattern, query)?
} else {
self.search_sequential(pattern, query)?
};
let search_duration = start.elapsed();
debug!("Search completed in {:?} with {} matches", search_duration, matches.len());
Ok(matches)
}
fn search_parallel(&self, pattern: &CompiledPattern, query: &GrepQuery) -> GrepResult<Vec<GrepMatch>> {
let matches: Result<Vec<Vec<GrepMatch>>, GrepError> = query.paths
.par_iter()
.map(|path| self.search_single_file(pattern, path, query))
.collect();
let mut all_matches: Vec<GrepMatch> = matches?.into_iter().flatten().collect();
for grep_match in &mut all_matches {
grep_match.confidence = self.result_ranker.score_match(grep_match, query);
}
all_matches.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal));
if let Some(limit) = query.limit {
all_matches.truncate(limit);
}
Ok(all_matches)
}
fn search_sequential(&self, pattern: &CompiledPattern, query: &GrepQuery) -> GrepResult<Vec<GrepMatch>> {
let mut all_matches = Vec::new();
for path in &query.paths {
let mut matches = self.search_single_file(pattern, path, query)?;
for grep_match in &mut matches {
grep_match.confidence = self.result_ranker.score_match(grep_match, query);
}
all_matches.extend(matches);
if let Some(limit) = query.limit {
if all_matches.len() >= limit {
break;
}
}
}
all_matches.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal));
if let Some(limit) = query.limit {
all_matches.truncate(limit);
}
Ok(all_matches)
}
fn search_single_file(&self, pattern: &CompiledPattern, path: &Path, query: &GrepQuery) -> GrepResult<Vec<GrepMatch>> {
let content = std::fs::read_to_string(path)
.map_err(|e| GrepError::FileAccess {
path: path.to_path_buf(),
reason: e.to_string(),
})?;
let language = pattern.language;
let lang_str = language.to_ast_grep_language_str();
let sg_lang = self.to_sg_lang(language);
let ast_grep = sg_lang.ast_grep(&content);
let mut matches = Vec::new();
match &pattern.rule_type {
RuleType::Pattern | RuleType::Query => {
let searcher = ast_grep.find(&pattern.original);
for node_match in searcher {
let grep_match = self.create_grep_match(node_match, path, &content, pattern, query)?;
matches.push(grep_match);
}
}
RuleType::YamlRule => {
if let Some(rule_config) = &pattern.rule_config {
let rule_matches = self.apply_yaml_rule(rule_config, &ast_grep, path, &content, pattern, query)?;
matches.extend(rule_matches);
}
}
}
Ok(matches)
}
fn get_tree_sitter_language(&self, language: SupportedLanguage, _path: &Path) -> GrepResult<tree_sitter::Language> {
match language {
SupportedLanguage::Rust => Ok(tree_sitter_rust::LANGUAGE.into()),
_ => {
warn!("Language {:?} not fully supported in ast-grep, using Rust parser as fallback", language);
Ok(tree_sitter_rust::LANGUAGE.into())
}
}
}
fn apply_yaml_rule(
&self,
rule_config: &str,
ast_grep: &AstGrep<SgLang>,
path: &Path,
content: &str,
pattern: &CompiledPattern,
query: &GrepQuery,
) -> GrepResult<Vec<GrepMatch>> {
let mut matches = Vec::new();
let search_pattern = if let Some(rule_pattern) = &rule_config.rule.pattern {
rule_pattern.as_str()
} else {
&pattern.original
};
let searcher = ast_grep.find(search_pattern);
for node_match in searcher {
let mut grep_match = self.create_grep_match(node_match, path, content, pattern, query)?;
if let Some(message) = &rule_config.message {
grep_match.suggested_fixes.push(SuggestedFix {
description: message.clone(),
replacement: String::new(), confidence: 0.8,
auto_applicable: false,
});
}
if self.matches_rule_constraints(rule_config, &grep_match) {
matches.push(grep_match);
}
}
Ok(matches)
}
fn matches_rule_constraints(&self, rule_config: &str, grep_match: &GrepMatch) -> bool {
true
}
fn create_grep_match(
&self,
node_match: NodeMatch<SgLang>,
path: &Path,
content: &str,
_pattern: &CompiledPattern,
query: &GrepQuery,
) -> GrepResult<GrepMatch> {
let node = node_match.get_node();
let text = node_match.text();
let range = node.range();
let line = range.start.row + 1;
let column = range.start.column + 1;
let byte_offset = range.start_byte;
let metavar_bindings = node_match.get_env().get_bindings()
.iter()
.map(|(k, v)| (k.clone(), v.text().to_string()))
.collect();
let semantic_context = self.build_semantic_context(&node, path)?;
let surrounding_context = if query.with_context {
self.build_surrounding_context(content, line, self.config.context_lines)?
} else {
SurroundingContext {
before_lines: Vec::new(),
after_lines: Vec::new(),
containing_function: None,
containing_class: None,
containing_module: None,
}
};
Ok(GrepMatch {
file: path.to_path_buf(),
line,
column,
byte_offset,
content: text.to_string(),
confidence: 0.5, metavar_bindings,
semantic_context,
surrounding_context,
suggested_fixes: Vec::new(), })
}
fn build_semantic_context(&self, node: &ast_grep_core::Node<SgLang>, _path: &Path) -> GrepResult<SemanticContext> {
let node_type = node.kind().to_string();
let role = match node_type.as_str() {
"function_declaration" | "function_item" | "method_definition" => SemanticRole::Declaration,
"call_expression" | "function_call" => SemanticRole::Call,
"identifier" | "variable" => SemanticRole::Reference,
"type_annotation" | "type_identifier" => SemanticRole::TypeAnnotation,
"import_statement" | "use_declaration" => SemanticRole::Import,
"export_statement" => SemanticRole::Export,
"if_statement" | "while_statement" | "for_statement" => SemanticRole::ControlFlow,
"comment" | "line_comment" | "block_comment" => SemanticRole::Comment,
_ => SemanticRole::Other,
};
let symbol_name = if node_type == "identifier" {
Some(node.text().to_string())
} else {
None
};
let is_definition = matches!(role, SemanticRole::Declaration | SemanticRole::TypeAnnotation);
Ok(SemanticContext {
role,
containing_scope: None, symbol_name,
is_definition,
access_level: None, node_type,
})
}
fn build_surrounding_context(&self, content: &str, match_line: usize, context_lines: usize) -> GrepResult<SurroundingContext> {
let lines: Vec<&str> = content.lines().collect();
let total_lines = lines.len();
let start_line = match_line.saturating_sub(context_lines + 1);
let end_line = (match_line + context_lines).min(total_lines);
let mut before_lines = Vec::new();
let mut after_lines = Vec::new();
for i in start_line..match_line.saturating_sub(1) {
if i < total_lines {
before_lines.push(ContextLine {
number: i + 1,
content: lines[i].to_string(),
is_match: false,
indent_level: Self::calculate_indent_level(lines[i]),
});
}
}
for i in match_line..end_line {
if i < total_lines {
after_lines.push(ContextLine {
number: i + 1,
content: lines[i].to_string(),
is_match: i + 1 == match_line,
indent_level: Self::calculate_indent_level(lines[i]),
});
}
}
Ok(SurroundingContext {
before_lines,
after_lines,
containing_function: None, containing_class: None,
containing_module: None,
})
}
fn calculate_indent_level(line: &str) -> usize {
let mut level = 0;
for ch in line.chars() {
match ch {
' ' => level += 1,
'\t' => level += 4, _ => break,
}
}
level
}
}
impl GrepTool {
pub fn new() -> Self {
Self::with_config(GrepConfig::default())
}
pub fn with_config(config: GrepConfig) -> Self {
Self {
engine: AstGrepEngine::new(config),
}
}
#[allow(dead_code)] pub fn with_ast_integration(config: GrepConfig) -> Self {
Self {
engine: AstGrepEngine::new(config),
}
}
pub fn grep(&self, pattern: &str, paths: Vec<PathBuf>) -> GrepResult<ComprehensiveToolOutput<Vec<GrepMatch>>> {
let query = GrepQuery {
pattern: pattern.to_string(),
paths: paths.clone(),
rule_type: RuleType::Pattern,
..Default::default()
};
self.search_with_query(query)
}
pub fn grep_query(&self, query_str: &str, paths: Vec<PathBuf>) -> GrepResult<ComprehensiveToolOutput<Vec<GrepMatch>>> {
let query = GrepQuery {
pattern: query_str.to_string(),
paths: paths.clone(),
rule_type: RuleType::Query,
..Default::default()
};
self.search_with_query(query)
}
pub fn grep_rule(&self, yaml_rule: &str, paths: Vec<PathBuf>) -> GrepResult<ComprehensiveToolOutput<Vec<GrepMatch>>> {
let query = GrepQuery {
pattern: yaml_rule.to_string(),
paths: paths.clone(),
rule_type: RuleType::YamlRule,
..Default::default()
};
self.search_with_query(query)
}
pub fn search_with_query(&self, query: GrepQuery) -> GrepResult<ComprehensiveToolOutput<Vec<GrepMatch>>> {
let start = Instant::now();
let language = if let Some(lang) = query.language {
lang
} else if let Some(path) = query.paths.first() {
self.engine.language_detector.detect(path)
.unwrap_or(SupportedLanguage::Rust) } else {
SupportedLanguage::Rust };
let compiled_pattern = self.engine.compile_pattern(&query.pattern, language, query.rule_type.clone())?;
let matches = self.engine.search_with_pattern(&compiled_pattern, &query)?;
let duration = start.elapsed();
let first_path = query.paths.first().cloned().unwrap_or_else(|| PathBuf::from("unknown"));
let context = OperationContext {
before: ContextSnapshot {
content: format!("Searching for pattern: {}", query.pattern),
timestamp: std::time::SystemTime::now(),
content_hash: format!("{:x}", md5::compute(&query.pattern)),
ast_summary: None,
symbols: Vec::new(),
},
after: None,
surrounding: Vec::new(),
location: SourceLocation {
file_path: first_path.to_string_lossy().to_string(),
line_start: 0,
column_start: 0,
line_end: 0,
column_end: 0,
byte_range: (0, 0),
},
scope: OperationScope {
scope_type: super::ScopeType::File,
name: "search".to_string(),
path: vec!["grep".to_string()],
file_path: first_path.clone(),
line_range: 0..0,
},
language_context: None,
project_context: None,
};
let changes = matches.iter().map(|m| {
Change {
id: uuid::Uuid::new_v4(),
kind: ChangeKind::Added {
reason: format!("Pattern match found for '{}'", query.pattern),
insertion_point: SourceLocation {
file_path: m.file.to_string_lossy().to_string(),
line_start: m.line,
column_start: m.column,
line_end: m.line,
column_end: m.column + m.matched_text.len(),
byte_range: (m.byte_offset, m.byte_offset + m.matched_text.len()),
},
},
old: None,
new: Some(m.matched_text.clone()),
line_range: m.line..m.line + 1,
char_range: m.column..m.column + m.matched_text.len(),
location: SourceLocation {
file_path: m.file.to_string_lossy().to_string(),
line_start: m.line,
column_start: m.column,
line_end: m.line,
column_end: m.column + m.matched_text.len(),
byte_range: (m.byte_offset, m.byte_offset + m.matched_text.len()),
},
semantic_impact: ComprehensiveSemanticImpact::minimal(),
affected_symbols: Vec::new(),
confidence: m.confidence,
description: format!("Found pattern '{}' in {} at line {}",
query.pattern, m.file.display(), m.line),
}
}).collect();
let summary = format!(
"Found {} matches for '{}' across {} files using {} in {:?}",
matches.len(),
query.pattern,
query.paths.len(),
match query.rule_type {
RuleType::Pattern => "pattern matching",
RuleType::Query => "query matching",
RuleType::YamlRule => "YAML rule",
},
duration
);
Ok(ComprehensiveToolOutput {
result: matches,
context,
changes,
metadata: OperationMetadata {
tool: "grep",
operation: "search".to_string(),
operation_id: uuid::Uuid::new_v4(),
started_at: std::time::SystemTime::now() - duration,
completed_at: std::time::SystemTime::now(),
confidence: 1.0,
parameters: [
("pattern".to_string(), query.pattern.clone()),
("language".to_string(), language.as_str().to_string()),
("rule_type".to_string(), format!("{:?}", query.rule_type)),
].iter().cloned().collect(),
},
summary,
performance: PerformanceMetrics {
execution_time: duration,
phase_times: std::collections::HashMap::new(),
},
diagnostics: Vec::new(),
})
}
pub fn clear_cache(&self) {
self.engine.pattern_cache.clear();
debug!("Cleared pattern cache");
}
pub fn cache_stats(&self) -> (usize, usize) {
(self.engine.pattern_cache.len(), self.engine.config.max_pattern_cache_size)
}
}
impl Default for GrepTool {
fn default() -> Self {
Self::new()
}
}
impl GrepQuery {
pub fn new(pattern: impl Into<String>) -> Self {
Self {
pattern: pattern.into(),
..Default::default()
}
}
pub fn paths(mut self, paths: Vec<PathBuf>) -> Self {
self.paths = paths;
self
}
pub fn language(mut self, lang: SupportedLanguage) -> Self {
self.language = Some(lang);
self
}
pub fn rule_type(mut self, rule_type: RuleType) -> Self {
self.rule_type = rule_type;
self
}
pub fn with_context(mut self, enabled: bool) -> Self {
self.with_context = enabled;
self
}
pub fn limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
pub fn case_sensitive(mut self, sensitive: bool) -> Self {
self.case_sensitive = sensitive;
self
}
}
impl fmt::Display for SemanticRole {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SemanticRole::Declaration => write!(f, "declaration"),
SemanticRole::Call => write!(f, "call"),
SemanticRole::Reference => write!(f, "reference"),
SemanticRole::TypeAnnotation => write!(f, "type"),
SemanticRole::Import => write!(f, "import"),
SemanticRole::Export => write!(f, "export"),
SemanticRole::ControlFlow => write!(f, "control_flow"),
SemanticRole::Comment => write!(f, "comment"),
SemanticRole::Other => write!(f, "other"),
}
}
}
impl fmt::Display for RuleType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RuleType::Pattern => write!(f, "pattern"),
RuleType::Query => write!(f, "query"),
RuleType::YamlRule => write!(f, "yaml_rule"),
}
}
}
impl fmt::Display for SupportedLanguage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
use std::fs::write;
#[test]
fn test_language_detection() {
let detector = LanguageDetector::new();
assert_eq!(detector.detect(Path::new("test.rs")), Some(SupportedLanguage::Rust));
assert_eq!(detector.detect(Path::new("test.py")), Some(SupportedLanguage::Python));
assert_eq!(detector.detect(Path::new("test.js")), Some(SupportedLanguage::JavaScript));
assert_eq!(detector.detect(Path::new("test.ts")), Some(SupportedLanguage::TypeScript));
}
#[test]
fn test_query_builder() {
let query = GrepQuery::new("test_function")
.paths(vec![PathBuf::from("test.rs")])
.language(SupportedLanguage::Rust)
.rule_type(RuleType::Pattern)
.with_context(true)
.limit(10)
.case_sensitive(false);
assert_eq!(query.pattern, "test_function");
assert_eq!(query.paths.len(), 1);
assert_eq!(query.language, Some(SupportedLanguage::Rust));
assert_eq!(query.rule_type, RuleType::Pattern);
assert!(query.with_context);
assert_eq!(query.limit, Some(10));
assert!(!query.case_sensitive);
}
#[test]
fn test_pattern_analysis() {
let optimizer = QueryOptimizer::new();
let analysis = optimizer.analyze("simple_function");
assert!(analysis.complexity <= 2);
assert!(!analysis.has_metavars);
assert_eq!(analysis.performance_hint, PerformanceHint::Literal);
let analysis = optimizer.analyze("function $NAME() { $BODY }");
assert!(analysis.complexity > 2);
assert!(analysis.has_metavars);
assert!(analysis.metavar_names.contains(&"NAME".to_string()));
assert!(analysis.metavar_names.contains(&"BODY".to_string()));
}
#[test]
fn test_grep_tool_creation() {
let tool = GrepTool::new();
let (cache_size, max_size) = tool.cache_stats();
assert_eq!(cache_size, 0);
assert_eq!(max_size, 1000); }
#[tokio::test]
async fn test_pattern_caching() {
let tool = GrepTool::new();
let pattern = "test_pattern";
let language = SupportedLanguage::Rust;
let compiled1 = tool.engine.compile_pattern(pattern, language, RuleType::Pattern).unwrap();
let (cache_size, _) = tool.cache_stats();
assert_eq!(cache_size, 1);
let compiled2 = tool.engine.compile_pattern(pattern, language, RuleType::Pattern).unwrap();
assert_eq!(compiled1.compiled_at, compiled2.compiled_at); }
#[test]
fn test_supported_language_conversions() {
assert_eq!(SupportedLanguage::Rust.as_str(), "rust");
assert_eq!(SupportedLanguage::Python.as_str(), "python");
assert_eq!(SupportedLanguage::JavaScript.as_str(), "javascript");
assert_eq!(SupportedLanguage::Rust.to_ast_grep_language_str(), "rust");
assert_eq!(SupportedLanguage::TypeScript.to_ast_grep_language_str(), "typescript");
}
#[test]
fn test_indent_level_calculation() {
assert_eq!(AstGrepEngine::calculate_indent_level("no indent"), 0);
assert_eq!(AstGrepEngine::calculate_indent_level(" 4 spaces"), 4);
assert_eq!(AstGrepEngine::calculate_indent_level("\t1 tab"), 4);
assert_eq!(AstGrepEngine::calculate_indent_level(" \tmixed"), 8);
}
}