1use std::time::Instant;
13
14use crate::converter::{CompositeOp, ConversionResult, QueryConverter};
15use crate::schema::{
16 MatchResult, Query, QueryMetadata, QueryResponse, QueryStatus, ResolveConfig, ResolveKind,
17 ResolveStatus, Suggestion, SuggestionKind, ViewMode,
18};
19use ryo_analysis::{AnalysisContext, DiscoveredSymbol, DiscoveryEngine, SymbolId, SymbolKind};
20use thiserror::Error;
21
22#[derive(Debug, Error)]
24pub enum ExecuteError {
25 #[error("conversion error: {0}")]
27 Conversion(#[from] crate::converter::ConvertError),
28
29 #[error("discovery error: {0}")]
31 Discovery(String),
32
33 #[error("post-filter error: {0}")]
35 PostFilter(String),
36}
37
38pub struct QueryExecutor<'a> {
40 ctx: &'a AnalysisContext,
41 view_mode: ViewMode,
42}
43
44impl<'a> QueryExecutor<'a> {
45 pub fn new(ctx: &'a AnalysisContext) -> Self {
47 Self {
48 ctx,
49 view_mode: ViewMode::default(),
50 }
51 }
52
53 pub fn with_view_mode(mut self, mode: ViewMode) -> Self {
55 self.view_mode = mode;
56 self
57 }
58
59 pub fn execute(&self, query: &Query) -> Result<QueryResponse, ExecuteError> {
61 let start = Instant::now();
62 let view_mode = query.view.unwrap_or(self.view_mode);
63
64 let conversion = QueryConverter::to_discovery_query(query)?;
66
67 let (results, status) = self.execute_conversion(&conversion)?;
69
70 let filter_processor = crate::filter::PostFilterProcessor::new(self.ctx);
72 let filtered = filter_processor.apply(results, &conversion.post_filters);
73
74 let (resolved, resolve_status) = if let Some(ref resolve_config) = query.resolve {
76 self.execute_resolve(&filtered, resolve_config)?
77 } else {
78 (filtered, None)
79 };
80
81 let match_results: Vec<MatchResult> = resolved
83 .iter()
84 .map(|s| self.to_match_result(s, view_mode))
85 .collect();
86
87 let (final_results, suggestions, final_status) = if match_results.is_empty() {
89 self.try_recovery(query, &conversion)?
90 } else {
91 (match_results, vec![], status)
92 };
93
94 let limited: Vec<MatchResult> = if let Some(limit) = query.limit {
96 final_results.into_iter().take(limit).collect()
97 } else {
98 final_results
99 };
100
101 let total = limited.len();
102 let elapsed = start.elapsed();
103
104 Ok(QueryResponse {
105 status: final_status,
106 results: limited,
107 suggestions,
108 metadata: QueryMetadata {
109 elapsed_ms: elapsed.as_millis() as u32,
110 total_matches: total,
111 resolve_status,
112 },
113 })
114 }
115
116 fn execute_conversion(
118 &self,
119 conversion: &ConversionResult,
120 ) -> Result<(Vec<DiscoveredSymbol>, QueryStatus), ExecuteError> {
121 if let Some(ref composite) = conversion.composite {
123 let mut all_results: Vec<DiscoveredSymbol> = Vec::new();
124
125 for sub in &composite.queries {
126 let (results, _) = self.execute_conversion(sub)?;
127 match composite.op {
128 CompositeOp::Or => {
129 for r in results {
131 if !all_results.iter().any(|existing| existing.path == r.path) {
132 all_results.push(r);
133 }
134 }
135 }
136 CompositeOp::And => {
137 if all_results.is_empty() {
138 all_results = results;
139 } else {
140 all_results
142 .retain(|existing| results.iter().any(|r| r.path == existing.path));
143 }
144 }
145 }
146 }
147
148 let status = if all_results.is_empty() {
149 QueryStatus::NotFound
150 } else {
151 QueryStatus::Found
152 };
153
154 return Ok((all_results, status));
155 }
156
157 if let Some(ref dq) = conversion.discovery_query {
159 let engine = DiscoveryEngine::new(&self.ctx.code_graph, &self.ctx.registry, None)
161 .set_typeflow(&self.ctx.typeflow_graph);
162 let result = engine.execute(dq);
163
164 let status = if result.symbols.is_empty() {
165 QueryStatus::NotFound
166 } else {
167 QueryStatus::Found
168 };
169
170 Ok((result.symbols, status))
171 } else {
172 Ok((vec![], QueryStatus::NotFound))
173 }
174 }
175
176 fn execute_resolve(
180 &self,
181 symbols: &[DiscoveredSymbol],
182 config: &ResolveConfig,
183 ) -> Result<(Vec<DiscoveredSymbol>, Option<ResolveStatus>), ExecuteError> {
184 let mut resolved_ids: Vec<SymbolId> = Vec::new();
185 let depth = config.depth.unwrap_or(1);
186
187 for symbol in symbols {
189 let related = self.resolve_single(symbol.id, config.kind, depth);
190 for id in related {
191 if !resolved_ids.contains(&id) {
192 resolved_ids.push(id);
193 }
194 }
195 }
196
197 let resolved_symbols: Vec<DiscoveredSymbol> = resolved_ids
199 .into_iter()
200 .filter_map(|id| self.symbol_id_to_discovered(id))
201 .collect();
202
203 Ok((resolved_symbols, Some(ResolveStatus::Complete)))
204 }
205
206 fn resolve_single(&self, id: SymbolId, kind: ResolveKind, depth: usize) -> Vec<SymbolId> {
208 if depth == 0 {
209 return vec![];
210 }
211
212 let direct: Vec<SymbolId> = match kind {
213 ResolveKind::Callers => self.ctx.code_graph.callers_of(id).collect(),
214 ResolveKind::Callees => self.ctx.code_graph.callees_of(id).collect(),
215 ResolveKind::Uses => self.ctx.typeflow_graph.types_used_by(id).collect(),
216 ResolveKind::UsedBy => self.ctx.typeflow_graph.type_users(id).collect(),
217 ResolveKind::Implementations => self.ctx.code_graph.implementors_of(id).collect(),
218 ResolveKind::References => {
219 self.ctx
221 .code_graph
222 .callers_of(id)
223 .chain(self.ctx.typeflow_graph.type_users(id))
224 .collect()
225 }
226 ResolveKind::Definition => {
227 vec![id]
230 }
231 };
232
233 if depth > 1 {
235 let mut all = direct.clone();
236 for child_id in &direct {
237 let deeper = self.resolve_single(*child_id, kind, depth - 1);
238 for d in deeper {
239 if !all.contains(&d) {
240 all.push(d);
241 }
242 }
243 }
244 all
245 } else {
246 direct
247 }
248 }
249
250 fn symbol_id_to_discovered(&self, id: SymbolId) -> Option<DiscoveredSymbol> {
252 let path = self.ctx.registry.resolve(id)?;
253 let kind = self.ctx.registry.kind(id).unwrap_or(SymbolKind::Other);
254 let span = self.ctx.registry.span(id).cloned();
255 let visibility = self.ctx.registry.visibility(id).cloned();
256
257 let mut symbol = DiscoveredSymbol::new(id, path.clone(), kind);
258 if let Some(s) = span {
259 symbol = symbol.with_span(s);
260 }
261 if let Some(v) = visibility {
262 symbol = symbol.with_visibility(v);
263 }
264
265 Some(symbol)
266 }
267
268 fn try_recovery(
272 &self,
273 query: &Query,
274 _conversion: &ConversionResult,
275 ) -> Result<(Vec<MatchResult>, Vec<Suggestion>, QueryStatus), ExecuteError> {
276 let default_recovery = crate::schema::RecoveryStrategy {
278 fuzzy: Some(crate::schema::FuzzyConfig { max_distance: 2 }),
279 split_words: None,
280 enumerate_scope: Some(10),
281 };
282
283 let on_empty = query.r#match.as_ref().and_then(|m| m.on_empty.as_ref());
284 let recovery = on_empty.unwrap_or(&default_recovery);
285 let mut suggestions = Vec::new();
286
287 if recovery.fuzzy.is_some() {
289 suggestions.push(Suggestion {
290 kind: SuggestionKind::Typo,
291 name: "[未実装] fuzzy検索は現在利用できません".to_string(),
292 distance: None,
293 confidence: 0.0,
294 });
295 }
296
297 if recovery.enumerate_scope.is_some() {
299 suggestions.push(Suggestion {
300 kind: SuggestionKind::InScope,
301 name: "[未実装] スコープ内シンボル列挙は現在利用できません".to_string(),
302 distance: None,
303 confidence: 0.0,
304 });
305 }
306
307 let status = if suggestions.is_empty() {
308 QueryStatus::NotFound
309 } else {
310 QueryStatus::Partial
311 };
312
313 Ok((vec![], suggestions, status))
314 }
315
316 pub fn to_match_result(&self, symbol: &DiscoveredSymbol, mode: ViewMode) -> MatchResult {
321 use crate::schema::MatchView;
322
323 let symbol_id = format!("{:?}", symbol.id);
325
326 let view = match mode {
328 ViewMode::Snippet => {
329 let text = format!("// {} at {}", symbol.path.name(), symbol.path);
331 MatchView::Snippet { text }
332 }
333 ViewMode::Precise => MatchView::Precise,
334 ViewMode::Count => {
335 MatchView::Precise
337 }
338 ViewMode::Def => {
339 let (module_path, definition, doc) = self.get_def_info(symbol);
340 MatchView::Def {
341 module_path: module_path.unwrap_or_else(|| symbol.path.module_path()),
342 definition: definition.unwrap_or_else(|| format!("{:?}", symbol.kind)),
343 doc,
344 }
345 }
346 ViewMode::Full => {
347 let (module_path, definition, doc) = self.get_def_info(symbol);
348 let body = self
349 .get_full_source(symbol)
350 .unwrap_or_else(|| "// source not available".to_string());
351 MatchView::Full {
352 module_path: module_path.unwrap_or_else(|| symbol.path.module_path()),
353 definition: definition.unwrap_or_else(|| format!("{:?}", symbol.kind)),
354 body,
355 doc,
356 }
357 }
358 };
359
360 MatchResult {
361 id: symbol_id,
362 uuid: symbol.uuid.map(|u| u.to_string()),
363 path: symbol.path.to_string(),
364 node_kind: format!("{:?}", symbol.kind),
365 name: symbol.path.name().to_string(),
366 view,
367 }
368 }
369
370 pub fn get_def_info_for_symbol(
372 &self,
373 symbol: &DiscoveredSymbol,
374 ) -> (Option<String>, Option<String>, Option<String>) {
375 self.get_def_info(symbol)
376 }
377
378 fn get_def_info(
383 &self,
384 symbol: &DiscoveredSymbol,
385 ) -> (Option<String>, Option<String>, Option<String>) {
386 use crate::formatter::SourceFormatter;
387 use ryo_source::pure::PureItem;
388
389 let module_path = Some(symbol.path.module_path());
390
391 if let Some(item) = self.ctx.ast_registry.get(symbol.id) {
393 let fmt_or_err =
394 |r: Result<String, _>| r.unwrap_or_else(|e| format!("<format error: {}>", e));
395 let (def, doc) = match item {
396 PureItem::Fn(f) => (
397 fmt_or_err(SourceFormatter::format_fn_signature(f)),
398 SourceFormatter::extract_doc_and_spec(&f.attrs),
399 ),
400 PureItem::Struct(s) => (
401 fmt_or_err(SourceFormatter::format_struct(s)),
402 SourceFormatter::extract_doc_and_spec(&s.attrs),
403 ),
404 PureItem::Enum(e) => (
405 fmt_or_err(SourceFormatter::format_enum(e)),
406 SourceFormatter::extract_doc_and_spec(&e.attrs),
407 ),
408 PureItem::Trait(t) => (
409 fmt_or_err(SourceFormatter::format_trait(t)),
410 SourceFormatter::extract_doc_and_spec(&t.attrs),
411 ),
412 PureItem::Mod(_) => {
413 let def = self.format_module_contents(symbol);
414 return (module_path, Some(def), None);
415 }
416 PureItem::Type(t) => (
417 fmt_or_err(SourceFormatter::format_item_source(item)),
418 SourceFormatter::extract_doc_and_spec(&t.attrs),
419 ),
420 PureItem::Const(c) => (
421 fmt_or_err(SourceFormatter::format_item_source(item)),
422 SourceFormatter::extract_doc_and_spec(&c.attrs),
423 ),
424 PureItem::Static(s) => (
425 fmt_or_err(SourceFormatter::format_item_source(item)),
426 SourceFormatter::extract_doc_and_spec(&s.attrs),
427 ),
428 _ => {
429 let definition = self.get_definition_from_detail_store(symbol);
431 return (module_path, definition, None);
432 }
433 };
434 return (module_path, Some(def), doc);
435 }
436
437 let definition = self.get_definition_from_detail_store(symbol);
439 (module_path, definition, None)
440 }
441
442 fn get_definition_from_detail_store(&self, symbol: &DiscoveredSymbol) -> Option<String> {
444 match symbol.kind {
445 SymbolKind::Function | SymbolKind::Method => {
446 self.ctx.detail_store.function(symbol.id).map(|d| {
447 let params: Vec<_> = d
448 .params
449 .iter()
450 .map(|p| format!("{}: {}", p.name, p.ty))
451 .collect();
452 let ret = d
453 .return_type
454 .as_ref()
455 .map(|t| format!(" -> {}", t))
456 .unwrap_or_default();
457 let async_kw = if d.is_async { "async " } else { "" };
458 format!(
459 "{}fn {}({}){}",
460 async_kw,
461 symbol.path.name(),
462 params.join(", "),
463 ret
464 )
465 })
466 }
467 SymbolKind::Struct => self.ctx.detail_store.struct_(symbol.id).map(|d| {
468 let fields: Vec<_> = d
469 .fields
470 .iter()
471 .map(|f| format!(" {}: {},", f.name, f.ty))
472 .collect();
473 if fields.is_empty() {
474 format!("struct {}", symbol.path.name())
475 } else {
476 format!(
477 "struct {} {{\n{}\n}}",
478 symbol.path.name(),
479 fields.join("\n")
480 )
481 }
482 }),
483 SymbolKind::Enum => self.ctx.detail_store.enum_(symbol.id).map(|d| {
484 let variants: Vec<_> = d
485 .variants
486 .iter()
487 .map(|v| format!(" {},", v.name))
488 .collect();
489 format!(
490 "enum {} {{\n{}\n}}",
491 symbol.path.name(),
492 variants.join("\n")
493 )
494 }),
495 SymbolKind::Trait => self
496 .ctx
497 .detail_store
498 .trait_(symbol.id)
499 .map(|_| format!("trait {} {{ ... }}", symbol.path.name())),
500 SymbolKind::Mod => {
501 Some(self.format_module_contents(symbol))
503 }
504 _ => None,
505 }
506 }
507
508 fn format_module_contents(&self, symbol: &DiscoveredSymbol) -> String {
510 use std::fmt::Write;
511
512 let mut items_by_kind: std::collections::BTreeMap<&'static str, Vec<String>> =
513 std::collections::BTreeMap::new();
514
515 let mod_path_str = symbol.path.to_string();
517 let depth = symbol.path.depth();
518
519 for (child_id, child_path) in self.ctx.registry.iter() {
521 if child_path.depth() == depth + 1 {
523 let child_path_str = child_path.to_string();
524 if child_path_str.starts_with(&mod_path_str) {
525 let kind = self.ctx.registry.kind(child_id).unwrap_or(SymbolKind::Any);
526 let kind_str = match kind {
527 SymbolKind::Function => "fn",
528 SymbolKind::Method => "fn",
529 SymbolKind::Struct => "struct",
530 SymbolKind::Enum => "enum",
531 SymbolKind::Trait => "trait",
532 SymbolKind::Impl => continue, SymbolKind::Const => "const",
534 SymbolKind::Static => "static",
535 SymbolKind::TypeAlias => "type",
536 SymbolKind::Mod => "mod",
537 _ => continue,
538 };
539 items_by_kind
540 .entry(kind_str)
541 .or_default()
542 .push(child_path.name().to_string());
543 }
544 }
545 }
546
547 let mut output = format!("mod {} {{\n", symbol.path.name());
548 for (kind, names) in items_by_kind {
549 for name in names.iter().take(10) {
550 writeln!(output, " {} {};", kind, name).unwrap();
551 }
552 if names.len() > 10 {
553 writeln!(output, " // ... +{} more {}", names.len() - 10, kind).unwrap();
554 }
555 }
556 output.push('}');
557 output
558 }
559
560 fn get_full_source(&self, symbol: &DiscoveredSymbol) -> Option<String> {
565 use crate::formatter::SourceFormatter;
566 use ryo_source::pure::PureItem;
567
568 if let Some(item) = self.ctx.ast_registry.get(symbol.id) {
570 let fmt_or_err =
571 |r: Result<String, _>| r.unwrap_or_else(|e| format!("<format error: {}>", e));
572 return match item {
573 PureItem::Fn(f) => Some(fmt_or_err(SourceFormatter::format_fn_full(f))),
574 PureItem::Struct(s) => Some(fmt_or_err(SourceFormatter::format_struct(s))),
575 PureItem::Enum(e) => Some(fmt_or_err(SourceFormatter::format_enum(e))),
576 PureItem::Trait(t) => Some(fmt_or_err(SourceFormatter::format_trait(t))),
577 PureItem::Mod(m) => {
578 if !m.items.is_empty() {
579 SourceFormatter::format_item_source(item).ok()
581 } else {
582 self.get_external_module_source(symbol)
584 }
585 }
586 _ => SourceFormatter::format_item_source(item).ok(),
587 };
588 }
589
590 if symbol.kind == SymbolKind::Mod {
592 return self.get_external_module_source(symbol);
593 }
594
595 None
596 }
597
598 fn get_external_module_source(&self, symbol: &DiscoveredSymbol) -> Option<String> {
603 use crate::formatter::SourceFormatter;
604
605 if let Some(children) = self.ctx.ast_registry.get_module_children(symbol.id) {
609 if let Some(&first_child) = children.first() {
610 if let Some(child_span) = self.ctx.registry.span(first_child) {
611 if let Some(file) = self.ctx.files.get(&child_span.file) {
612 let mut output = String::new();
613 for item in &file.items {
614 if let Ok(formatted) = SourceFormatter::format_item_source(item) {
615 if !output.is_empty() {
616 output.push_str("\n\n");
617 }
618 output.push_str(&formatted);
619 }
620 }
621 if !output.is_empty() {
622 return Some(output);
623 }
624 }
625 }
626 }
627 }
628
629 let module_path = symbol.path.module_path();
631 let module_name = symbol.path.name();
632
633 for (file_path, file) in self.ctx.files.iter() {
634 let path_str = file_path.as_relative().to_string_lossy();
635
636 let is_match = path_str.ends_with(&format!("{}.rs", module_name))
637 || path_str.ends_with(&format!("{}/mod.rs", module_name));
638
639 let crate_name = module_path.split("::").next().unwrap_or("");
640 let file_crate = file_path.crate_name().as_str();
641 let crate_matches = crate_name == file_crate
642 || crate_name.replace('_', "-") == file_crate
643 || crate_name.replace('-', "_") == file_crate;
644
645 if is_match && crate_matches {
646 let mut output = String::new();
647 for item in &file.items {
648 if let Ok(formatted) = SourceFormatter::format_item_source(item) {
649 if !output.is_empty() {
650 output.push_str("\n\n");
651 }
652 output.push_str(&formatted);
653 }
654 }
655 if !output.is_empty() {
656 return Some(output);
657 }
658 }
659 }
660 None
661 }
662}
663
664pub fn execute_query(ctx: &AnalysisContext, query: &Query) -> Result<QueryResponse, ExecuteError> {
668 QueryExecutor::new(ctx).execute(query)
669}
670
671pub fn execute_yaml(ctx: &AnalysisContext, yaml: &str) -> Result<QueryResponse, ExecuteError> {
673 let query = crate::parser::QueryParser::from_yaml(yaml)
674 .map_err(|e| ExecuteError::Discovery(e.to_string()))?;
675 execute_query(ctx, &query)
676}
677
678#[cfg(test)]
679mod tests {
680 use super::*;
681 use crate::schema::MatchView;
682
683 #[test]
687 fn test_query_response_structure() {
688 let response = QueryResponse {
689 status: QueryStatus::Found,
690 results: vec![MatchResult {
691 id: "SymbolId(1v1)".to_string(),
692 uuid: None,
693 path: "test::foo".to_string(),
694 node_kind: "Function".to_string(),
695 name: "foo".to_string(),
696 view: MatchView::Snippet {
697 text: "fn foo() {}".to_string(),
698 },
699 }],
700 suggestions: vec![],
701 metadata: QueryMetadata {
702 elapsed_ms: 5,
703 total_matches: 1,
704 resolve_status: None,
705 },
706 };
707
708 assert_eq!(response.status, QueryStatus::Found);
709 assert_eq!(response.results.len(), 1);
710 assert_eq!(response.results[0].name, "foo");
711 }
712
713 #[test]
714 fn test_match_view_variants() {
715 let snippet = MatchView::Snippet {
717 text: "fn example() {}".to_string(),
718 };
719 assert!(matches!(snippet, MatchView::Snippet { .. }));
720
721 let def = MatchView::Def {
723 module_path: "mylib::handlers".to_string(),
724 definition: "pub fn handle() -> Result<()>".to_string(),
725 doc: Some("Handles requests".to_string()),
726 };
727 assert!(matches!(def, MatchView::Def { .. }));
728
729 let full = MatchView::Full {
731 module_path: "mylib::handlers".to_string(),
732 definition: "pub fn handle() -> Result<()>".to_string(),
733 body: "{ Ok(()) }".to_string(),
734 doc: None,
735 };
736 assert!(matches!(full, MatchView::Full { .. }));
737 }
738}