use std::time::Instant;
use crate::converter::{CompositeOp, ConversionResult, QueryConverter};
use crate::schema::{
MatchResult, Query, QueryMetadata, QueryResponse, QueryStatus, ResolveConfig, ResolveKind,
ResolveStatus, Suggestion, SuggestionKind, ViewMode,
};
use ryo_analysis::{AnalysisContext, DiscoveredSymbol, DiscoveryEngine, SymbolId, SymbolKind};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ExecuteError {
#[error("conversion error: {0}")]
Conversion(#[from] crate::converter::ConvertError),
#[error("discovery error: {0}")]
Discovery(String),
#[error("post-filter error: {0}")]
PostFilter(String),
}
pub struct QueryExecutor<'a> {
ctx: &'a AnalysisContext,
view_mode: ViewMode,
}
impl<'a> QueryExecutor<'a> {
pub fn new(ctx: &'a AnalysisContext) -> Self {
Self {
ctx,
view_mode: ViewMode::default(),
}
}
pub fn with_view_mode(mut self, mode: ViewMode) -> Self {
self.view_mode = mode;
self
}
pub fn execute(&self, query: &Query) -> Result<QueryResponse, ExecuteError> {
let start = Instant::now();
let view_mode = query.view.unwrap_or(self.view_mode);
let conversion = QueryConverter::to_discovery_query(query)?;
let (results, status) = self.execute_conversion(&conversion)?;
let filter_processor = crate::filter::PostFilterProcessor::new(self.ctx);
let filtered = filter_processor.apply(results, &conversion.post_filters);
let (resolved, resolve_status) = if let Some(ref resolve_config) = query.resolve {
self.execute_resolve(&filtered, resolve_config)?
} else {
(filtered, None)
};
let match_results: Vec<MatchResult> = resolved
.iter()
.map(|s| self.to_match_result(s, view_mode))
.collect();
let (final_results, suggestions, final_status) = if match_results.is_empty() {
self.try_recovery(query, &conversion)?
} else {
(match_results, vec![], status)
};
let limited: Vec<MatchResult> = if let Some(limit) = query.limit {
final_results.into_iter().take(limit).collect()
} else {
final_results
};
let total = limited.len();
let elapsed = start.elapsed();
Ok(QueryResponse {
status: final_status,
results: limited,
suggestions,
metadata: QueryMetadata {
elapsed_ms: elapsed.as_millis() as u32,
total_matches: total,
resolve_status,
},
})
}
fn execute_conversion(
&self,
conversion: &ConversionResult,
) -> Result<(Vec<DiscoveredSymbol>, QueryStatus), ExecuteError> {
if let Some(ref composite) = conversion.composite {
let mut all_results: Vec<DiscoveredSymbol> = Vec::new();
for sub in &composite.queries {
let (results, _) = self.execute_conversion(sub)?;
match composite.op {
CompositeOp::Or => {
for r in results {
if !all_results.iter().any(|existing| existing.path == r.path) {
all_results.push(r);
}
}
}
CompositeOp::And => {
if all_results.is_empty() {
all_results = results;
} else {
all_results
.retain(|existing| results.iter().any(|r| r.path == existing.path));
}
}
}
}
let status = if all_results.is_empty() {
QueryStatus::NotFound
} else {
QueryStatus::Found
};
return Ok((all_results, status));
}
if let Some(ref dq) = conversion.discovery_query {
let engine = DiscoveryEngine::new(&self.ctx.code_graph, &self.ctx.registry, None)
.set_typeflow(&self.ctx.typeflow_graph);
let result = engine.execute(dq);
let status = if result.symbols.is_empty() {
QueryStatus::NotFound
} else {
QueryStatus::Found
};
Ok((result.symbols, status))
} else {
Ok((vec![], QueryStatus::NotFound))
}
}
fn execute_resolve(
&self,
symbols: &[DiscoveredSymbol],
config: &ResolveConfig,
) -> Result<(Vec<DiscoveredSymbol>, Option<ResolveStatus>), ExecuteError> {
let mut resolved_ids: Vec<SymbolId> = Vec::new();
let depth = config.depth.unwrap_or(1);
for symbol in symbols {
let related = self.resolve_single(symbol.id, config.kind, depth);
for id in related {
if !resolved_ids.contains(&id) {
resolved_ids.push(id);
}
}
}
let resolved_symbols: Vec<DiscoveredSymbol> = resolved_ids
.into_iter()
.filter_map(|id| self.symbol_id_to_discovered(id))
.collect();
Ok((resolved_symbols, Some(ResolveStatus::Complete)))
}
fn resolve_single(&self, id: SymbolId, kind: ResolveKind, depth: usize) -> Vec<SymbolId> {
if depth == 0 {
return vec![];
}
let direct: Vec<SymbolId> = match kind {
ResolveKind::Callers => self.ctx.code_graph.callers_of(id).collect(),
ResolveKind::Callees => self.ctx.code_graph.callees_of(id).collect(),
ResolveKind::Uses => self.ctx.typeflow_graph.types_used_by(id).collect(),
ResolveKind::UsedBy => self.ctx.typeflow_graph.type_users(id).collect(),
ResolveKind::Implementations => self.ctx.code_graph.implementors_of(id).collect(),
ResolveKind::References => {
self.ctx
.code_graph
.callers_of(id)
.chain(self.ctx.typeflow_graph.type_users(id))
.collect()
}
ResolveKind::Definition => {
vec![id]
}
};
if depth > 1 {
let mut all = direct.clone();
for child_id in &direct {
let deeper = self.resolve_single(*child_id, kind, depth - 1);
for d in deeper {
if !all.contains(&d) {
all.push(d);
}
}
}
all
} else {
direct
}
}
fn symbol_id_to_discovered(&self, id: SymbolId) -> Option<DiscoveredSymbol> {
let path = self.ctx.registry.resolve(id)?;
let kind = self.ctx.registry.kind(id).unwrap_or(SymbolKind::Other);
let span = self.ctx.registry.span(id).cloned();
let visibility = self.ctx.registry.visibility(id).cloned();
let mut symbol = DiscoveredSymbol::new(id, path.clone(), kind);
if let Some(s) = span {
symbol = symbol.with_span(s);
}
if let Some(v) = visibility {
symbol = symbol.with_visibility(v);
}
Some(symbol)
}
fn try_recovery(
&self,
query: &Query,
_conversion: &ConversionResult,
) -> Result<(Vec<MatchResult>, Vec<Suggestion>, QueryStatus), ExecuteError> {
let default_recovery = crate::schema::RecoveryStrategy {
fuzzy: Some(crate::schema::FuzzyConfig { max_distance: 2 }),
split_words: None,
enumerate_scope: Some(10),
};
let on_empty = query.r#match.as_ref().and_then(|m| m.on_empty.as_ref());
let recovery = on_empty.unwrap_or(&default_recovery);
let mut suggestions = Vec::new();
if recovery.fuzzy.is_some() {
suggestions.push(Suggestion {
kind: SuggestionKind::Typo,
name: "[未実装] fuzzy検索は現在利用できません".to_string(),
distance: None,
confidence: 0.0,
});
}
if recovery.enumerate_scope.is_some() {
suggestions.push(Suggestion {
kind: SuggestionKind::InScope,
name: "[未実装] スコープ内シンボル列挙は現在利用できません".to_string(),
distance: None,
confidence: 0.0,
});
}
let status = if suggestions.is_empty() {
QueryStatus::NotFound
} else {
QueryStatus::Partial
};
Ok((vec![], suggestions, status))
}
pub fn to_match_result(&self, symbol: &DiscoveredSymbol, mode: ViewMode) -> MatchResult {
use crate::schema::MatchView;
let symbol_id = format!("{:?}", symbol.id);
let view = match mode {
ViewMode::Snippet => {
let text = format!("// {} at {}", symbol.path.name(), symbol.path);
MatchView::Snippet { text }
}
ViewMode::Precise => MatchView::Precise,
ViewMode::Count => {
MatchView::Precise
}
ViewMode::Def => {
let (module_path, definition, doc) = self.get_def_info(symbol);
MatchView::Def {
module_path: module_path.unwrap_or_else(|| symbol.path.module_path()),
definition: definition.unwrap_or_else(|| format!("{:?}", symbol.kind)),
doc,
}
}
ViewMode::Full => {
let (module_path, definition, doc) = self.get_def_info(symbol);
let body = self
.get_full_source(symbol)
.unwrap_or_else(|| "// source not available".to_string());
MatchView::Full {
module_path: module_path.unwrap_or_else(|| symbol.path.module_path()),
definition: definition.unwrap_or_else(|| format!("{:?}", symbol.kind)),
body,
doc,
}
}
};
MatchResult {
id: symbol_id,
uuid: symbol.uuid.map(|u| u.to_string()),
path: symbol.path.to_string(),
node_kind: format!("{:?}", symbol.kind),
name: symbol.path.name().to_string(),
view,
}
}
pub fn get_def_info_for_symbol(
&self,
symbol: &DiscoveredSymbol,
) -> (Option<String>, Option<String>, Option<String>) {
self.get_def_info(symbol)
}
fn get_def_info(
&self,
symbol: &DiscoveredSymbol,
) -> (Option<String>, Option<String>, Option<String>) {
use crate::formatter::SourceFormatter;
use ryo_source::pure::PureItem;
let module_path = Some(symbol.path.module_path());
if let Some(item) = self.ctx.ast_registry.get(symbol.id) {
let fmt_or_err =
|r: Result<String, _>| r.unwrap_or_else(|e| format!("<format error: {}>", e));
let (def, doc) = match item {
PureItem::Fn(f) => (
fmt_or_err(SourceFormatter::format_fn_signature(f)),
SourceFormatter::extract_doc_and_spec(&f.attrs),
),
PureItem::Struct(s) => (
fmt_or_err(SourceFormatter::format_struct(s)),
SourceFormatter::extract_doc_and_spec(&s.attrs),
),
PureItem::Enum(e) => (
fmt_or_err(SourceFormatter::format_enum(e)),
SourceFormatter::extract_doc_and_spec(&e.attrs),
),
PureItem::Trait(t) => (
fmt_or_err(SourceFormatter::format_trait(t)),
SourceFormatter::extract_doc_and_spec(&t.attrs),
),
PureItem::Mod(_) => {
let def = self.format_module_contents(symbol);
return (module_path, Some(def), None);
}
PureItem::Type(t) => (
fmt_or_err(SourceFormatter::format_item_source(item)),
SourceFormatter::extract_doc_and_spec(&t.attrs),
),
PureItem::Const(c) => (
fmt_or_err(SourceFormatter::format_item_source(item)),
SourceFormatter::extract_doc_and_spec(&c.attrs),
),
PureItem::Static(s) => (
fmt_or_err(SourceFormatter::format_item_source(item)),
SourceFormatter::extract_doc_and_spec(&s.attrs),
),
_ => {
let definition = self.get_definition_from_detail_store(symbol);
return (module_path, definition, None);
}
};
return (module_path, Some(def), doc);
}
let definition = self.get_definition_from_detail_store(symbol);
(module_path, definition, None)
}
fn get_definition_from_detail_store(&self, symbol: &DiscoveredSymbol) -> Option<String> {
match symbol.kind {
SymbolKind::Function | SymbolKind::Method => {
self.ctx.detail_store.function(symbol.id).map(|d| {
let params: Vec<_> = d
.params
.iter()
.map(|p| format!("{}: {}", p.name, p.ty))
.collect();
let ret = d
.return_type
.as_ref()
.map(|t| format!(" -> {}", t))
.unwrap_or_default();
let async_kw = if d.is_async { "async " } else { "" };
format!(
"{}fn {}({}){}",
async_kw,
symbol.path.name(),
params.join(", "),
ret
)
})
}
SymbolKind::Struct => self.ctx.detail_store.struct_(symbol.id).map(|d| {
let fields: Vec<_> = d
.fields
.iter()
.map(|f| format!(" {}: {},", f.name, f.ty))
.collect();
if fields.is_empty() {
format!("struct {}", symbol.path.name())
} else {
format!(
"struct {} {{\n{}\n}}",
symbol.path.name(),
fields.join("\n")
)
}
}),
SymbolKind::Enum => self.ctx.detail_store.enum_(symbol.id).map(|d| {
let variants: Vec<_> = d
.variants
.iter()
.map(|v| format!(" {},", v.name))
.collect();
format!(
"enum {} {{\n{}\n}}",
symbol.path.name(),
variants.join("\n")
)
}),
SymbolKind::Trait => self
.ctx
.detail_store
.trait_(symbol.id)
.map(|_| format!("trait {} {{ ... }}", symbol.path.name())),
SymbolKind::Mod => {
Some(self.format_module_contents(symbol))
}
_ => None,
}
}
fn format_module_contents(&self, symbol: &DiscoveredSymbol) -> String {
use std::fmt::Write;
let mut items_by_kind: std::collections::BTreeMap<&'static str, Vec<String>> =
std::collections::BTreeMap::new();
let mod_path_str = symbol.path.to_string();
let depth = symbol.path.depth();
for (child_id, child_path) in self.ctx.registry.iter() {
if child_path.depth() == depth + 1 {
let child_path_str = child_path.to_string();
if child_path_str.starts_with(&mod_path_str) {
let kind = self.ctx.registry.kind(child_id).unwrap_or(SymbolKind::Any);
let kind_str = match kind {
SymbolKind::Function => "fn",
SymbolKind::Method => "fn",
SymbolKind::Struct => "struct",
SymbolKind::Enum => "enum",
SymbolKind::Trait => "trait",
SymbolKind::Impl => continue, SymbolKind::Const => "const",
SymbolKind::Static => "static",
SymbolKind::TypeAlias => "type",
SymbolKind::Mod => "mod",
_ => continue,
};
items_by_kind
.entry(kind_str)
.or_default()
.push(child_path.name().to_string());
}
}
}
let mut output = format!("mod {} {{\n", symbol.path.name());
for (kind, names) in items_by_kind {
for name in names.iter().take(10) {
writeln!(output, " {} {};", kind, name).unwrap();
}
if names.len() > 10 {
writeln!(output, " // ... +{} more {}", names.len() - 10, kind).unwrap();
}
}
output.push('}');
output
}
fn get_full_source(&self, symbol: &DiscoveredSymbol) -> Option<String> {
use crate::formatter::SourceFormatter;
use ryo_source::pure::PureItem;
if let Some(item) = self.ctx.ast_registry.get(symbol.id) {
let fmt_or_err =
|r: Result<String, _>| r.unwrap_or_else(|e| format!("<format error: {}>", e));
return match item {
PureItem::Fn(f) => Some(fmt_or_err(SourceFormatter::format_fn_full(f))),
PureItem::Struct(s) => Some(fmt_or_err(SourceFormatter::format_struct(s))),
PureItem::Enum(e) => Some(fmt_or_err(SourceFormatter::format_enum(e))),
PureItem::Trait(t) => Some(fmt_or_err(SourceFormatter::format_trait(t))),
PureItem::Mod(m) => {
if !m.items.is_empty() {
SourceFormatter::format_item_source(item).ok()
} else {
self.get_external_module_source(symbol)
}
}
_ => SourceFormatter::format_item_source(item).ok(),
};
}
if symbol.kind == SymbolKind::Mod {
return self.get_external_module_source(symbol);
}
None
}
fn get_external_module_source(&self, symbol: &DiscoveredSymbol) -> Option<String> {
use crate::formatter::SourceFormatter;
if let Some(children) = self.ctx.ast_registry.get_module_children(symbol.id) {
if let Some(&first_child) = children.first() {
if let Some(child_span) = self.ctx.registry.span(first_child) {
if let Some(file) = self.ctx.files.get(&child_span.file) {
let mut output = String::new();
for item in &file.items {
if let Ok(formatted) = SourceFormatter::format_item_source(item) {
if !output.is_empty() {
output.push_str("\n\n");
}
output.push_str(&formatted);
}
}
if !output.is_empty() {
return Some(output);
}
}
}
}
}
let module_path = symbol.path.module_path();
let module_name = symbol.path.name();
for (file_path, file) in self.ctx.files.iter() {
let path_str = file_path.as_relative().to_string_lossy();
let is_match = path_str.ends_with(&format!("{}.rs", module_name))
|| path_str.ends_with(&format!("{}/mod.rs", module_name));
let crate_name = module_path.split("::").next().unwrap_or("");
let file_crate = file_path.crate_name().as_str();
let crate_matches = crate_name == file_crate
|| crate_name.replace('_', "-") == file_crate
|| crate_name.replace('-', "_") == file_crate;
if is_match && crate_matches {
let mut output = String::new();
for item in &file.items {
if let Ok(formatted) = SourceFormatter::format_item_source(item) {
if !output.is_empty() {
output.push_str("\n\n");
}
output.push_str(&formatted);
}
}
if !output.is_empty() {
return Some(output);
}
}
}
None
}
}
pub fn execute_query(ctx: &AnalysisContext, query: &Query) -> Result<QueryResponse, ExecuteError> {
QueryExecutor::new(ctx).execute(query)
}
pub fn execute_yaml(ctx: &AnalysisContext, yaml: &str) -> Result<QueryResponse, ExecuteError> {
let query = crate::parser::QueryParser::from_yaml(yaml)
.map_err(|e| ExecuteError::Discovery(e.to_string()))?;
execute_query(ctx, &query)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::MatchView;
#[test]
fn test_query_response_structure() {
let response = QueryResponse {
status: QueryStatus::Found,
results: vec![MatchResult {
id: "SymbolId(1v1)".to_string(),
uuid: None,
path: "test::foo".to_string(),
node_kind: "Function".to_string(),
name: "foo".to_string(),
view: MatchView::Snippet {
text: "fn foo() {}".to_string(),
},
}],
suggestions: vec![],
metadata: QueryMetadata {
elapsed_ms: 5,
total_matches: 1,
resolve_status: None,
},
};
assert_eq!(response.status, QueryStatus::Found);
assert_eq!(response.results.len(), 1);
assert_eq!(response.results[0].name, "foo");
}
#[test]
fn test_match_view_variants() {
let snippet = MatchView::Snippet {
text: "fn example() {}".to_string(),
};
assert!(matches!(snippet, MatchView::Snippet { .. }));
let def = MatchView::Def {
module_path: "mylib::handlers".to_string(),
definition: "pub fn handle() -> Result<()>".to_string(),
doc: Some("Handles requests".to_string()),
};
assert!(matches!(def, MatchView::Def { .. }));
let full = MatchView::Full {
module_path: "mylib::handlers".to_string(),
definition: "pub fn handle() -> Result<()>".to_string(),
body: "{ Ok(()) }".to_string(),
doc: None,
};
assert!(matches!(full, MatchView::Full { .. }));
}
}