Skip to main content

ryo_query_language/
executor.rs

1//! RyoQL Executor - クエリ実行オーケストレーション
2//!
3//! RyoQLクエリをDiscoveryEngineで実行し、結果を変換して返す。
4//!
5//! ## 責務
6//! 1. Query → DiscoveryQuery変換
7//! 2. DiscoveryEngine実行
8//! 3. 後処理フィルタ適用
9//! 4. 結果をQueryResponseに変換
10//! 5. on_emptyリカバリー
11
12use 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/// 実行エラー
23#[derive(Debug, Error)]
24pub enum ExecuteError {
25    /// クエリ変換段階の失敗。
26    #[error("conversion error: {0}")]
27    Conversion(#[from] crate::converter::ConvertError),
28
29    /// `DiscoveryEngine` 呼び出し失敗。
30    #[error("discovery error: {0}")]
31    Discovery(String),
32
33    /// 後処理フィルタ適用失敗。
34    #[error("post-filter error: {0}")]
35    PostFilter(String),
36}
37
38/// クエリ実行器
39pub struct QueryExecutor<'a> {
40    ctx: &'a AnalysisContext,
41    view_mode: ViewMode,
42}
43
44impl<'a> QueryExecutor<'a> {
45    /// 新しいExecutorを作成
46    pub fn new(ctx: &'a AnalysisContext) -> Self {
47        Self {
48            ctx,
49            view_mode: ViewMode::default(),
50        }
51    }
52
53    /// ViewModeを設定
54    pub fn with_view_mode(mut self, mode: ViewMode) -> Self {
55        self.view_mode = mode;
56        self
57    }
58
59    /// クエリを実行
60    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        // 1. 変換
65        let conversion = QueryConverter::to_discovery_query(query)?;
66
67        // 2. 実行
68        let (results, status) = self.execute_conversion(&conversion)?;
69
70        // 3. 後処理フィルタ
71        let filter_processor = crate::filter::PostFilterProcessor::new(self.ctx);
72        let filtered = filter_processor.apply(results, &conversion.post_filters);
73
74        // 4. Resolve処理(指定されている場合)
75        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        // 5. ViewMode適用してMatchResultに変換
82        let match_results: Vec<MatchResult> = resolved
83            .iter()
84            .map(|s| self.to_match_result(s, view_mode))
85            .collect();
86
87        // 6. on_emptyリカバリー
88        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        // 7. limit適用
95        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    /// ConversionResultを実行
117    fn execute_conversion(
118        &self,
119        conversion: &ConversionResult,
120    ) -> Result<(Vec<DiscoveredSymbol>, QueryStatus), ExecuteError> {
121        // 複合クエリの場合
122        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                        // Or: マージ(重複排除)
130                        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                            // And: intersection
141                            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        // 単純クエリの場合
158        if let Some(ref dq) = conversion.discovery_query {
159            // TypeFlowGraphがあれば使用
160            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    /// Resolveクエリを実行
177    ///
178    /// 初期結果の各シンボルに対して、指定された関係のシンボルを検索する。
179    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        // 各シンボルに対してresolve
188        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        // SymbolIdをDiscoveredSymbolに変換
198        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    /// 単一シンボルのresolve
207    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                // References = 参照箇所 = callers + type users
220                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                // Definition = 定義元(id自身、または継承元など)
228                // 現時点では元のシンボルを返す
229                vec![id]
230            }
231        };
232
233        // depth > 1 の場合は再帰的に探索
234        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    /// SymbolId → DiscoveredSymbol 変換
251    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    /// on_emptyリカバリーを試行
269    ///
270    /// デフォルトでsuggestが有効(UX向上のため)
271    fn try_recovery(
272        &self,
273        query: &Query,
274        _conversion: &ConversionResult,
275    ) -> Result<(Vec<MatchResult>, Vec<Suggestion>, QueryStatus), ExecuteError> {
276        // デフォルトでsuggestを有効にする(UX向上)
277        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        // fuzzy検索(未実装 - ryo-fuzzy-parserのdistance.rsを使って実装予定)
288        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        // enumerate_scope(未実装 - スコープ内シンボル列挙)
298        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    /// DiscoveredSymbol → MatchResult 変換 (公開API)
317    ///
318    /// CLI等から直接呼び出せるように公開。パターン検索結果を任意のViewModeで
319    /// MatchResultに変換する。
320    pub fn to_match_result(&self, symbol: &DiscoveredSymbol, mode: ViewMode) -> MatchResult {
321        use crate::schema::MatchView;
322
323        // SymbolId: slotmapのキー形式
324        let symbol_id = format!("{:?}", symbol.id);
325
326        // ViewMode別のデータを構築
327        let view = match mode {
328            ViewMode::Snippet => {
329                // TODO: ソースコードスニペットを取得
330                let text = format!("// {} at {}", symbol.path.name(), symbol.path);
331                MatchView::Snippet { text }
332            }
333            ViewMode::Precise => MatchView::Precise,
334            ViewMode::Count => {
335                // CountモードではMatchResult自体を生成しないはずだが、フォールバック
336                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    /// Defモード用: 定義詳細情報を取得(公開API)
371    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    /// Defモード用: 定義詳細情報を取得
379    ///
380    /// ASTRegistry から SymbolId で O(1) 直接取得。
381    /// 全ファイル走査や名前マッチは行わない。
382    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        // ASTRegistry: SymbolId → PureItem (O(1))
392        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                    // Impl, Use, etc. — use DetailStore fallback
430                    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        // フォールバック: DetailStoreから取得
438        let definition = self.get_definition_from_detail_store(symbol);
439        (module_path, definition, None)
440    }
441
442    /// DetailStoreから定義を取得(フォールバック)
443    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                // モジュール内のアイテム一覧を表示
502                Some(self.format_module_contents(symbol))
503            }
504            _ => None,
505        }
506    }
507
508    /// モジュール内のアイテムを一覧表示
509    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        // このモジュールのパスプレフィックス
516        let mod_path_str = symbol.path.to_string();
517        let depth = symbol.path.depth();
518
519        // レジストリから直接の子シンボルを取得
520        for (child_id, child_path) in self.ctx.registry.iter() {
521            // 直接の子かチェック(パスの深さが1つだけ深い && プレフィックスが一致)
522            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, // implは省略
533                        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    /// Full: 関数bodyを含む完全なソースを取得
561    ///
562    /// ASTRegistry から SymbolId で O(1) 直接取得。
563    /// 外部モジュールのみ registry.span() → files lookup で取得。
564    fn get_full_source(&self, symbol: &DiscoveredSymbol) -> Option<String> {
565        use crate::formatter::SourceFormatter;
566        use ryo_source::pure::PureItem;
567
568        // ASTRegistry: SymbolId → PureItem (O(1))
569        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                        // Inline module: format from AST
580                        SourceFormatter::format_item_source(item).ok()
581                    } else {
582                        // External module (mod foo;): find source file
583                        self.get_external_module_source(symbol)
584                    }
585                }
586                _ => SourceFormatter::format_item_source(item).ok(),
587            };
588        }
589
590        // ASTRegistry に未登録の場合: 外部モジュールの可能性
591        if symbol.kind == SymbolKind::Mod {
592            return self.get_external_module_source(symbol);
593        }
594
595        None
596    }
597
598    /// Get source for external module (mod foo;) by finding the corresponding file.
599    ///
600    /// SymbolId → registry.span() → FileSpan.file でファイル特定を試み、
601    /// 該当しない場合のみモジュール名ベースのフォールバックを行う。
602    fn get_external_module_source(&self, symbol: &DiscoveredSymbol) -> Option<String> {
603        use crate::formatter::SourceFormatter;
604
605        // 1. span → file で O(1) 特定を試みる
606        //    ただし mod 宣言の span は宣言側ファイルを指すため、
607        //    module_children 経由で子シンボルのファイルを探す。
608        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        // 2. フォールバック: モジュール名ベースでファイル検索
630        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
664/// 簡易実行関数
665///
666/// AnalysisContextがある場合に、クエリを直接実行する。
667pub fn execute_query(ctx: &AnalysisContext, query: &Query) -> Result<QueryResponse, ExecuteError> {
668    QueryExecutor::new(ctx).execute(query)
669}
670
671/// YAMLからクエリを実行
672pub 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    // Note: 実際のテストにはAnalysisContextが必要
684    // ここでは変換〜実行フローの型チェックのみ
685
686    #[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        // Snippet
716        let snippet = MatchView::Snippet {
717            text: "fn example() {}".to_string(),
718        };
719        assert!(matches!(snippet, MatchView::Snippet { .. }));
720
721        // Def
722        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        // Full
730        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}